Skip to content

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:

curl https://your-seskit-instance/v1/emails/email_01J8XQ... \
  -H "Authorization: Bearer sk_..."

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:

{ "error": { "type": "domain_not_verified", "message": "..." } }

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/inbound List received emails
GET /v1/inbound/{inbound_id} Retrieve a received email
GET /v1/inbound/{inbound_id}/attachments/{index} Download an attachment
GET /v1/inbound/{inbound_id}/raw Download the original message
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/inbound

List received emails

This key's project, newest first, without bodies.

Paged by cursor for the reason /v1/emails is: ids sort in the order they were created, so starting_after names a fixed point and stays correct while mail arrives underneath it, where an offset would silently skip the message that moved across the boundary. An unknown starting_after is a 404 and not an empty page, because the comparison is lexical and an id from another project would otherwise position a page of real messages by a value the caller cannot see.

Sorted by id and not by received_at. A backlog means a message can be recorded long after it arrived, so the two orders can differ; id order is the one a cursor can be stable over, and received_at is on every row.

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.
domain query optional Only messages received for this domain.

Responses

Status
200 Successful Response
401 Invalid or missing API key.
422 Validation Error
429 Rate limit exceeded.

GET /v1/inbound/{inbound_id}

Retrieve a received email

One message, parsed. Bodies, headers, attachments and what SES concluded.

The bodies are the sender's, not SESKit's. In particular the HTML is whatever was sent, and has to be treated as hostile by anything that renders it.

Parameters

Name In
inbound_id path required The id from the list, or from an email.received event.

Responses

Status
200 Successful Response
401 Invalid or missing API key.
422 Validation Error
429 Rate limit exceeded.

GET /v1/inbound/{inbound_id}/attachments/{index}

Download an attachment

One attachment's bytes, fetched from the stored message.

Always served as application/octet-stream, whatever the sender declared, with Content-Disposition: attachment, nosniff and a sandboxing CSP. The declared type is on the message's attachments for you to read; it is never one this endpoint believes. These are files a stranger sent, served from your API's origin.

Needs the stored message, so it stops working when retention removes it. The parsed message does not.

Parameters

Name In
inbound_id path required The id of the received message.
index path required The attachment's index from the message's attachments.

Responses

Status
200 The bytes.
401 Invalid or missing API key.
422 Validation Error
429 Rate limit exceeded.

GET /v1/inbound/{inbound_id}/raw

Download the original message

The message exactly as SES received it, as an .eml file.

Unparsed and unmodified: every header, every part, every attachment. Useful for debugging a message that did not parse as expected, and for handing to another tool. It is the sender's bytes, served as a download with the same protections as an attachment.

Needs the stored message, so it stops working when retention removes it.

Parameters

Name In
inbound_id path required The id of the received message.

Responses

Status
200 The original message, byte for byte.
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.