Send a message
Idempotency, limits and retries are covered once, in the Send API guide.
Sends one message through the organization's AWS SES account.
Two body formats. Structured JSON (Content-Type: application/json)
is the documented default: SendOps builds the MIME message from
from, to, subject, html/text, attachments and headers, or
renders a stored template with variables. Raw MIME
(Content-Type: message/rfc822) is also accepted for senders that
already build their own messages, such as an SMTP relay or a migration
from SendRawEmail; the body is sent as-is, with envelope recipients
taken from its To, Cc and Bcc headers. JSON is the default
because it is what lets SendOps validate the request, render templates
and report a precise 400; raw MIME exists so that nothing a customer
sends today through SES is impossible here.
A body that is neither application/json nor message/rfc822 is
400 validation_failed with field: Content-Type. A structured body
is decoded strictly: an unknown member is a 400 naming it.
200 means SES accepted the message and returns its message_id.
202 is answered by the edge only, never by the origin: the request is
stored and will be sent when the origin recovers. See the API
description for idempotency, size and retry rules.
Headers
Idempotency-Key string optional A client-chosen key, unique per message (a UUID is ideal). Reuse it
when retrying the same message: a repeat with the same body for at
least 7 days replays the original response instead of sending again.
The same key with a different body is 409 idempotency_key_conflict.
If absent, one is generated for the request. At most 255 characters
(400 validation_failed on Idempotency-Key otherwise).
Request body
Content type: application/json
from string required Sender, as address or Name <address>. The address must be on a
domain, or be an email identity, verified in the organization's SES
account and managed by SendOps for the organization.
to array<string> required Recipients, each as address or Name <address>.
Constraints: 1–50 items
subject string optional Required with html/text. With template it is optional and is
passed to the template as its subject variable.
html string optional HTML body.
text string optional Plain-text body.
attachments array<object> optional Each entry in attachments:
filename string required content string required The file's bytes, base64-encoded. Counts toward the 40 MB request cap at its encoded size.
content_type string optional MIME type. Inferred from filename when absent.
headers object optional Extra message headers, name to value. Headers SendOps sets itself
(From, To, Subject, Date, Message-ID, MIME-Version,
Content-Type) cannot be overridden. A Reply-To header is
accepted and treated as reply_to; set one or the other.
reply_to string | array<string> optional Where replies go, as one address or Name <address>, or an array
of them (at most 50). Works with html/text and with template.
template string optional Id or name of a stored template to render, which must be deployed
to the organization's SES account. Excludes html, text and
attachments.
variables object optional Values substituted into template. Ignored without one.
Responses
Errors follow the RFC 7807 problem format — see the error reference.
status string required message_id string required The SES message id.
request_id string required Resolves through GET /v1/messages/{request_id}.
GET /v1/messages/{request_id} for the outcome. Never
returned by the origin.
application/json status string required request_id string required Resolves through GET /v1/messages/{request_id}.
code: validation_failed). Terminal: fix the request before retrying. The
edge also answers it, with X-SendOps-Path: rejected, when the body
was cut off mid-upload; resend the whole request.
application/problem+json code: invalid_key); or a rotated key past its 24-hour grace window (code: key_expired). Carries a WWW-Authenticate: Bearer challenge. The edge answers the same codes itself, with X-SendOps-Path: fallback, when it checks the key before queueing a send. A revoked key is invalid_key; key_revoked is never an HTTP response, only a lookup's error.code. application/problem+json api.send scope (code: invalid_scope), or it is an sk_test_ key and a recipient is neither an SES mailbox-simulator address nor on one of the organization's own verified, managed identities (code: test_environment_forbidden), or the request did not come through the edge at ingest.sendops.dev (code: edge_required). Nothing was sent. The edge answers invalid_scope itself, with X-SendOps-Path: fallback, when it checks the key before queueing. application/problem+json Idempotency-Key was already used for a different request body, or
a request under the same key is still in flight (code: idempotency_key_conflict). Use a new key for a new message. The
in-flight case carries Retry-After: retry with the same key after it
to get the outcome.
application/problem+json code: payload_too_large). application/problem+json code: rate_limited), or the organization has
used its plan's monthly fair-use number of API sends (code: send_quota_exceeded, with limit, used and resets_at). Nothing
was sent. Retry after Retry-After; for send_quota_exceeded that is
the moment the billing period resets. While the edge is queueing
sends, it also limits each key to 100 requests per 10 seconds and
answers rate_limited itself, with Retry-After: 10 and
X-SendOps-Path: fallback.
application/problem+json code: internal_error). Quote request_id to support. application/problem+json code: overloaded with
X-SendOps-Shed: 1 when SendOps is shedding load;
code: upstream_unavailable when the organization's AWS SES throttled,
is out of daily quota, could not be reached, or failed without
confirming the send (never with X-SendOps-Shed). The edge also
answers upstream_unavailable with Retry-After: 2 and
X-SendOps-Path: fallback when it cannot reach the SendOps origin,
the origin times out, or recent failures have it skipping the origin
for a few seconds; and with X-SendOps-Path: direct plus
X-SendOps-Reason: unreplayable when the origin failed after a body
over 10 MB was already streamed to it. Both codes are retryable after
Retry-After, with the same Idempotency-Key. A definite SES
refusal of the message (MessageRejected, sending paused, unverified
MAIL FROM) is a 400 validation_failed with SES's reason instead.
application/problem+json