Sending mail
Queue a message through the outbox, follow its delivery, schedule it, and undo or cancel it.
Send a message from a connected account with one outbox request. This page covers single sends. For other jobs, see:
- Drafts: compose in the user's mailbox and send later.
- Mail merge: one personalized message per recipient in one call.
- Templates: stored, reusable content.
- Tracking: open and click tracking, unsubscribe links.
- Suppression lists: who bulk mail must skip.
Send a message
A send queues an outbox entry for the account and returns 201 at once. The account's provider (Gmail, Microsoft 365 or its SMTP server) sends it in the background.
The key needs the send scope. An account token for that account also works.
curl -X POST "https://api.workwae.com/v1/accounts/$ACCOUNT_ID/outbox" \
-H "Authorization: Bearer $WORKWAE_API_KEY" \
-H "content-type: application/json" \
-d '{
"to": [{ "name": "Ada Lovelace", "address": "ada@example.com" }],
"subject": "Your invoice",
"text": "Hi Ada, your invoice is attached.",
"html": "<p>Hi Ada, your invoice is attached.</p>",
"reference": "invoice-1042"
}'Request fields
| Field | Meaning |
|---|---|
to, cc, bcc, replyTo | Arrays of { name?, address }, up to 500 each. At least one of to, cc or bcc is required (no_recipients otherwise). |
subject, text, html | The content. With format: "markdown", html holds Markdown and is converted to HTML. |
attachments | Up to 50 files; see Attachments. |
from | One of the account's send-as identities (GET /v1/accounts/{accountId}/identities). Omitted, it sends as the account itself. |
signature | Appends the sender identity's signature. Omitted, it follows the account's signByDefault mail setting. |
inReplyToMessageId | The Workwae id of the message being answered. Sets In-Reply-To and References so the reply stays in its thread. |
priority | high, normal (default) or low, written as the provider's priority headers. |
reference | Your own identifier, echoed in the send.* events. |
sendAt | Schedule the send; see Scheduling and undo. |
template, render | Render stored or inline content; see Templates. |
tracking | Open and click tracking; see Tracking. |
listId | A suppression list for bulk mail; see Tracking. |
An unknown field is refused, not ignored. Every refusal and its reason is listed under Send a message in the API reference.
Attachments
attachments takes up to 50 objects of { filename, contentType, content }, with content in base64. To show one inline, set its contentId and reference it from the HTML as cid:....
await workwae.outbox.create({
accountId,
to: [{ address: "ada@example.com" }],
subject: "Your invoice",
html: '<p>Invoice attached.</p><img src="cid:logo">',
attachments: [
{ filename: "invoice.pdf", contentType: "application/pdf", content: pdfBase64 },
{ filename: "logo.png", contentType: "image/png", content: logoBase64, contentId: "logo" },
],
});Delivery and events
The entry's status moves from queued to sending, then ends sent, failed or cancelled. Follow it with webhooks or the change feed. Each event carries the entry's id as outboxId and your reference.
| Event | When |
|---|---|
send.sent | The provider accepted the message. |
send.retrying | An attempt failed; the entry will be tried again. |
send.failed | The entry failed for good. |
send.rescheduled | sendAt was moved. |
message.bounced | A bounce for the sent message arrived later. |
- Delivery is at least once: a retry first looks for a copy that already went out.
send.failedwith codeoutcome_unknownmeans the server may have accepted the message but its reply was lost. Workwae does not send it again; decide yourself whether to resend.GET /v1/accounts/{accountId}/outboxlists entries newest first (filter withstatus).GET .../outbox/{outboxId}reads one.- The sent copy indexed from the account's Sent folder carries the same
outboxId.
Full payloads are in the mail events catalog.
Scheduling and undo
sendAtschedules a send. Omitted or in the past, it goes as soon as possible.- The account's
undoSendSecondssetting (0 to 60) holds every send at least that long. - While an entry waits,
undoUntilsays until when cancelling undoes it.
# Move a queued send
curl -X PATCH "https://api.workwae.com/v1/accounts/$ACCOUNT_ID/outbox/$OUTBOX_ID" \
-H "Authorization: Bearer $WORKWAE_API_KEY" \
-H "content-type: application/json" \
-d '{ "sendAt": "2026-11-02T09:00:00Z" }'
# Cancel (undo) it
curl -X DELETE "https://api.workwae.com/v1/accounts/$ACCOUNT_ID/outbox/$OUTBOX_ID" \
-H "Authorization: Bearer $WORKWAE_API_KEY"Once it is sending, it is too late
Cancelling or rescheduling an entry that is no longer queued returns 409 send_not_cancellable or send_not_reschedulable. The message says which state it is in.
Send limits
- The tenant's plan caps the sends it may queue per day (UTC). Past the cap, a send returns 403
plan_limit_sendsand a mail merge entry fails with that reason.GET /v1/usageshows the cap (sendsPerDay) andsendsToday. - A tenant whose payment is past due beyond the grace period is refused new sends with 402
billing_past_due. Sends already queued still go out.
Details are in Daily sends; request rate limits and other plan limits are in Rate limits.