POST/emails/send

POST /emails/send

Send a single transactional email, immediately or at a scheduled time.

View as Markdown

Request

curl -X POST 'https://api.sendheron.com/api/v1/emails/send' \
  -H 'Authorization: Bearer <YOUR_API_KEY>' \
  -H 'Content-Type: application/json'

Required scopes

The key must carry these:

  • emails:send

Body parameters

NameTypeRequiredDescription
tostringYesRecipient address.
subjectstringYesSubject line.
htmlstringYesBody as HTML.
fromstringNoSender address. Defaults to the workspace's configured sender.
fromNamestringNoDisplay name for this send, shown beside the address and overriding the sender identity's. The address is what domain verification gates, so the name may vary per send: one verified address, several product names. At most 100 characters, with no line breaks and no angle brackets.
replyTostringNoReply-To address.
ccstring[]NoUp to 50 addresses. Each is checked against the suppression list on its own, because a cc is a real delivery, and a blocked copy is dropped without failing the message.
bccstring[]NoUp to 50 addresses, suppression-checked the same way, and a blocked copy is likewise dropped rather than failing the message.
headersobjectNoCustom headers. Headers the platform owns (List-Unsubscribe, authentication, envelope) are rejected rather than quietly dropped.
sendAtISO-8601 datetimeNoSchedule the send instead of dispatching it now. Must be in the future. Cancel before it fires with DELETE /emails/scheduled/{id}. Cannot be combined with attachments.
attachmentsobject[]NoAt most 10 per message and 10 MB of decoded bytes across all of them together, in the same shape SendGrid takes. Transactional sends only, and this route always qualifies. Cannot be combined with sendAt. The nested fields are documented below.
attachments[].contentstringYesFile content, base64-encoded. The bytes are passed straight to the provider and never stored, and they are deliberately excluded from the idempotency-key payload comparison: a retried job that regenerated the same file (a fresh creation timestamp inside the PDF) replays the original response instead of failing the key with a 409. filename and type still count towards that comparison.
attachments[].filenamestringYesFilename shown to the recipient, e.g. receipt-1234.pdf.
attachments[].typestringYesMIME type, from the allowlist: application/pdf, image/png, image/jpeg, image/gif, image/webp, text/calendar, text/csv, text/plain, application/zip. Anything else is refused.
attachments[].dispositionstringNoOnly attachment is accepted. The field exists for SendGrid payload compatibility; inline (CID) images are not supported.

Responses

Returns 201 on success.

400
The payload failed validation, or the request is not valid for the current state. The body's `description` names the field and says what was wrong with it in words, e.g. "domain: domain must be a bare hostname such as acme.com, with no scheme, path, or port". The `error` code is stable and safe to branch on; `description` is for the human reading the log.
401
Missing or invalid API key.
403
Valid key, but it does not carry the required scope. The `error` code is `apiKeys.insufficientScopes` and the `description` names both halves of the problem: "Missing required scope(s): X. This key holds: Y." Neither is a secret, since the required scopes are on this page and the held ones are your own credential, and a bare "Forbidden" costs a debugging pass to work out which of the two it was.
404
No such record in this workspace. An id belonging to a different workspace returns this too, never a 403, because the API will not confirm that a record exists outside the workspace your key was issued in. Read it as "not yours or not there" rather than as "definitely gone".
409
A duplicate, or an idempotency key reused with a different payload.
429
Either rate-limit ceiling was exceeded.
400
`emailSending.tooManyAttachments`, `emailSending.attachmentsTooLarge` or `emailSending.attachmentTypeNotAllowed`: an attachment cap or the MIME allowlist was breached. The keys are stable, so branch on them rather than on the text.
400
`emailSending.attachmentsNotSchedulable`: `attachments` and `sendAt` in the same request. Attachment bytes are never stored, so a scheduled send would have nothing left to attach when it fires.
403
subscription.transactionalGraceExhausted: the pooled monthly allowance plus the 10% transactional fair-use grace is spent. Marketing routes stop at the pool itself; this route keeps flowing for another 10% of the pool, then refuses. A lapsed subscription does not trigger this on its own: transactional mail from a lapsed account still runs, within the smallest plan's grace.
503
`emailSending.sendFailed`: the provider refused the send. The FAILED attempt is recorded and the idempotency key is released, so retry with the SAME key.

Rate limits

100/min per key, counted against the organization's SEND ceiling (200/min). Both windows are one minute. There is no hourly or daily quota.