Domains¶
Manage email domains within organizations, including DNS verification, DKIM key management, and quota settings.
Domain IDs are 26-character ULID strings (e.g. 01J1DOM0000000000000000000), not integers.
List Domains¶
Retrieve a paginated list of domains. A tenant credential always sees only its own organization's domains; a platform-scoped credential sees every organization and may narrow with organization_id.
Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
q | string | -- | Search the domain name (substring) |
organization_id | string | -- | Platform scope only -- narrow to one organization. Ignored for tenant credentials |
active | string | -- | true or false (anything else is 422) |
sort_by | string | domain | domain, created_at, total_storage_used |
sort_dir | string | asc | asc or desc |
page | integer | 1 | Page number |
per_page | integer | 50 | Items per page (max 200) |
Example Request
Example Response
{
"type": "success",
"msg": "Domains retrieved successfully",
"data": {
"items": [
{
"id": "01J1DOM0000000000000000000",
"domain": "acme.com",
"organization_id": "01J1ABCDEF2345GHJKMNPQRSTV",
"organization_name": "Acme Corp",
"description": "Primary domain",
"active": true,
"max_quota": 10737418240,
"max_users": 1000,
"total_storage_used": 3221225472,
"dkim_enabled": true,
"dkim_selector": "default",
"email_account_count": 20,
"external_id": null,
"created_at": "2026-01-15T10:30:00",
"updated_at": "2026-03-20T14:22:00"
}
],
"pagination": {
"page": 1,
"per_page": 50,
"total": 1,
"total_pages": 1
}
}
}
Get Domain¶
Retrieve a single domain with its email accounts, usage statistics, current DKIM record, and the full set of DNS records to publish.
Path Parameters
| Parameter | Type | Description |
|---|---|---|
domain_id | string | The domain's ULID |
Example Response (abridged)
{
"type": "success",
"msg": "Domain retrieved successfully",
"data": {
"id": "01J1DOM0000000000000000000",
"domain": "acme.com",
"organization_id": "01J1ABCDEF2345GHJKMNPQRSTV",
"organization": { "id": "01J1ABCDEF2345GHJKMNPQRSTV", "name": "Acme Corp" },
"active": true,
"max_quota": 10737418240,
"max_users": 1000,
"dkim_enabled": true,
"dkim_selector": "default",
"dkim_record": "v=DKIM1; k=rsa; p=MIIBIjANBg...",
"dkim_dns_name": "default._domainkey.acme.com",
"dns_records": [
{"type": "MX", "name": "acme.com", "value": "mx.mailyte.com.", "priority": 10, "description": "Routes all email for this domain to Mailyte"},
{"type": "TXT", "name": "acme.com", "value": "v=spf1 include:spf.mx.mailyte.com ~all", "description": "Authorizes Mailyte servers to send email for this domain"},
{"type": "TXT", "name": "_dmarc.acme.com", "value": "v=DMARC1; p=quarantine; rua=mailto:dmarc-reports@acme.com", "description": "Policy for handling emails that fail SPF/DKIM checks"},
{"type": "TXT", "name": "default._domainkey.acme.com", "value": "v=DKIM1; k=rsa; p=MIIBIjANBg...", "description": "Public key for DKIM signature verification"}
],
"email_accounts": [ ... ],
"email_account_count": 20,
"usage_statistics": {
"account_usage_percentage": 2.0,
"storage_usage_percentage": 30.0,
"total_account_storage": 3221225472
}
}
}
The dns_records are regenerated on every read from the current server hostname and active DKIM key, so they always reflect the configuration in force right now.
Create Domain¶
Add a new domain. A tenant credential's domain is always created in its own organization (a supplied organization_id is ignored); a platform-scoped caller must supply organization_id.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
domain | string | Yes | Domain name (e.g., example.com) |
organization_id | string | Platform callers only | Owning organization (ignored for tenant credentials) |
description | string | No | Description (HTML is stripped) |
active | boolean | No | Whether the domain is active (default: true) |
max_quota | integer | No | Max storage in bytes (default: 10737418240 = 10 GB) |
max_users | integer | No | Max email accounts (default: 1000) |
dkim_enabled | boolean | No | Enable DKIM signing (default: true) |
dkim_selector | string | No | DKIM selector name (default: "default") |
rate_limits | object | No | Domain-level rate limits |
storage_quotas | object | No | Domain-level storage quotas |
external_id | string | No | Your own system's identifier |
Example Request
curl -X POST -H "X-API-Key: YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"domain": "acme.com",
"description": "Primary domain",
"max_users": 500
}' \
http://your-server:8083/api/v1/domains/
Example Response (201 Created)
The full domain record, plus dkim_record, dkim_selector, dkim_dns_name, and the generated dns_records array (see Get Domain).
DKIM key generation
When you create a domain with dkim_enabled: true, the server generates a 2048-bit RSA key pair automatically (the private key is stored envelope-encrypted and never returned by any endpoint), and exports the key file + selector-map entry to rspamd's key directory (best-effort write-through; scripts/generate_dkim.py sync reconciles any drift). Publish the returned dkim_record as a TXT record at {dkim_selector}._domainkey.{domain} -- the dns_records array names the domain's actual selector (it previously hardcoded default._domainkey even for rotated domains).
dkim_selector must be a single DNS label (letters, digits, -, _; no dots; max 63 chars) -- anything else is rejected with a validation error, both here and on update.
Duplicate check
Domain names are globally unique and checked case-insensitively. A collision returns 409 with error_code: "DOMAIN_ALREADY_CLAIMED". If your own organization already owns the domain, data.existing_id carries the existing domain's ID; if another organization owns it, no detail is disclosed.
Update Domain¶
Update an existing domain. Only the fields you include are changed.
Request Body
Accepted fields: description, active, max_quota, max_users, dkim_enabled, dkim_selector, rate_limits, storage_quotas, external_id. The domain name and owning organization cannot be changed. A duplicate external_id returns 409.
Example Request
curl -X PUT -H "X-API-Key: YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"max_users": 1000, "description": "Primary corporate domain"}' \
http://your-server:8083/api/v1/domains/01J1DOM0000000000000000000
The response data is the full updated domain record.
Delete Domain¶
Delete a domain. The domain must have no email accounts.
Prerequisite
You must delete all email accounts on the domain first, or the API returns 400: "Cannot delete domain with {n} email accounts. Delete them first."
Example Response
Verify DNS Records¶
Perform live DNS lookups to check whether the domain's MX, SPF, DKIM, and DMARC records are correctly configured. SPF is evaluated mechanism-by-mechanism (RFC 7208, including include:/redirect= chains and the 10-lookup budget), not by substring match.
Example Response
{
"type": "success",
"msg": "DNS verification completed",
"data": {
"domain": "acme.com",
"verification": {
"mx": {"status": "pass", "expected": "mx.mailyte.com", "actual": "mx.mailyte.com"},
"spf": {"status": "pass", "found": true, "expected": "include:spf.mx.mailyte.com", "actual": "v=spf1 include:spf.mx.mailyte.com ~all"},
"dkim": {"status": "pass", "found": true},
"dmarc": {"status": "pass", "policy": "quarantine"}
}
}
}
Each check reports status: "pass" or "fail"; failing checks include a message explaining why (e.g. multiple SPF records, missing DKIM record, over-budget SPF).
Get Domain Quotas¶
Returns max_quota, max_users, storage_used, usage_percentage, account_usage_percentage, rate_limits, storage_quotas, and a per-account breakdown (email_accounts[] with storage_quota, storage_used, usage_percentage).
Update Domain Quotas¶
Request Body
| Field | Type | Description |
|---|---|---|
max_quota | integer | Max storage in bytes |
max_users | integer | Max email accounts |
rate_limits | object | Domain-level rate limits |
storage_quotas | object | Domain-level storage quotas |
The response data echoes the updated values.
DKIM Key Management¶
Safe DKIM rotation is a two-step flow: mint a key under a new selector, publish its TXT record, wait for propagation, then cut signing over. Private key material is never returned by any of these endpoints in any form.
Operator console only
These three endpoints are role-gated (support to list, operator to rotate/activate) and therefore reachable only with an operator session.
List DKIM keys¶
Returns every DKIM key on the domain with the exact TXT record to publish for each:
{
"type": "success",
"msg": "DKIM keys retrieved",
"data": {
"domain": "acme.com",
"domain_id": "01J1DOM0000000000000000000",
"dkim_enabled": true,
"signing_selector": "default",
"items": [
{
"selector": "default",
"algorithm": "rsa",
"key_size": 2048,
"active": true,
"created_at": "2026-01-15T10:30:00",
"updated_at": "2026-01-15T10:30:00",
"record_name": "default._domainkey.acme.com",
"record_type": "TXT",
"record_value": "v=DKIM1; k=rsa; p=MIIBIjANBg..."
}
],
"total": 1
}
}
Rotate (step 1): generate a new key¶
Request Body: {"reason": "<why -- recorded in the audit log>"}
Mints a fresh 2048-bit RSA key under a new timestamped selector (mailyte<YYYYMMDDHHMMSS>) and leaves the existing key active and signing. Returns 201 with the new selector, the DNS record to publish, the currently active selectors, and an activate_url. Nothing changes about what is signing until you activate.
Activate (step 2): cut signing over¶
Request Body: {"reason": "..."}
Makes the named selector the signing key and deactivates every other selector on the domain. Call only once the selector's TXT record resolves publicly. 404 if the selector does not exist on the domain.
Rspamd write-through
Both endpoints export to rspamd automatically: rotate writes the new selector's key file immediately (so it is on disk while the DNS record propagates), and activate rewrites the domain->selector map. Each response carries rspamd_synced -- true means the export ran; false means the key state is safe in MySQL but disk was not updated (e.g. the api container cannot reach the shared key directory): run scripts/generate_dkim.py sync on the docker host before relying on the cutover. Rspamd re-reads the selector map on its map watch interval (about a minute).
Legacy Endpoints¶
These mailcow-compatible endpoints predate the ID-based CRUD above and operate on domain names.
Edit domains (bulk)¶
Request Body
Allowed attr keys: description, aliases, mailboxes, maxquota, quota, defquota, transport, backupmx, active, relay_all_recipients, rl_value, rl_frame, gal. Returns per-domain success/error results.
Delete domains (bulk)¶
Request Body: a JSON array of domain names, e.g. ["acme.com"]. Deletes each domain together with its mailboxes, aliases, and DKIM keys, and returns per-domain results with stats (mailboxes_deleted, aliases_deleted).
Get domain policy¶
Returns the domain's spam/security policy: policy_bl_only, policy_reject_spam, policy_greylist, policy_rbl, plus rate_limits and active from the domain record.
Edit domain policy¶
Request Body
| Field | Type | Default | Description |
|---|---|---|---|
domain | string | required | Domain name |
policy_bl_only | integer | 0 | Blacklist-only mode (0/1) |
policy_reject_spam | integer | 0 | Reject detected spam (0/1) |
policy_greylist | integer | 1 | Enable greylisting (0/1) |
policy_rbl | integer | 1 | Enable RBL checks (0/1) |
Upsert semantics -- the policy row is created if it does not exist.
Get domain statistics¶
Example Response