Emails API
POST /api/v1/emails
Compose an email to one contact. The email is saved as a DRAFT and grouped under a campaign. Requires the write:emails scope (or *).
Draft-first
This call never sends. Review the draft at its reviewUrl, or release it with POST /api/v1/emails/:id/send.
Request
POST /api/v1/emails HTTP/1.1
Host: agentlyleads.com
Authorization: Bearer alk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Content-Type: application/json
{
"contactExternalId": "hs_contact_123",
"subject": "Pickleball sets, 15,830 sets, in Los Angeles",
"bodyHtml": "<p>{{firstName}},</p><p>Branded pickleball sets already in the US...</p>",
"campaign": "DEAL-1995 Pickleball outreach",
"from": "kevin@example.com",
"contactContext": "Pitched pickleball 2026-09-08 at $5.00. Next: his volume."
}Body
| Field | Description |
|---|---|
contactId or contactExternalId | The recipient. One is required. The To address is always the contact's own email on file, never a caller-supplied address. |
subject | Required. {{firstName}} and {{company}} are replaced at send time. |
bodyHtml | Required. Email-client-safe HTML: inline styles only, no <style> blocks or classes. Same tokens as subject. |
campaign | A short title to group related emails under. Reused if it exists. Omit to file under the standing "Direct Outreach" campaign. |
from | A sending identity from GET /api/v1/emails/identities. Any other address is rejected with 400. |
offeringId | Link the email to an offering so it shows in that offering's communication view. |
contactContext | A compact rolling summary saved on the contact (aiContext, 4,000 characters max). Merge the existing context with what this email did and the next step. |
attachments | Up to 10. Each has fileName and exactly one of contentBase64 (10 MB decoded max) or a public https url the server downloads. |
asUserEmail | Act as this workspace member. Defaults to the workspace owner. |
Response — 201
{
"email": {
"id": "cmtsv6ts404hzii01dnmu4k5t",
"status": "DRAFT",
"direction": "OUTBOUND",
"subject": "Pickleball sets, 15,830 sets, in Los Angeles",
"toEmail": "jane@buyer.test",
"fromEmail": "kevin@example.com",
"contactId": "cmr8dlczc00xkic01prvioybz",
"campaignId": "cmtsv6tr704hxii01xyz",
"campaign": { "id": "cmtsv6tr704hxii01xyz", "name": "DEAL-1995 Pickleball outreach" },
"attachmentCount": 0,
"reviewUrl": "https://agentlyleads.com/emails/cmtsv6ts404hzii01dnmu4k5t",
"createdAt": "2026-09-08T16:16:05.256Z"
},
"contact": { "id": "cmr8dlczc00xkic01prvioybz", "name": "Jane Buyer", "email": "jane@buyer.test" },
"contextSaved": true,
"otherPendingDrafts": []
}otherPendingDrafts lists up to five other drafts already waiting on the same contact, so a superseded draft is not left behind to be sent by mistake. Delete one with DELETE /api/v1/emails/:id.
Reply in a thread
To continue an existing conversation instead of starting a new one, use POST /api/v1/emails/:id/reply with the id of the email being answered. The reply is drafted to that email's contact, with a Re: subject, threaded onto the original.
POST /api/v1/emails/cmtsy2o2v05peii01g2ebysb0/reply HTTP/1.1
Authorization: Bearer alk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Content-Type: application/json
{ "bodyHtml": "<p>Fair enough. What does the take-all need to be a pack?</p>" }Returns 201 with the same email object plus inReplyTo.