API Reference¶
Base URL: /api/v1
Authentication¶
All API requests require an API key in the header:
Key scopes¶
An API key carries a list of scopes, empty by default. Two independent checks then guard a route: the caller's role on the domain (or superuser status), and the scope.
| Scope | Guards |
|---|---|
recipients:read / recipients:write |
the recipient routes. POST /test-webhook needs recipients:write unless the caller is a superuser |
rules:read / rules:write |
the ruleset and rule routes |
logs:read |
every delivery-log route, POST /delivery-log/{id}/retry included |
admin |
satisfies every scope check |
A key with no scopes cannot reach those routes
POST /auth/keys and fsmtp auth create-key default to no scopes, and a missing
scope is 403 Missing required scope: recipients:read. Create keys with the scopes
they need, or with admin. The scopes on an existing key cannot be edited: rotate it
(POST /auth/keys/{id}/rotate keeps the old key's scopes) or create a new one.
The domain, member and user routes are gated by role and superuser status only - the
domains:*, members:* and users:* names the server accepts on a key are not checked
by any route today, so granting them changes nothing. FASTSMTP_ROOT_API_KEY bypasses
every check, scopes included.
Endpoints¶
Routes marked ?include_deleted and ?purge take those boolean query parameters;
see Deletion, restore and purge for what they do and
who may pass them.
Operations¶
GET /health- Health checkGET /ready- Readiness check (database connectivity;?include_queue,?include_smtp)POST /test-webhook- Test a webhook URL
Auth¶
All routes act on the caller's own account and keys.
GET /auth/me- Current user and domain memberships (memberships of deleted domains are omitted)GET /auth/keys?include_deleted- List your API keysPOST /auth/keys- Create an API keyDELETE /auth/keys/{id}- Delete an API key (soft; a deleted key answers 404 afterwards)POST /auth/keys/{id}/rotate- Rotate an API key (the old key is deleted)
Users¶
Superuser only.
GET /users?include_deleted- List usersPOST /users- Create userGET /users/{id}?include_deleted- Get userPUT /users/{id}- Update userDELETE /users/{id}?purge- Delete user (soft), or purge an already-deleted onePOST /users/{id}/restore- Restore a deleted user
Domains¶
GET /domains?include_deleted- List domainsPOST /domains- Create domain (superuser)GET /domains/{id}?include_deleted- Get domainPUT /domains/{id}- Update domain (admin)DELETE /domains/{id}?purge- Delete domain (owner; soft), or purge an already-deleted one (superuser)POST /domains/{id}/restore- Restore a deleted domain (owner)
A domain carries four tri-state authentication fields: verify_dkim, verify_spf,
reject_dkim_fail and reject_spf_fail. null (the default) inherits the matching
FASTSMTP_SMTP_* server setting; true or false overrides it for mail addressed to
that domain and takes effect at receive time. Send null on a PUT to go back to
inheriting.
Setting them is superuser only: they are the operator's mail policy, and a domain
admin could otherwise turn off a check the server rejects on. PUT /domains/{id}
otherwise needs only the admin role, so a payload that sets any of them without
superuser rights is answered 403 naming the fields, and nothing is written; an
absent field, or an explicit null to inherit, is not an override and stays open to
domain admins. (POST /domains is superuser-only in full, so it answers the generic
403 first.) See
Per-domain overrides for what they do when a
message has recipients on several domains.
Members¶
GET /domains/{id}/members- List members (memberships of deleted users are omitted)POST /domains/{id}/members- Add member (admin; a deleted user is404 User not found)PUT /domains/{id}/members/{uid}- Change a member's role (admin)DELETE /domains/{id}/members/{uid}- Remove member (admin; this is a hard delete of the membership)
Recipients¶
GET /domains/{id}/recipients?include_deleted- List recipientsPOST /domains/{id}/recipients- Create recipient (admin)GET /domains/{id}/recipients/{rid}?include_deleted- Get recipientPUT /domains/{id}/recipients/{rid}- Update recipient (admin)DELETE /domains/{id}/recipients/{rid}?purge- Delete recipient (admin; soft), or purge an already-deleted one (superuser)POST /domains/{id}/recipients/{rid}/restore- Restore a deleted recipient (admin)
Rules¶
GET /domains/{id}/rulesets- List rulesetsPOST /domains/{id}/rulesets- Create rulesetGET /domains/{id}/rulesets/{rsid}- Get ruleset with its rulesPUT /domains/{id}/rulesets/{rsid}- Update rulesetDELETE /domains/{id}/rulesets/{rsid}- Delete rulesetPOST /domains/{id}/rulesets/{rsid}/reorder- Set the evaluation order of a ruleset's rulesPOST /domains/{id}/rulesets/{rsid}/rules- Create rulePUT /domains/{id}/rules/{rule_id}- Update ruleDELETE /domains/{id}/rules/{rule_id}- Delete rule
Rulesets and rules have no soft delete of their own: deleting one is permanent. While
their domain is deleted they are unreachable (every route above answers its members
404 Domain not found, and a non-member the same 403 as on a live domain) and they
come back untouched when the domain is restored.
Delivery Logs¶
GET /domains/{id}/delivery-log?include_deleted- List delivery logs (?status,?message_id,?limit,?offset)GET /delivery-log/{id}- Get delivery log detailsPOST /delivery-log/{id}/retry- Retry afailed,exhaustedorcancelleddelivery
The status filter and the status field use the vocabulary listed under
Delivery statuses.
Interactive documentation
Full API documentation is available on a running server at /docs (Swagger UI) or /redoc.
Deletion, restore and purge¶
Behaviour change in v0.5.0
DELETE on a user, API key, domain or recipient no longer removes the row. It
soft-deletes it: the row is stamped with a deleted_at timestamp, disappears
from every list and lookup, stops authenticating, receiving mail and delivering
webhooks, and can be brought back with POST .../restore. Permanent removal is a
separate, superuser-only step (?purge=true) that only works on a row that is
already deleted.
Delivery history is no longer severed by a delete: log rows keep their
domain_id and recipient_id, and the history of a deleted domain stays readable
to its owners and to superusers.
Users, API keys, domains and recipients carry a nullable deleted_at field in their
responses. It is null on a live row and you will only ever see it set on rows returned
with ?include_deleted=true.
What a delete does¶
DELETE returns 200 with the same message as before ("Domain example.com deleted"),
and the id answers 404 from then on, exactly as if the row were gone. The name is
released immediately: a new user, domain or recipient with the same name can be
created while the old one is deleted.
The delete cascades to what depends on the row, stamping the children with the same timestamp as the parent:
| Deleting a... | Also... |
|---|---|
| user | deletes and revokes every API key the user still has. Memberships are kept, hidden while the user is deleted, and return with the user |
| API key | sets is_active=false as well; the key never authenticates again. A second DELETE answers 404 API key not found |
| domain | deletes its recipients and cancels their pending and failed deliveries (status=cancelled, last_error="Domain deleted"). Rulesets, rules and members are kept, unreachable while the domain is deleted, and return with it |
| recipient | cancels its pending and failed deliveries (last_error="Recipient deleted") |
A deleted row cannot be observed as live anywhere: a deleted API key or the key of a
deleted user gets 401, mail for a deleted domain is refused with 550, a deleted named
recipient falls through to the domain's catch-all or is refused, and the delivery worker
cancels rather than sends anything still addressed to a deleted recipient or domain.
Reading deleted rows¶
Pass ?include_deleted=true to include deleted rows in a list, or to fetch one by id.
The flag requires the role that may delete the resource:
| Route | Who may pass include_deleted |
|---|---|
GET /users, GET /users/{id} |
superuser (as for every user route) |
GET /auth/keys |
anyone, for their own keys. Also lists keys retired before v0.5.0, which have is_active=false and no deleted_at |
GET /domains |
superuser sees every deleted domain; a regular user sees deleted domains only where they are owner |
GET /domains/{id} |
owner or superuser |
GET /domains/{id}/recipients, GET .../recipients/{rid} |
admin or superuser while the domain is live. On a deleted domain the domain itself is resolved with the flag, so only its owners and superusers get through (the recipients of a deleted domain can be audited before restoring it); an admin gets 404 there |
GET /domains/{id}/delivery-log |
owner or superuser |
Without the flag a deleted id is 404 with the ordinary "not found" detail. With it,
the user routes check the superuser gate before looking anything up, so an insufficient
caller gets 403 whether or not the row exists.
The domain-scoped routes resolve the domain first, and answer in this order:
| Caller | Live domain | Deleted domain |
|---|---|---|
| not a member | 403 Access denied to this domain |
403 Access denied to this domain |
| member below owner | 200, or 403 below the required role |
404 Domain not found |
| owner or superuser | 200 |
404 without the flag, 200 with it |
A non-member is refused before the tombstone is consulted, so the status code never
says whether a domain it cannot reach has been deleted. Its members do learn: for them
a tombstone reads as 404 until an owner passes include_deleted.
GET /delivery-log/{id} needs no flag: a log whose domain is live is readable by any
member; one whose domain is deleted by its owners and superusers (404 to its other
members, 403 to a non-member, as in the table above); one whose domain was purged
(domain_id is null) by superusers only.
Restore¶
POST /users/{id}/restore- superuserPOST /domains/{id}/restore- owner or superuserPOST /domains/{id}/recipients/{rid}/restore- admin or superuser, and the domain must be live (404otherwise: restore the domain first)
Each returns 200 with the restored resource. Restoring a row that is not deleted answers
409 ("User is not deleted", "Domain is not deleted", "Recipient is not deleted").
If the name was re-taken by a live row in the meantime the restore answers 409 with the
same detail the create route uses - "Username already exists",
"Domain already exists", "Recipient 'sales' already exists for this domain" - so
delete or rename the new row first.
Restore brings back exactly what the delete took, and nothing more:
- Users come back with their memberships. API keys are not restored - revoking a credential is one-way. Create new keys.
- Domains come back with the recipients that were deleted with them (same timestamp). A recipient deleted on its own earlier keeps its own tombstone.
- Deliveries cancelled by the delete stay
cancelled.POST /delivery-log/{id}/retryre-queues each one you want sent. It answers409with one of three details:"Domain is deleted; restore it before retrying"and"Recipient is deleted; restore it before retrying"while the row is still deleted, or"Delivery is no longer retryable"when the delivery changed under the request (a concurrent retry, the worker delivering it) or iscancelledfor a recipient that has since been purged - with no recipient row left to authenticate with, such a delivery cannot be re-armed. is_enabled/is_activeare never changed by a delete or a restore.
Purge¶
DELETE ...?purge=true on a user, domain or recipient removes the row permanently. It is
superuser only and only works on a row that is already deleted: on a live row it
answers 409 ("User must be deleted before it can be purged", and likewise for domains
and recipients). Because deletion already cancelled every queued delivery, a purge can
never orphan a pending one.
Purge runs the pre-v0.5.0 cascade:
- a user takes their API keys and memberships with them;
- a domain takes its recipients, rulesets, rules and members with it;
- delivery-log rows are kept, with
domain_id/recipient_idset tonull. Those rows are then readable by superusers only.
API keys have no purge route: they go with their user's purge, or by retention.
Retention¶
Nothing is purged automatically unless FASTSMTP_SOFT_DELETE_RETENTION_DAYS is set; by
default deleted rows are kept forever. With it set, the cleanup worker purges rows
deleted longer ago than that, and fastsmtp purge-deleted runs the same job by hand.
See Retention.