HTTP API¶
Every endpoint below is derived from the running application's OpenAPI schema,
so it cannot describe an endpoint that does not exist or miss a field that
does. The raw schema is openapi.json; a running instance also
serves it at /openapi.json, with interactive documentation at /docs.
Authentication¶
Every /v1 endpoint takes an API key as a bearer token:
Keys are scoped to a project and created in the dashboard. A missing header and
an invalid token both return 401 with the same message — distinguishing them
would tell whoever is probing which half they had right.
Errors¶
One shape, always:
Branch on type, which is stable; show message to humans. The full list is
in Errors.
Rate limits¶
Per project, not per key. Every response carries X-RateLimit-Limit,
X-RateLimit-Remaining and X-RateLimit-Reset; exceeding the limit returns
429.
One endpoint is deliberately absent
POST /v1/events/ses receives SES notifications from Amazon SNS and is
excluded from the schema, because it is not for callers — it authenticates
by verifying SNS's signature rather than by API key, and is only mounted
when EVENT_INGESTION asks for it. See
delivery events.
Endpoints¶
| Method | Path | |
|---|---|---|
GET |
/v1/api-keys |
List API keys |
GET |
/v1/domains |
List sending domains |
POST |
/v1/emails |
Send an email |
GET |
/v1/emails |
List emails |
GET |
/v1/emails/{email_id} |
Retrieve an email |
GET |
/v1/webhooks |
List webhook endpoints |
GET |
/v1/webhooks/{endpoint_id}/deliveries |
List recent webhook deliveries |
GET /v1/api-keys¶
List API keys
Every key belonging to the calling key's project.
Scoped by the authenticated project rather than by anything in the request, so there is no parameter a caller could change to see someone else's keys.
Responses
| Status | |
|---|---|
200 |
Successful Response |
401 |
Invalid or missing API key. |
429 |
Rate limit exceeded. |
GET /v1/domains¶
List sending domains
Every domain this key's project can send from.
Responses
| Status | |
|---|---|
200 |
Successful Response |
401 |
Invalid or missing API key. |
429 |
Rate limit exceeded. |
POST /v1/emails¶
Send an email
Accept a message for sending.
Parameters
| Name | In | ||
|---|---|---|---|
Idempotency-Key |
header | optional | Repeat a request safely. A second send with the same key returns the first message's id and sends nothing further, so a retry after a timeout cannot deliver twice. Scoped to the project. |
Request body
| Field | Type | ||
|---|---|---|---|
from |
string | required | The sender, optionally with a display name. The address or its domain must be verified in SES — an unverified sender is refused by SES, not by SESKit, so it fails at send time rather than here. |
to |
string[] or string | required | One recipient, or a list of them. While your account is in the SES sandbox every recipient must also be verified. |
subject |
string | required | Max 998 characters, which is the RFC 5322 line limit. |
html |
string | optional | HTML body. Provide html, text, or both; a message with neither is refused. |
text |
string | optional | Plain-text body. Sending both makes a multipart message, which is what clients that will not render HTML fall back to. |
cc |
string[] or string | optional | Visible to every recipient. |
bcc |
string[] or string | optional | Hidden from every recipient. Recorded, but never returned by the API — a blind copy readable from a GET is not blind. |
reply_to |
string[] or string | optional | Where replies go, if not to from. Needs no SES verification. |
headers |
object | optional | Custom headers to add to the message. Names are RFC 5322 field-names: printable ASCII with no colon, spaces or line breaks. |
attachments |
AttachmentRequest[] | optional | The size limit applies to the assembled message, not to each file: base64 inflates content by about a third, and it is the assembled size SES rejects. See EMAIL_MAX_MESSAGE_BYTES (10 MiB by default). |
Responses
| Status | |
|---|---|
201 |
Successful Response |
401 |
Invalid or missing API key. |
422 |
Validation Error |
429 |
Rate limit exceeded. |
GET /v1/emails¶
List emails
This key's project, newest first.
Paged by cursor rather than by offset. Ids sort in the order they were
created, so starting_after names a fixed point in the list and stays
correct while new messages arrive underneath it. An offset does not: a send
between two pages shifts every row down one, and the reader silently skips
the message that moved across the boundary. For a send log that is data
loss nobody can see.
An unknown starting_after is a 404 rather than an empty page. The
comparison is lexical, so an id from another project would otherwise return
a page of real messages positioned by an id the caller cannot see - a wrong
answer that looks like a right one.
Parameters
| Name | In | ||
|---|---|---|---|
limit |
query | optional | How many messages to return, newest first. |
starting_after |
query | optional | Return messages older than this id - the last id from the previous page. The id must belong to this project. |
status |
query | optional | Only messages in this status. |
Responses
| Status | |
|---|---|
200 |
Successful Response |
401 |
Invalid or missing API key. |
422 |
Validation Error |
429 |
Rate limit exceeded. |
GET /v1/emails/{email_id}¶
Retrieve an email
One message, if it belongs to this key's project.
Ownership is part of the query, so an id from another project is a 404 rather than a 403 - which would confirm the id exists.
Parameters
| Name | In | ||
|---|---|---|---|
email_id |
path | required | The id returned when the message was accepted. |
Responses
| Status | |
|---|---|
200 |
Successful Response |
401 |
Invalid or missing API key. |
422 |
Validation Error |
429 |
Rate limit exceeded. |
GET /v1/webhooks¶
List webhook endpoints
Every webhook endpoint registered on this key's project.
The signing secret is not included - see the note in
schemas/webhooks.py. It is available on the dashboard, where it is read
once by a person rather than returned to code on every call.
Responses
| Status | |
|---|---|
200 |
Successful Response |
401 |
Invalid or missing API key. |
429 |
Rate limit exceeded. |
GET /v1/webhooks/{endpoint_id}/deliveries¶
List recent webhook deliveries
Recent delivery attempts for one endpoint, newest first.
The delivery row is the queue as well as the log, so what comes back is the actual state of each attempt - including one still pending a retry, with the time it is next due.
Parameters
| Name | In | ||
|---|---|---|---|
endpoint_id |
path | required | The endpoint whose deliveries to list. |
Responses
| Status | |
|---|---|
200 |
Successful Response |
401 |
Invalid or missing API key. |
422 |
Validation Error |
429 |
Rate limit exceeded. |
Models¶
AttachmentRequest¶
One attachment, base64 encoded.
JSON has no byte type, so content arrives encoded. The size limit is applied to the assembled message rather than to this field, because base64 inflates content by about a third and it is the assembled size SES rejects.
| Field | Type | ||
|---|---|---|---|
filename |
string | required | The name the recipient sees. Max 255 characters. |
content |
string | required | Base64-encoded file content. |
content_type |
string | optional | MIME type. Defaults to application/octet-stream, which most clients offer as a download rather than displaying. |