Skip to content

API Overview & Authentication

The Vectis Mail API is a RESTful JSON API for managing domains, mailboxes, aliases, sending email, and administering your mail server. All endpoints are versioned under a single base path.

https://mail.example.com/api/v1

Replace mail.example.com with your Vectis hostname. All requests and responses use application/json.

Every endpoint requires authentication except GET /health, GET /version, and POST /auth/login. Vectis supports two authentication methods.

The admin dashboard authenticates via POST /auth/login, which sets a signed HttpOnly session cookie named vectis_session. The cookie is Secure, SameSite=Strict, and expires after the configured session TTL (default 24 hours).

Terminal window
curl -X POST https://mail.example.com/api/v1/auth/login \
-H "Content-Type: application/json" \
-d '{"email": "admin@example.com", "password": "your-password"}' \
-c cookies.txt

If the admin has TOTP enabled, the first call returns a temporary totp_session token. Complete login by sending the TOTP code:

Terminal window
curl -X POST https://mail.example.com/api/v1/auth/login \
-H "Content-Type: application/json" \
-d '{"totp_session": "abc123...", "totp_code": "482910"}'

API keys are prefixed with vectis_ and authenticate via the Authorization header. Keys can be scoped to specific domains and have configurable rate limits.

Terminal window
curl https://mail.example.com/api/v1/domains \
-H "Authorization: Bearer vectis_sk_abc123..."

API keys inherit the role of the admin who created them. They can be further restricted to specific domains via scoped_domain_ids at creation time. See the Admin & RBAC API for key management endpoints.

Every response uses a consistent JSON envelope:

{
"data": { ... },
"error": null,
"meta": {
"request_id": "0192abc0-def1-7000-8000-000000000001",
"timestamp": "2026-04-04T12:00:00Z"
}
}
FieldTypeDescription
dataobject/arrayThe response payload. Null on error.
errorobject/nullError details. Null on success.
error.codestringMachine-readable error code (e.g. DOMAIN_EXISTS).
error.messagestringHuman-readable error message.
error.detailsobject/nullAdditional context for the error, when available.
meta.request_idstringUUIDv7 identifying this request. Include in support tickets.
meta.timestampstringISO 8601 UTC timestamp of the response.
meta.paginationobject/nullPresent on paginated endpoints. See Pagination.

Errors return the appropriate HTTP status code along with a structured error object.

HTTP StatusCodeDescription
400INVALID_JSONMalformed JSON request body.
400MISSING_FIELDSRequired fields are absent.
400INVALID_PERIODInvalid period parameter for analytics.
400INVALID_PAGINATIONMalformed cursor or limit value.
401AUTH_REQUIREDNo session cookie or Bearer token provided.
401SESSION_INVALIDSession expired or token invalid.
401API_KEY_INVALIDAPI key not found or expired.
401INVALID_CREDENTIALSWrong email or password.
401TOTP_INVALIDInvalid TOTP code.
403FORBIDDENInsufficient role or domain access.
403API_KEY_DOMAIN_SCOPEAPI key does not have access to the requested domain.
403DOMAIN_INACTIVEThe domain exists but is deactivated.
404NOT_FOUNDResource does not exist.
409DOMAIN_EXISTSDomain already registered.
409CONFLICTResource has dependents (e.g. domain has mailboxes).
409ORCHESTRATOR_BUSYOrchestrator is currently running an operation.
429RATE_LIMIT_EXCEEDEDToo many requests or sending rate exceeded.
500INTERNAL_ERRORUnexpected server error.

Example error response:

{
"data": null,
"error": {
"code": "DOMAIN_EXISTS",
"message": "Domain example.com already exists"
},
"meta": {
"request_id": "0192abc0-def1-7000-8000-000000000001",
"timestamp": "2026-04-04T12:00:00Z"
}
}

List endpoints use cursor-based pagination. Pass cursor and limit as query parameters.

ParameterTypeDefaultMaxDescription
cursorstring(none)Opaque cursor from a previous response.
limitinteger50200Number of items to return.

Paginated responses include a pagination object in the meta field:

{
"data": [ ... ],
"meta": {
"request_id": "...",
"timestamp": "...",
"pagination": {
"next_cursor": "MjAyNi0wNC0wNFQxMjowMDowMFo=",
"has_more": true
}
}
}

To fetch the next page, pass next_cursor as the cursor parameter:

Terminal window
curl "https://mail.example.com/api/v1/domains?cursor=MjAyNi0wNC0wNFQxMjowMDowMFo=&limit=50" \
-H "Authorization: Bearer vectis_sk_abc123..."

When has_more is false, you have reached the last page.

Most list endpoints support sorting and filtering via query parameters:

ParameterExampleDescription
sortcreated_atField to sort by.
orderdescSort direction: asc or desc.
activetrueFilter by active status (domains, mailboxes).
domain_iduuidFilter by domain (mailboxes, aliases, messages).

Rate limiting is enforced at the Traefik reverse proxy layer and is configurable per endpoint group.

TierApplies toDefault limit
AuthenticationPOST /auth/login5 requests/second (throttled).
API keyAll authenticated endpoints60 requests/minute per key (configurable at key creation).
GlobalAll endpointsConfigurable in config.yaml.

When rate-limited, the API returns HTTP 429 with the RATE_LIMIT_EXCEEDED error code.

All requests must send Content-Type: application/json. Request bodies are limited to 1 MB. The API returns application/json for all responses.

CORS is disabled by default. If you need to access the API from a custom frontend on a different origin, configure the CORS settings in config.yaml.

The API is versioned via the URL prefix (/api/v1). Breaking changes will be introduced under a new version prefix (e.g. /api/v2). Non-breaking additions (new fields, new endpoints) are made to the current version.

Every mutating API call (POST, PATCH, DELETE) is recorded in the audit log with the admin ID, action, resource, IP address, and a details object capturing what changed. See the Admin & RBAC API for querying the audit log.