Skip to content
runlot
ReferenceAPIemail

email

Covers 11 API operations in the email category.

MethodPathDescription
POST/v1/hooks/sesSES bounce, complaint, and delivery webhook
GET/v1/orgs/{orgSlug}/projects/{projectName}/emailEmail status (runlot email)
POST/v1/orgs/{orgSlug}/projects/{projectName}/emailTurns email back on after it was turned off
DELETE/v1/orgs/{orgSlug}/projects/{projectName}/emailTurns off email (runlot email delete)
GET/v1/orgs/{orgSlug}/projects/{projectName}/email/domainsCustom sending domains (runlot email domain list)
POST/v1/orgs/{orgSlug}/projects/{projectName}/email/domainsAdds a custom sending domain (runlot email domain add)
POST/v1/orgs/{orgSlug}/projects/{projectName}/email/domains/{domain}/verifyRe-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/logEmail 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}/rawRaw 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 codeDescriptionResponse body
200received (unknown event types and unknown ids also return 200)—
400——
403the SNS signature does not match—
501the 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 codeDescriptionResponse body
200Email statusEmailStatus
403——
404——
503no_core — the service is temporarily unavailableError

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 codeDescriptionResponse body
200Grant status (existing value if it was already there)EmailStatus
403——
404——
409email_unavailable — this deployment has no mail domain. email_address_taken — another project already holds the same addressError
503no_core — the service is temporarily unavailableError

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 codeDescriptionResponse body
204turned off—
403——
404——
503no_core — the service is temporarily unavailableError

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 codeDescriptionResponse body
200The sending domainsobject
403——
404——
503no_core — the service is temporarily unavailableError

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 codeDescriptionResponse body
200The domain and the records to publishEmailDomain
400——
403——
404——
409email_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 refusedError
503no_core — the service is temporarily unavailableError

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 codeDescriptionResponse body
200The domain after the checkEmailDomain
403——
404——
503no_core — the service is temporarily unavailableError

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 codeDescriptionResponse body
204removed—
403——
404——
409email_domain_provider — the sending identity could not be deleted, so the row is kept and the call can be retriedError
503no_core — the service is temporarily unavailableError

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

ParameterLocationRequiredTypeDescription
directionqueryNostringBoth, if empty
cursorqueryNostringThe cursor from the previous page
limitqueryNointeger—
Status codeDescriptionResponse body
200One pageMailLogPage
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 codeDescriptionResponse body
200One emailMailLogEntry
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 codeDescriptionResponse body
200Original textstring
403——
404——

On this page