A certificate agent that gets the ACORD 25 right and then answers from agent@youragency.com in a brand-new email has failed. The general contractor's AP clerk sent the request from a thread that already has four messages in it. She will look for the answer there. If it arrives as a fresh message from an address nobody recognizes, it goes to clutter, and three days later she calls the account manager anyway.
This is the last hundred feet of every mailbox-driven agent we build, and it is the part demos skip. The agent drafts. A licensed person approves. Then the approved text has to leave from the mailbox the requester wrote to, in the thread she wrote in, with the attachment, and the send has to be recorded in the AMS exactly once.
This tutorial covers the send path on Microsoft 365 with Microsoft Graph. It assumes you already read the mailbox and stored the message ID with the draft, which is the earlier tutorial on reading a certificates mailbox. Google Workspace has the same shape with different verbs; the threading rules at the end apply to both.
Do not build the reply yourself
The temptation is to compose a new message, set the subject to RE: plus the original subject, and add the requester as the recipient. Outlook and Gmail will sometimes thread that. Sometimes they will not, because threading is driven by the In-Reply-To and References headers, not by the subject line.
Graph will do the header work for you. Call createReply on the original message and you get back a draft that already carries the conversation ID, the reply headers, the quoted body, and the recipient list. You then patch your approved text and the attachment onto that draft and send it.
POST /v1.0/users/certificates@youragency.com/messages/{originalMessageId}/createReply
The response is a message resource with an ID of its own, sitting in Drafts. Nothing has left the building yet.
Two details to get right immediately:
createReplyreplies to the sender only.createReplyAllkeeps the carbon copies. For a certificate request that came in with the insured's controller copied, reply-all is almost always what the account manager expects. Make it a per-agency setting, not a code decision.- The message ID you stored can go stale. Graph message IDs change if the item is moved between folders, and mailbox rules move things. If the original request is auto-filed into a subfolder between intake and approval, your
createReplyreturns 404. Store theinternetMessageIdalongside the Graph ID and, on 404, re-find the message with$filter=internetMessageId eq '<...>'before you give up.
Patch the approved body in, keep the quote
The draft body Graph hands you is the quoted original wrapped in HTML. Prepend the approved text above the quote rather than replacing the whole body, because the quote is what makes the thread readable when it gets forwarded to the project manager who actually asked for the certificate.
PATCH /v1.0/users/certificates@youragency.com/messages/{replyDraftId}
Content-Type: application/json
{
"body": {
"contentType": "HTML",
"content": "<div>{{approvedHtml}}</div><hr>{{originalDraftBody}}"
}
}
The approved HTML is the CSR-approved text, exactly as approved. Do not re-run a model after the approval gate to "polish" it. If anything regenerates between the approval click and the send, the thing you have on file as approved is not the thing that went out, and that is the one fact an E&O carrier will ask you to prove. Version the body, store its hash with the approval record, and compare the hash before sending.
Attach the certificate
Under 3 MB, attach inline. An ACORD 25 is a one-page PDF, so this covers nearly everything.
POST /v1.0/users/certificates@youragency.com/messages/{replyDraftId}/attachments
Content-Type: application/json
{
"@odata.type": "#microsoft.graph.fileAttachment",
"name": "ACORD25-Riverbend-Mechanical-2026.pdf",
"contentBytes": "{{base64Pdf}}"
}
Over 3 MB, which a loss-run bundle or a 40-page policy copy will be, use an upload session: POST .../attachments/createUploadSession, then PUT chunks with Content-Range. Write the chunk loop once and share it; it is the same code your intake side already needs for large inbound documents.
Name the file the way a stranger's AP inbox would want it named: form number, insured, effective year. certificate_final_v3.pdf gets rejected by exactly the people you are trying to satisfy.
Send exactly once
POST /v1.0/users/certificates@youragency.com/messages/{replyDraftId}/send
A 202 means accepted. It does not mean delivered, and a timeout on your side does not mean it failed. That combination is how an agency ends up sending the same certificate twice to a holder, which looks sloppy at best and, on a corrected certificate, actively confusing.
The defensive pattern:
- Before creating the reply draft, write a row keyed on
approval_idwithsend_state = 'pending'and the draft ID once you have it. - On a send timeout or 5xx, do not blind-retry the
/sendcall. Re-read the draft ID withGET /messages/{replyDraftId}. If it is gone from Drafts, the send went through; look for it in Sent Items, filtered byinternetMessageId, and mark the rowsent. - Only if the draft is still in Drafts do you retry the send.
That inversion, check the mailbox rather than trust your own retry logic, is the same reasoning as idempotent AMS write-back. The mailbox is the system of record for what was sent. Your database is a claim about it.
Permissions, and why Mail.Send is the wrong scope
Application permission Mail.Send grants send-as on every mailbox in the tenant. No agency IT director should approve that, and you should not ask for it. Use an application access policy to scope the app registration to the specific mailboxes the agent touches:
New-ApplicationAccessPolicy -AppId <app-id> `
-PolicyScopeGroupId agent-mailboxes@youragency.com `
-AccessRight RestrictAccess `
-Description "Bindmatic agent: certificates and service mailboxes only"
Then verify it with Test-ApplicationAccessPolicy against a mailbox that should be off limits, and screenshot the denial for the security review. Expect one round of questions from the agency's IT provider; having the policy already in place turns that from a three-week negotiation into a twenty-minute call.
One more thing worth deciding up front: whether the reply comes from a shared mailbox (certificates@) or send-on-behalf of the approving CSR. Shared mailbox is cleaner operationally, since coverage does not break when someone is out, and it keeps the audit trail in one place. Send-on-behalf reads better to the holder. We default to the shared mailbox with the CSR's name in the signature block, and we let the agency overrule that.
File it in the AMS in the same transaction of thought
The send is not done until the AMS says it happened. After the mailbox confirms sent, write the activity: the Epic activity, HawkSoft log note, or EZLynx task, attach the PDF to the policy, and record who approved it and when. Include the internetMessageId in the note body. Six months later, when the holder claims they never got the certificate, that string is what lets someone find the exact message in Sent Items in under a minute.
What this does not do
It does not decide whether the certificate was correct; a licensed person did that at the approval gate. It does not chase a bounce, though it should surface one, so subscribe to the mailbox for delivery failures and route them back to the account owner as a service request rather than letting them die in certificates@. And it does not send anything without an approval record whose hash matches the body. That last constraint is worth enforcing in code, not in policy.
If you are building the mailbox side of a COI or service-request agent on Applied Epic, HawkSoft, or EZLynx, tell us which AMS you run and where the volume is, and a senior engineer will reply within one business day.