Covers 11 API operations in the email category.
| Method | Path | Description |
|---|---|---|
| POST | /v1/hooks/ses | SES bounce, complaint, and delivery webhook |
| GET | /v1/orgs/{orgSlug}/projects/{projectName}/email | Email status (runlot email) |
| POST | /v1/orgs/{orgSlug}/projects/{projectName}/email | Turns email back on after it was turned off |
| DELETE | /v1/orgs/{orgSlug}/projects/{projectName}/email | Turns off email (runlot email delete) |
| GET | /v1/orgs/{orgSlug}/projects/{projectName}/email/domains | Custom sending domains (runlot email domain list) |
| POST | /v1/orgs/{orgSlug}/projects/{projectName}/email/domains | Adds a custom sending domain (runlot email domain add) |
| POST | /v1/orgs/{orgSlug}/projects/{projectName}/email/domains/{domain}/verify | Re-checks a sending domain now (runlot email domain verify) |
| DELETE | /v1/orgs/{orgSlug}/projects/{projectName}/email/domains/{domain} | Removes a sending domain (runlot email domain rm) |
| GET | /v1/orgs/{orgSlug}/projects/{projectName}/email/log | Email log (runlot email log) |
| GET | /v1/orgs/{orgSlug}/projects/{projectName}/email/log/{mailId} | Metadata for one email |
| GET | /v1/orgs/{orgSlug}/projects/{projectName}/email/log/{mailId}/raw | Raw email message (message/rfc822) |
POST /v1/hooks/ses
This is not a path you call. Amazon SNS delivers SES events here over an HTTPS
subscription. Authentication is not a token but an SNS signature — the request is
accepted only when the certificate URL is https on sns.<region>.amazonaws.com and the
topic matches the server's RUNLOT_SES_SNS_TOPIC_ARN (otherwise 403).
SubscriptionConfirmation completes automatically by sending a GET to SubscribeURL.
Returns 501 if the server has no topic ARN. Client libraries do not call this.
operationId sesHook
Request body: text/plain · string
| Status code | Description | Response body |
|---|---|---|
| 200 | received (unknown event types and unknown ids also return 200) | — |
| 400 | — | — |
| 403 | the SNS signature does not match | — |
| 501 | the webhook is off | — |
GET /v1/orgs/{orgSlug}/projects/{projectName}/email
Whether email is granted, plus the address, sender, hourly cap, and recent sent and
received counts. Even without a grant, address and from come populated, so you can
see what the address would be if you turned it on; granted is the real state.
Requires viewer or higher.
operationId getEmail
| Status code | Description | Response body |
|---|---|---|
| 200 | Email status | EmailStatus |
| 403 | — | — |
| 404 | — | — |
| 503 | no_core — the service is temporarily unavailable | Error |
POST /v1/orgs/{orgSlug}/projects/{projectName}/email
This is idempotent. There is no request body — the address is derived
(<project>.<org>.<mail-domain>), and the plan determines the limit. The address is
fixed to the project name at the time of the grant.
Once enabled, env.email is connected to the worker shortly afterwards, and mail sent
to that address reaches the worker's email() handler. Sending and receiving turn on
together — one domain serves both directions.
Requires member or higher. Audited as email.grant.
operationId grantEmail
| Status code | Description | Response body |
|---|---|---|
| 200 | Grant status (existing value if it was already there) | EmailStatus |
| 403 | — | — |
| 404 | — | — |
| 409 | email_unavailable — this deployment has no mail domain. email_address_taken — another project already holds the same address | Error |
| 503 | no_core — the service is temporarily unavailable | Error |
DELETE /v1/orgs/{orgSlug}/projects/{projectName}/email
Mail sent to that address now gets 550 — the sending MTA receives a bounce and does not retry. This is idempotent.
Requires admin or higher. Audit event email.revoke.
operationId revokeEmail
| Status code | Description | Response body |
|---|---|---|
| 204 | turned off | — |
| 403 | — | — |
| 404 | — | — |
| 503 | no_core — the service is temporarily unavailable | Error |
GET /v1/orgs/{orgSlug}/projects/{projectName}/email/domains
The project's own sending domains and their verification state. Each row carries the exact DNS records to publish.
Requires viewer or higher.
operationId listEmailDomains
| Status code | Description | Response body |
|---|---|---|
| 200 | The sending domains | object |
| 403 | — | — |
| 404 | — | — |
| 503 | no_core — the service is temporarily unavailable | Error |
POST /v1/orgs/{orgSlug}/projects/{projectName}/email/domains
Registers the domain for sending with DKIM and answers with the records to publish:
three <token>._domainkey.<domain> CNAMEs (required), an SPF TXT and a DMARC TXT
(both advisory). The domain becomes usable in env.email.send({ from }) only once it
reads verified.
publish: true writes the records straight into the zone when Runlot hosts one
covering the domain and it belongs to the same organization. Records you wrote
yourself are never overwritten.
A repeat call is not an error and does not change the tokens — records already published
stay correct. Requires member or higher. Audited as email.domain.add.
operationId addEmailDomain
Request body: application/json · object
| Status code | Description | Response body |
|---|---|---|
| 200 | The domain and the records to publish | EmailDomain |
| 400 | — | — |
| 403 | — | — |
| 404 | — | — |
| 409 | email_not_granted — this project does not have email. email_domains_unavailable — this installation does not offer custom sending domains. email_domain_taken — another project already proved this domain. email_domain_provider — the provider refused | Error |
| 503 | no_core — the service is temporarily unavailable | Error |
POST /v1/orgs/{orgSlug}/projects/{projectName}/email/domains/{domain}/verify
Re-checks the domain's DKIM state now and updates the row. A background check does the same once a minute while a domain is pending; this is the button for when you have just published the records and do not want to wait.
If the check cannot be completed, the state is left where it is and the reason appears
in lastError.
Requires member or higher.
operationId verifyEmailDomain
| Status code | Description | Response body |
|---|---|---|
| 200 | The domain after the check | EmailDomain |
| 403 | — | — |
| 404 | — | — |
| 503 | no_core — the service is temporarily unavailable | Error |
DELETE /v1/orgs/{orgSlug}/projects/{projectName}/email/domains/{domain}
Deletes the sending identity and the row, and removes any records Runlot wrote into a
zone it hosts. Shortly afterwards every env.email.send({ from }) naming this domain
is refused with from_not_verified.
Removing one that is not there is also a 204. Requires admin or higher. Audited as
email.domain.remove.
operationId removeEmailDomain
| Status code | Description | Response body |
|---|---|---|
| 204 | removed | — |
| 403 | — | — |
| 404 | — | — |
| 409 | email_domain_provider — the sending identity could not be deleted, so the row is kept and the call can be retried | Error |
| 503 | no_core — the service is temporarily unavailable | Error |
GET /v1/orgs/{orgSlug}/projects/{projectName}/email/log
One line per sent and received message, newest first: envelope, outcome, size, and SPF
result. The raw message is at …/log/{mailId}/raw.
Retention follows the plan — 30 days on Free, 365 days on Pro (keepDays).
Requires viewer or higher.
operationId listMailLog
| Parameter | Location | Required | Type | Description |
|---|---|---|---|---|
direction | query | No | string | Both, if empty |
cursor | query | No | string | The cursor from the previous page |
limit | query | No | integer | — |
| Status code | Description | Response body |
|---|---|---|
| 200 | One page | MailLogPage |
| 403 | — | — |
| 404 | — | — |
GET /v1/orgs/{orgSlug}/projects/{projectName}/email/log/{mailId}
An id outside the project returns 404 — you cannot probe another project's logs by id. Requires viewer or higher.
operationId getMailLog
| Status code | Description | Response body |
|---|---|---|
| 200 | One email | MailLogEntry |
| 403 | — | — |
| 404 | — | — |
GET /v1/orgs/{orgSlug}/projects/{projectName}/email/log/{mailId}/raw
The raw message as-is, including headers. For received mail this includes the
Received and Authentication-Results headers Runlot added; for sent mail it is
assembled from what was submitted. Downloads as .eml. Rows with no raw message
(hasRaw: false) return 404. Requires viewer or higher.
operationId getMailLogRaw
| Status code | Description | Response body |
|---|---|---|
| 200 | Original text | string |
| 403 | — | — |
| 404 | — | — |