Domains
Add, verify, and manage domains: DNS records, catch-all webhooks, retention and encryption, and domain registration. See Domains & DNS for a guided walk-through.
Auth: Session token · Base URL:
https://api.mailkite.dev
Endpoints
Each row expands to its request and response schema. The address bar follows along, so any endpoint can be linked to directly.
GET /api/domains List your domains, each with its webhook URL.
List your domains, each with its webhook URL.
A sending/receiving domain on your account. List and detail responses enrich the stored row with the domain's catch-all webhook (webhookUrl/webhookAckMode/webhookStatus) and the account's daily-limit state; write responses return the row alone, so those fields are optional here.
| Field | Type | Notes |
|---|---|---|
| id req | string | Domain id (dom_…). |
| user_id req | string | Owning account (usr_…). |
| domain req | string | The domain name, lowercased. |
| status req | "pending" · "verified" · "failed" | Overall verification state. Inbound routing requires at least MX verification; `verified` follows the MX check. |
| mx_verified req | 0 · 1 | 1 once the MX record resolves to MailKite — the gate for inbound mail, webhooks, and routes. |
| spf_verified req | 0 · 1 | 1 once the SPF TXT record is in place. |
| dkim_verified req | 0 · 1 | 1 once the DKIM records are accepted by the sending provider. |
| dmarc_verified req | 0 · 1 | 1 once a DMARC policy is published. |
| zero_retention | 0 · 1 | 1 when zero-retention passthrough is on: inbound mail is delivered to the webhook and never stored (no replay, no dashboard history). |
| track_opens | 0 · 1 | Domain-wide default for outbound open tracking (a per-send trackOpens overrides it). |
| enc_public_key | string · null | At-rest encryption public key (PEM SPKI, armored OpenPGP key, or X.509 cert). null = encryption off. |
| enc_key_alg | string · null | Key algorithm recorded for the uploaded key ('RSA-OAEP-256', 'PGP', or 'S/MIME'). null = encryption off. |
| enc_fingerprint | string · null | Fingerprint of the uploaded key (sha256 of the SPKI/cert DER, or the OpenPGP fingerprint). |
| enc_format | string · null | Which client-decryptable standard the key is: 'pgp' or 'smime'. null = off or legacy custom envelope. |
| provider req | string | Email sending provider type currently backing this domain (e.g. 'ses', 'cloudflare'). Assigned by MailKite, never chosen by the caller. |
| email_provider_id | string · null | Registry id of the concrete provider instance backing the domain. |
| dns_tokens | string · null | Provider-specific DNS tokens as a JSON string (e.g. SES DKIM CNAME targets). Feeds the dns record set; null until providers report them. |
| last_checked_at | integer · null | When domain verification last ran, in Unix epoch milliseconds (UTC); null when it has never happened. |
| created_at req | integer | When the domain was added, in Unix epoch milliseconds (UTC). |
| webhookUrl | string · null | URL of the domain's catch-all webhook, or null when none is set. List/detail responses only. |
| webhookAckMode | string · null | The webhook's acknowledgement mode: 'lenient' (any 2xx) or 'ack' (2xx + explicit ok). List/detail responses only. |
| webhookStatus | object · null | Health of the webhook's most recent delivery, or null when it has never been exercised. List/detail responses only. |
| webhookStatus.status | string · null | 'ok', 'fail', 'running', or 'timeout'. |
| webhookStatus.message | string · null | Human-readable detail of the last outcome (e.g. 'HTTP 500'). |
| webhookStatus.code | integer · null | HTTP status of the last delivery, when relevant. |
| webhookStatus.at | integer · null | When the last outcome landed, in Unix epoch milliseconds (UTC); null when it has never happened. |
| webhookStatus.consecutiveFailures | integer | Current failing streak (0 when healthy). |
| webhookStatus.firstFailedAt | integer · null | When the current failing streak began, in Unix epoch milliseconds (UTC); null when it has never happened. |
| webhookStatus.unhealthy | boolean | True once the failing streak passes the unhealthy threshold. |
| dailyLimitReached | boolean | True when the owning account has hit its daily send cap. List/detail responses only. |
An actual array of results, recorded from the conformance suite.
[
{
"id": "dom_2VbXqTpN8rKw",
"user_id": "usr_7Fj3MnQw",
"domain": "myapp.ai",
"status": "verified",
"mx_verified": 1,
"spf_verified": 1,
"dkim_verified": 1,
"dmarc_verified": 1,
"zero_retention": 0,
"track_opens": 0,
"enc_public_key": null,
"enc_key_alg": null,
"enc_fingerprint": null,
"enc_format": null,
"provider": "ses",
"email_provider_id": "prov_ses_use2",
"dns_tokens": null,
"created_at": 1769731200000,
"last_checked_at": 1769817600000,
"webhookUrl": "https://myapp.ai/hooks/mailkite",
"webhookAckMode": "lenient",
"webhookStatus": {
"status": "ok",
"message": null,
"code": 200,
"at": 1769817600000,
"consecutiveFailures": 0,
"firstFailedAt": null,
"unhealthy": false
},
"dailyLimitReached": false
}
] {
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://api.mailkite.dev/v1/schemas/domain.json",
"title": "Domain",
"description": "A sending/receiving domain on your account. List and detail responses enrich the stored row with the domain's catch-all webhook (webhookUrl/webhookAckMode/webhookStatus) and the account's daily-limit state; write responses return the row alone, so those fields are optional here.",
"type": "object",
"required": [
"id",
"user_id",
"domain",
"status",
"mx_verified",
"spf_verified",
"dkim_verified",
"dmarc_verified",
"provider",
"created_at"
],
"properties": {
"id": {
"type": "string",
"description": "Domain id (dom_…).",
"examples": [
"dom_2VbXqTpN8rKw"
]
},
"user_id": {
"type": "string",
"description": "Owning account (usr_…).",
"examples": [
"usr_7Fj3MnQw"
]
},
"domain": {
"type": "string",
"description": "The domain name, lowercased.",
"examples": [
"myapp.ai"
]
},
"status": {
"type": "string",
"enum": [
"pending",
"verified",
"failed"
],
"description": "Overall verification state. Inbound routing requires at least MX verification; `verified` follows the MX check."
},
"mx_verified": {
"type": "integer",
"enum": [
0,
1
],
"description": "1 once the MX record resolves to MailKite — the gate for inbound mail, webhooks, and routes."
},
"spf_verified": {
"type": "integer",
"enum": [
0,
1
],
"description": "1 once the SPF TXT record is in place."
},
"dkim_verified": {
"type": "integer",
"enum": [
0,
1
],
"description": "1 once the DKIM records are accepted by the sending provider."
},
"dmarc_verified": {
"type": "integer",
"enum": [
0,
1
],
"description": "1 once a DMARC policy is published."
},
"zero_retention": {
"type": "integer",
"enum": [
0,
1
],
"description": "1 when zero-retention passthrough is on: inbound mail is delivered to the webhook and never stored (no replay, no dashboard history)."
},
"track_opens": {
"type": "integer",
"enum": [
0,
1
],
"description": "Domain-wide default for outbound open tracking (a per-send trackOpens overrides it)."
},
"enc_public_key": {
"type": [
"string",
"null"
],
"description": "At-rest encryption public key (PEM SPKI, armored OpenPGP key, or X.509 cert). null = encryption off."
},
"enc_key_alg": {
"type": [
"string",
"null"
],
"description": "Key algorithm recorded for the uploaded key ('RSA-OAEP-256', 'PGP', or 'S/MIME'). null = encryption off."
},
"enc_fingerprint": {
"type": [
"string",
"null"
],
"description": "Fingerprint of the uploaded key (sha256 of the SPKI/cert DER, or the OpenPGP fingerprint)."
},
"enc_format": {
"type": [
"string",
"null"
],
"description": "Which client-decryptable standard the key is: 'pgp' or 'smime'. null = off or legacy custom envelope."
},
"provider": {
"type": "string",
"description": "Email sending provider type currently backing this domain (e.g. 'ses', 'cloudflare'). Assigned by MailKite, never chosen by the caller.",
"examples": [
"ses"
]
},
"email_provider_id": {
"type": [
"string",
"null"
],
"description": "Registry id of the concrete provider instance backing the domain."
},
"dns_tokens": {
"type": [
"string",
"null"
],
"description": "Provider-specific DNS tokens as a JSON string (e.g. SES DKIM CNAME targets). Feeds the dns record set; null until providers report them."
},
"last_checked_at": {
"type": [
"integer",
"null"
],
"description": "When domain verification last ran, in Unix epoch milliseconds (UTC); null when it has never happened.",
"examples": [
1769731200000
]
},
"created_at": {
"type": "integer",
"description": "When the domain was added, in Unix epoch milliseconds (UTC).",
"examples": [
1769731200000
]
},
"webhookUrl": {
"type": [
"string",
"null"
],
"description": "URL of the domain's catch-all webhook, or null when none is set. List/detail responses only.",
"examples": [
"https://myapp.ai/hooks/mailkite"
]
},
"webhookAckMode": {
"type": [
"string",
"null"
],
"description": "The webhook's acknowledgement mode: 'lenient' (any 2xx) or 'ack' (2xx + explicit ok). List/detail responses only."
},
"webhookStatus": {
"type": [
"object",
"null"
],
"description": "Health of the webhook's most recent delivery, or null when it has never been exercised. List/detail responses only.",
"properties": {
"status": {
"type": [
"string",
"null"
],
"description": "'ok', 'fail', 'running', or 'timeout'."
},
"message": {
"type": [
"string",
"null"
],
"description": "Human-readable detail of the last outcome (e.g. 'HTTP 500')."
},
"code": {
"type": [
"integer",
"null"
],
"description": "HTTP status of the last delivery, when relevant."
},
"at": {
"type": [
"integer",
"null"
],
"description": "When the last outcome landed, in Unix epoch milliseconds (UTC); null when it has never happened.",
"examples": [
1769731200000
]
},
"consecutiveFailures": {
"type": "integer",
"description": "Current failing streak (0 when healthy)."
},
"firstFailedAt": {
"type": [
"integer",
"null"
],
"description": "When the current failing streak began, in Unix epoch milliseconds (UTC); null when it has never happened.",
"examples": [
1769731200000
]
},
"unhealthy": {
"type": "boolean",
"description": "True once the failing streak passes the unhealthy threshold."
}
}
},
"dailyLimitReached": {
"type": "boolean",
"description": "True when the owning account has hit its daily send cap. List/detail responses only."
}
}
} POST /api/domains Add a domain. Returns the domain + DNS records. Paid plans may pass `email_provider_id` to choose a provider (list available providers with listEmailProviders); free plans are always pinned to the platform default (SES US East 2 production).
Add a domain. Returns the domain + DNS records. Paid plans may pass `email_provider_id` to choose a provider (list available providers with listEmailProviders); free plans are always pinned to the platform default (SES US East 2 production).
Request body
create-domain-request.json| Field | Type | Notes |
|---|---|---|
| domain req | string |
The smallest body that makes this call — every other field is optional.
{
"domain": "app.mailkite.dev"
} {
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://api.mailkite.dev/v1/schemas/create-domain-request.json",
"title": "Create domain request body",
"type": "object",
"required": [
"domain"
],
"additionalProperties": false,
"properties": {
"domain": {
"type": "string"
}
}
} Response body
create-domain-response.jsonPOST /api/domains — the new domain plus the complete DNS record set to publish (every enabled provider's records merged, so one set works wherever the domain lands).
| Field | Type | Notes |
|---|---|---|
| domain req | domain | The new domain row. |
| domain.id req | string | Domain id (dom_…). |
| domain.user_id req | string | Owning account (usr_…). |
| domain.domain req | string | The domain name, lowercased. |
| domain.status req | "pending" · "verified" · "failed" | Overall verification state. Inbound routing requires at least MX verification; `verified` follows the MX check. |
| domain.mx_verified req | 0 · 1 | 1 once the MX record resolves to MailKite — the gate for inbound mail, webhooks, and routes. |
| domain.spf_verified req | 0 · 1 | 1 once the SPF TXT record is in place. |
| domain.dkim_verified req | 0 · 1 | 1 once the DKIM records are accepted by the sending provider. |
| domain.dmarc_verified req | 0 · 1 | 1 once a DMARC policy is published. |
| domain.zero_retention | 0 · 1 | 1 when zero-retention passthrough is on: inbound mail is delivered to the webhook and never stored (no replay, no dashboard history). |
| domain.track_opens | 0 · 1 | Domain-wide default for outbound open tracking (a per-send trackOpens overrides it). |
| domain.enc_public_key | string · null | At-rest encryption public key (PEM SPKI, armored OpenPGP key, or X.509 cert). null = encryption off. |
| domain.enc_key_alg | string · null | Key algorithm recorded for the uploaded key ('RSA-OAEP-256', 'PGP', or 'S/MIME'). null = encryption off. |
| domain.enc_fingerprint | string · null | Fingerprint of the uploaded key (sha256 of the SPKI/cert DER, or the OpenPGP fingerprint). |
| domain.enc_format | string · null | Which client-decryptable standard the key is: 'pgp' or 'smime'. null = off or legacy custom envelope. |
| domain.provider req | string | Email sending provider type currently backing this domain (e.g. 'ses', 'cloudflare'). Assigned by MailKite, never chosen by the caller. |
| domain.email_provider_id | string · null | Registry id of the concrete provider instance backing the domain. |
| domain.dns_tokens | string · null | Provider-specific DNS tokens as a JSON string (e.g. SES DKIM CNAME targets). Feeds the dns record set; null until providers report them. |
| domain.last_checked_at | integer · null | When domain verification last ran, in Unix epoch milliseconds (UTC); null when it has never happened. |
| domain.created_at req | integer | When the domain was added, in Unix epoch milliseconds (UTC). |
| domain.webhookUrl | string · null | URL of the domain's catch-all webhook, or null when none is set. List/detail responses only. |
| domain.webhookAckMode | string · null | The webhook's acknowledgement mode: 'lenient' (any 2xx) or 'ack' (2xx + explicit ok). List/detail responses only. |
| domain.webhookStatus | object · null | Health of the webhook's most recent delivery, or null when it has never been exercised. List/detail responses only. |
| domain.webhookStatus.status | string · null | 'ok', 'fail', 'running', or 'timeout'. |
| domain.webhookStatus.message | string · null | Human-readable detail of the last outcome (e.g. 'HTTP 500'). |
| domain.webhookStatus.code | integer · null | HTTP status of the last delivery, when relevant. |
| domain.webhookStatus.at | integer · null | When the last outcome landed, in Unix epoch milliseconds (UTC); null when it has never happened. |
| domain.webhookStatus.consecutiveFailures | integer | Current failing streak (0 when healthy). |
| domain.webhookStatus.firstFailedAt | integer · null | When the current failing streak began, in Unix epoch milliseconds (UTC); null when it has never happened. |
| domain.webhookStatus.unhealthy | boolean | True once the failing streak passes the unhealthy threshold. |
| domain.dailyLimitReached | boolean | True when the owning account has hit its daily send cap. List/detail responses only. |
| dns req | dns-record[] | The records to publish at your DNS provider. |
| dns[].type req | "MX" · "TXT" · "CNAME" · "A" | Record type. |
| dns[].name req | string | Full hostname (FQDN) the record lives at. |
| dns[].value req | string | Bare RDATA value. |
| dns[].priority | integer | MX priority — present on MX records only. |
| warnings | string[] | Present when a provider registration failed — the DNS set omits that provider's records until a later verify backfills them. |
An actual response, recorded from the conformance suite.
{
"domain": {
"id": "dom_2VbXqTpN8rKw",
"user_id": "usr_7Fj3MnQw",
"domain": "myapp.ai",
"status": "pending",
"mx_verified": 0,
"spf_verified": 0,
"dkim_verified": 0,
"dmarc_verified": 0,
"zero_retention": 0,
"track_opens": 0,
"enc_public_key": null,
"enc_key_alg": null,
"enc_fingerprint": null,
"enc_format": null,
"provider": "ses",
"email_provider_id": "prov_ses_use2",
"dns_tokens": null,
"created_at": 1769731200000,
"last_checked_at": null
},
"dns": [
{
"type": "MX",
"name": "myapp.ai",
"value": "mx.mailkite.dev",
"priority": 10
},
{
"type": "TXT",
"name": "myapp.ai",
"value": "v=spf1 include:spf.mailkite.dev ~all"
},
{
"type": "TXT",
"name": "_dmarc.myapp.ai",
"value": "v=DMARC1; p=none; rua=mailto:dmarc@myapp.ai"
},
{
"type": "CNAME",
"name": "abc123._domainkey.myapp.ai",
"value": "abc123.dkim.amazonses.com"
}
]
} {
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://api.mailkite.dev/v1/schemas/create-domain-response.json",
"title": "Create domain response",
"description": "POST /api/domains — the new domain plus the complete DNS record set to publish (every enabled provider's records merged, so one set works wherever the domain lands).",
"type": "object",
"required": [
"domain",
"dns"
],
"properties": {
"domain": {
"$ref": "domain.json",
"description": "The new domain row."
},
"dns": {
"type": "array",
"description": "The records to publish at your DNS provider.",
"items": {
"$ref": "dns-record.json"
}
},
"warnings": {
"type": "array",
"description": "Present when a provider registration failed — the DNS set omits that provider's records until a later verify backfills them.",
"items": {
"type": "string"
}
}
}
} GET /api/domains/subdomain/suggest Suggest a free, currently-unclaimed subdomain label to prefill the input with, plus the `base` zone it would live on. Read-only — suggesting does not reserve the name. Always take the zone from `base` rather than hard-coding it: which zones are on offer changes over time.
Suggest a free, currently-unclaimed subdomain label to prefill the input with, plus the `base` zone it would live on. Read-only — suggesting does not reserve the name. Always take the zone from `base` rather than hard-coding it: which zones are on offer changes over time.
Response body
subdomain-suggestion.jsonGET /api/domains/subdomain/suggest — a randomly generated, currently-unclaimed label to prefill the input with. Read-only: suggesting does not reserve the name, so claim it promptly or re-check before claiming.
| Field | Type | Notes |
|---|---|---|
| subdomain req | string | The suggested label on its own (adjective + noun, sometimes with a 2-digit suffix). |
| domain req | string | The full hostname the label would become — `<subdomain>.<base>`. |
| base req | string | The zone this subdomain would live on. Always read it from here rather than hard-coding a hostname: which zone new subdomains are handed out under changes over time, and more than one may be on offer. |
An actual response, recorded from the conformance suite.
{
"subdomain": "swift-otter",
"domain": "swift-otter.mailk.us",
"base": "mailk.us"
} {
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://api.mailkite.dev/v1/schemas/subdomain-suggestion.json",
"title": "Subdomain suggestion",
"description": "GET /api/domains/subdomain/suggest — a randomly generated, currently-unclaimed label to prefill the input with. Read-only: suggesting does not reserve the name, so claim it promptly or re-check before claiming.",
"type": "object",
"required": [
"subdomain",
"domain",
"base"
],
"properties": {
"subdomain": {
"type": "string",
"description": "The suggested label on its own (adjective + noun, sometimes with a 2-digit suffix).",
"examples": [
"swift-otter"
]
},
"domain": {
"type": "string",
"description": "The full hostname the label would become — `<subdomain>.<base>`.",
"examples": [
"swift-otter.mailk.us"
]
},
"base": {
"type": "string",
"description": "The zone this subdomain would live on. Always read it from here rather than hard-coding a hostname: which zone new subdomains are handed out under changes over time, and more than one may be on offer.",
"examples": [
"mailk.us"
]
}
}
} GET /api/domains/subdomain/check Check whether a free subdomain label can be claimed. Read-only and cheap — call it as the user types. `reason` explains a rejection and is safe to show verbatim.
Check whether a free subdomain label can be claimed. Read-only and cheap — call it as the user types. `reason` explains a rejection and is safe to show verbatim.
Response body
subdomain-availability.jsonGET /api/domains/subdomain/check — whether a free subdomain label can be claimed. Read-only; safe to call on every keystroke as the user edits the label.
| Field | Type | Notes |
|---|---|---|
| available req | boolean | True when the label passes validation and nobody holds it yet. |
| reason | string | Why an unavailable label was rejected — show it verbatim. Either a format rule ('At least 3 characters', 'Use a single label (no dots)', 'Use letters, numbers and hyphens only', 'That subdomain is reserved') or 'Already taken'. |
| domain | string | The full hostname the label resolves to. Absent when the label is malformed enough that no hostname could be formed. |
An actual response, recorded from the conformance suite.
{
"available": true,
"domain": "swift-otter.mailk.us"
} {
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://api.mailkite.dev/v1/schemas/subdomain-availability.json",
"title": "Subdomain availability",
"description": "GET /api/domains/subdomain/check — whether a free subdomain label can be claimed. Read-only; safe to call on every keystroke as the user edits the label.",
"type": "object",
"required": [
"available"
],
"properties": {
"available": {
"type": "boolean",
"description": "True when the label passes validation and nobody holds it yet."
},
"reason": {
"type": "string",
"description": "Why an unavailable label was rejected — show it verbatim. Either a format rule ('At least 3 characters', 'Use a single label (no dots)', 'Use letters, numbers and hyphens only', 'That subdomain is reserved') or 'Already taken'.",
"examples": [
"Already taken"
]
},
"domain": {
"type": "string",
"description": "The full hostname the label resolves to. Absent when the label is malformed enough that no hostname could be formed.",
"examples": [
"swift-otter.mailk.us"
]
}
}
} POST /api/domains/subdomain Claim a free MailKite subdomain — a `<label>.<base>` host on a zone we run (call suggestSubdomain for the current `base`; the pool changes over time and more than one may be offered). The fastest path to a sending identity: we host the zone, so it comes back already verified with an empty `dns` array — nothing for the customer to publish. Use it when you want onboarding to work without asking anyone to touch DNS; bring your own domain with createDomain when you want mail to come from your own name.
Claim a free MailKite subdomain — a `<label>.<base>` host on a zone we run (call suggestSubdomain for the current `base`; the pool changes over time and more than one may be offered). The fastest path to a sending identity: we host the zone, so it comes back already verified with an empty `dns` array — nothing for the customer to publish. Use it when you want onboarding to work without asking anyone to touch DNS; bring your own domain with createDomain when you want mail to come from your own name.
Request body
claim-subdomain-request.jsonPOST /api/domains/subdomain — the label to claim on the free pool zone.
| Field | Type | Notes |
|---|---|---|
| subdomain req | string | The label to claim: 3–32 characters, lowercase letters, digits and hyphens, no leading or trailing hyphen, and not a reserved word. Just the label — a full hostname (`<label>.<base>`) is also accepted and the base stripped. |
The smallest body that makes this call — every other field is optional.
{
"subdomain": "swift-otter"
} {
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://api.mailkite.dev/v1/schemas/claim-subdomain-request.json",
"title": "Claim subdomain request body",
"description": "POST /api/domains/subdomain — the label to claim on the free pool zone.",
"type": "object",
"required": [
"subdomain"
],
"additionalProperties": false,
"properties": {
"subdomain": {
"type": "string",
"description": "The label to claim: 3–32 characters, lowercase letters, digits and hyphens, no leading or trailing hyphen, and not a reserved word. Just the label — a full hostname (`<label>.<base>`) is also accepted and the base stripped.",
"examples": [
"swift-otter"
]
}
}
} Response body
claim-subdomain-response.jsonPOST /api/domains/subdomain — the claimed subdomain, already verified. Unlike a domain you bring yourself, the pool zone covers this hostname entirely: `dns` is an empty array because there is nothing for you to publish.
| Field | Type | Notes |
|---|---|---|
| domain req | domain | The new domain row. Returned with `status: "verified"` and all four DNS checks passing — it can send and receive immediately (subject to the account email being verified). |
| domain.id req | string | Domain id (dom_…). |
| domain.user_id req | string | Owning account (usr_…). |
| domain.domain req | string | The domain name, lowercased. |
| domain.status req | "pending" · "verified" · "failed" | Overall verification state. Inbound routing requires at least MX verification; `verified` follows the MX check. |
| domain.mx_verified req | 0 · 1 | 1 once the MX record resolves to MailKite — the gate for inbound mail, webhooks, and routes. |
| domain.spf_verified req | 0 · 1 | 1 once the SPF TXT record is in place. |
| domain.dkim_verified req | 0 · 1 | 1 once the DKIM records are accepted by the sending provider. |
| domain.dmarc_verified req | 0 · 1 | 1 once a DMARC policy is published. |
| domain.zero_retention | 0 · 1 | 1 when zero-retention passthrough is on: inbound mail is delivered to the webhook and never stored (no replay, no dashboard history). |
| domain.track_opens | 0 · 1 | Domain-wide default for outbound open tracking (a per-send trackOpens overrides it). |
| domain.enc_public_key | string · null | At-rest encryption public key (PEM SPKI, armored OpenPGP key, or X.509 cert). null = encryption off. |
| domain.enc_key_alg | string · null | Key algorithm recorded for the uploaded key ('RSA-OAEP-256', 'PGP', or 'S/MIME'). null = encryption off. |
| domain.enc_fingerprint | string · null | Fingerprint of the uploaded key (sha256 of the SPKI/cert DER, or the OpenPGP fingerprint). |
| domain.enc_format | string · null | Which client-decryptable standard the key is: 'pgp' or 'smime'. null = off or legacy custom envelope. |
| domain.provider req | string | Email sending provider type currently backing this domain (e.g. 'ses', 'cloudflare'). Assigned by MailKite, never chosen by the caller. |
| domain.email_provider_id | string · null | Registry id of the concrete provider instance backing the domain. |
| domain.dns_tokens | string · null | Provider-specific DNS tokens as a JSON string (e.g. SES DKIM CNAME targets). Feeds the dns record set; null until providers report them. |
| domain.last_checked_at | integer · null | When domain verification last ran, in Unix epoch milliseconds (UTC); null when it has never happened. |
| domain.created_at req | integer | When the domain was added, in Unix epoch milliseconds (UTC). |
| domain.webhookUrl | string · null | URL of the domain's catch-all webhook, or null when none is set. List/detail responses only. |
| domain.webhookAckMode | string · null | The webhook's acknowledgement mode: 'lenient' (any 2xx) or 'ack' (2xx + explicit ok). List/detail responses only. |
| domain.webhookStatus | object · null | Health of the webhook's most recent delivery, or null when it has never been exercised. List/detail responses only. |
| domain.webhookStatus.status | string · null | 'ok', 'fail', 'running', or 'timeout'. |
| domain.webhookStatus.message | string · null | Human-readable detail of the last outcome (e.g. 'HTTP 500'). |
| domain.webhookStatus.code | integer · null | HTTP status of the last delivery, when relevant. |
| domain.webhookStatus.at | integer · null | When the last outcome landed, in Unix epoch milliseconds (UTC); null when it has never happened. |
| domain.webhookStatus.consecutiveFailures | integer | Current failing streak (0 when healthy). |
| domain.webhookStatus.firstFailedAt | integer · null | When the current failing streak began, in Unix epoch milliseconds (UTC); null when it has never happened. |
| domain.webhookStatus.unhealthy | boolean | True once the failing streak passes the unhealthy threshold. |
| domain.dailyLimitReached | boolean | True when the owning account has hit its daily send cap. List/detail responses only. |
| dns req | dns-record[] | Always empty for a claimed subdomain. Present so the shape matches createDomain, where it carries the records to publish. |
| dns[].type req | "MX" · "TXT" · "CNAME" · "A" | Record type. |
| dns[].name req | string | Full hostname (FQDN) the record lives at. |
| dns[].value req | string | Bare RDATA value. |
| dns[].priority | integer | MX priority — present on MX records only. |
An actual response, recorded from the conformance suite.
{
"domain": {
"id": "dom_2VbXqTpN8rKw",
"user_id": "usr_7Fj3MnQw",
"domain": "swift-otter.mailk.us",
"status": "verified",
"mx_verified": 1,
"spf_verified": 1,
"dkim_verified": 1,
"dmarc_verified": 1,
"zero_retention": 0,
"track_opens": 0,
"enc_public_key": null,
"enc_key_alg": null,
"enc_fingerprint": null,
"enc_format": null,
"provider": "cloudflare",
"email_provider_id": "prov_cloudflare",
"dns_tokens": null,
"created_at": 1769731200000,
"last_checked_at": 1769731200000
},
"dns": []
} {
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://api.mailkite.dev/v1/schemas/claim-subdomain-response.json",
"title": "Claim subdomain response",
"description": "POST /api/domains/subdomain — the claimed subdomain, already verified. Unlike a domain you bring yourself, the pool zone covers this hostname entirely: `dns` is an empty array because there is nothing for you to publish.",
"type": "object",
"required": [
"domain",
"dns"
],
"properties": {
"domain": {
"$ref": "domain.json",
"description": "The new domain row. Returned with `status: \"verified\"` and all four DNS checks passing — it can send and receive immediately (subject to the account email being verified)."
},
"dns": {
"type": "array",
"description": "Always empty for a claimed subdomain. Present so the shape matches createDomain, where it carries the records to publish.",
"items": {
"$ref": "dns-record.json"
},
"examples": [
[]
]
}
}
} GET /api/domains/:id Get one domain with DNS records + webhook.
Get one domain with DNS records + webhook.
Response body
get-domain-response.jsonGET /api/domains/:id — the domain (with webhook enrichment) plus its current DNS record set.
| Field | Type | Notes |
|---|---|---|
| domain req | domain | A sending/receiving domain on your account. List and detail responses enrich the stored row with the domain's catch-all webhook (webhookUrl/webhookAckMode/webhookStatus) and the account's daily-limit state; write responses return the row alone, so those fields are optional here. |
| domain.id req | string | Domain id (dom_…). |
| domain.user_id req | string | Owning account (usr_…). |
| domain.domain req | string | The domain name, lowercased. |
| domain.status req | "pending" · "verified" · "failed" | Overall verification state. Inbound routing requires at least MX verification; `verified` follows the MX check. |
| domain.mx_verified req | 0 · 1 | 1 once the MX record resolves to MailKite — the gate for inbound mail, webhooks, and routes. |
| domain.spf_verified req | 0 · 1 | 1 once the SPF TXT record is in place. |
| domain.dkim_verified req | 0 · 1 | 1 once the DKIM records are accepted by the sending provider. |
| domain.dmarc_verified req | 0 · 1 | 1 once a DMARC policy is published. |
| domain.zero_retention | 0 · 1 | 1 when zero-retention passthrough is on: inbound mail is delivered to the webhook and never stored (no replay, no dashboard history). |
| domain.track_opens | 0 · 1 | Domain-wide default for outbound open tracking (a per-send trackOpens overrides it). |
| domain.enc_public_key | string · null | At-rest encryption public key (PEM SPKI, armored OpenPGP key, or X.509 cert). null = encryption off. |
| domain.enc_key_alg | string · null | Key algorithm recorded for the uploaded key ('RSA-OAEP-256', 'PGP', or 'S/MIME'). null = encryption off. |
| domain.enc_fingerprint | string · null | Fingerprint of the uploaded key (sha256 of the SPKI/cert DER, or the OpenPGP fingerprint). |
| domain.enc_format | string · null | Which client-decryptable standard the key is: 'pgp' or 'smime'. null = off or legacy custom envelope. |
| domain.provider req | string | Email sending provider type currently backing this domain (e.g. 'ses', 'cloudflare'). Assigned by MailKite, never chosen by the caller. |
| domain.email_provider_id | string · null | Registry id of the concrete provider instance backing the domain. |
| domain.dns_tokens | string · null | Provider-specific DNS tokens as a JSON string (e.g. SES DKIM CNAME targets). Feeds the dns record set; null until providers report them. |
| domain.last_checked_at | integer · null | When domain verification last ran, in Unix epoch milliseconds (UTC); null when it has never happened. |
| domain.created_at req | integer | When the domain was added, in Unix epoch milliseconds (UTC). |
| domain.webhookUrl | string · null | URL of the domain's catch-all webhook, or null when none is set. List/detail responses only. |
| domain.webhookAckMode | string · null | The webhook's acknowledgement mode: 'lenient' (any 2xx) or 'ack' (2xx + explicit ok). List/detail responses only. |
| domain.webhookStatus | object · null | Health of the webhook's most recent delivery, or null when it has never been exercised. List/detail responses only. |
| domain.webhookStatus.status | string · null | 'ok', 'fail', 'running', or 'timeout'. |
| domain.webhookStatus.message | string · null | Human-readable detail of the last outcome (e.g. 'HTTP 500'). |
| domain.webhookStatus.code | integer · null | HTTP status of the last delivery, when relevant. |
| domain.webhookStatus.at | integer · null | When the last outcome landed, in Unix epoch milliseconds (UTC); null when it has never happened. |
| domain.webhookStatus.consecutiveFailures | integer | Current failing streak (0 when healthy). |
| domain.webhookStatus.firstFailedAt | integer · null | When the current failing streak began, in Unix epoch milliseconds (UTC); null when it has never happened. |
| domain.webhookStatus.unhealthy | boolean | True once the failing streak passes the unhealthy threshold. |
| domain.dailyLimitReached | boolean | True when the owning account has hit its daily send cap. List/detail responses only. |
| dns req | dns-record[] | The records to publish at your DNS provider. |
| dns[].type req | "MX" · "TXT" · "CNAME" · "A" | Record type. |
| dns[].name req | string | Full hostname (FQDN) the record lives at. |
| dns[].value req | string | Bare RDATA value. |
| dns[].priority | integer | MX priority — present on MX records only. |
An actual response, recorded from the conformance suite.
{
"domain": {
"id": "dom_2VbXqTpN8rKw",
"user_id": "usr_7Fj3MnQw",
"domain": "myapp.ai",
"status": "verified",
"mx_verified": 1,
"spf_verified": 1,
"dkim_verified": 1,
"dmarc_verified": 1,
"zero_retention": 0,
"track_opens": 0,
"enc_public_key": null,
"enc_key_alg": null,
"enc_fingerprint": null,
"enc_format": null,
"provider": "ses",
"email_provider_id": "prov_ses_use2",
"dns_tokens": null,
"created_at": 1769731200000,
"last_checked_at": 1769817600000,
"webhookUrl": "https://myapp.ai/hooks/mailkite",
"webhookAckMode": "lenient",
"webhookStatus": {
"status": "ok",
"message": null,
"code": 200,
"at": 1769817600000,
"consecutiveFailures": 0,
"firstFailedAt": null,
"unhealthy": false
},
"dailyLimitReached": false
},
"dns": [
{
"type": "MX",
"name": "myapp.ai",
"value": "mx.mailkite.dev",
"priority": 10
},
{
"type": "TXT",
"name": "myapp.ai",
"value": "v=spf1 include:spf.mailkite.dev ~all"
},
{
"type": "TXT",
"name": "_dmarc.myapp.ai",
"value": "v=DMARC1; p=none; rua=mailto:dmarc@myapp.ai"
},
{
"type": "CNAME",
"name": "abc123._domainkey.myapp.ai",
"value": "abc123.dkim.amazonses.com"
}
]
} {
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://api.mailkite.dev/v1/schemas/get-domain-response.json",
"title": "Domain detail response",
"description": "GET /api/domains/:id — the domain (with webhook enrichment) plus its current DNS record set.",
"type": "object",
"required": [
"domain",
"dns"
],
"properties": {
"domain": {
"$ref": "domain.json"
},
"dns": {
"type": "array",
"description": "The records to publish at your DNS provider.",
"items": {
"$ref": "dns-record.json"
}
}
}
} DELETE /api/domains/:id Remove a domain.
Remove a domain.
Response body
delete-domain-response.jsonDELETE /api/domains/:id — confirmation plus what infrastructure teardown accomplished. Teardown is best-effort and never blocks the delete; warnings record anything left behind.
| Field | Type | Notes |
|---|---|---|
| ok req | true | |
| cleanup req | object | What the teardown did. |
| cleanup.deregistered req | string[] | Provider types the domain was deregistered from. |
| cleanup.dnsDeleted req | integer | How many MailKite-hosted DNS records were removed (0 when we don't host the domain's DNS). |
| cleanup.warnings req | string[] | Steps that failed — the only record that something was left behind. |
An actual response, recorded from the conformance suite.
{
"ok": true,
"cleanup": {
"deregistered": [
"ses",
"cloudflare"
],
"dnsDeleted": 0,
"warnings": []
}
} {
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://api.mailkite.dev/v1/schemas/delete-domain-response.json",
"title": "Delete domain response",
"description": "DELETE /api/domains/:id — confirmation plus what infrastructure teardown accomplished. Teardown is best-effort and never blocks the delete; warnings record anything left behind.",
"type": "object",
"required": [
"ok",
"cleanup"
],
"properties": {
"ok": {
"type": "boolean",
"const": true
},
"cleanup": {
"type": "object",
"required": [
"deregistered",
"dnsDeleted",
"warnings"
],
"description": "What the teardown did.",
"properties": {
"deregistered": {
"type": "array",
"description": "Provider types the domain was deregistered from.",
"items": {
"type": "string"
}
},
"dnsDeleted": {
"type": "integer",
"description": "How many MailKite-hosted DNS records were removed (0 when we don't host the domain's DNS)."
},
"warnings": {
"type": "array",
"description": "Steps that failed — the only record that something was left behind.",
"items": {
"type": "string"
}
}
}
}
}
} POST /api/domains/:id/verify Check DNS and update status.
Check DNS and update status.
Response body
verify-domain-response.jsonPOST /api/domains/:id/verify — fresh DNS check results, the resulting status, and the sending provider's enablement outcome.
| Field | Type | Notes |
|---|---|---|
| status req | "verified" · "pending" | 'verified' once MX resolves; SPF/DKIM/DMARC refine sending but don't gate inbound. |
| checks req | object | Per-record verification results from this check. |
| checks.mx req | boolean | MX resolves to MailKite. |
| checks.spf req | boolean | SPF TXT record is in place. |
| checks.dkim req | boolean | DKIM records are accepted by the sending provider. |
| checks.dmarc req | boolean | A DMARC policy is published. |
| checkedAt req | integer | When this check ran, in Unix epoch milliseconds (UTC). |
| sending req | object | Outcome of enabling outbound sending with the domain's provider (attempted once SPF+DKIM pass). |
| sending.ok req | boolean | The provider call succeeded. |
| sending.enabled | boolean | Final enabled state reported by the provider. |
| sending.dkimVerified | boolean | Whether the provider accepted the DKIM records (can lead `enabled`). |
| sending.alreadyEnabled | boolean | True when the domain was already registered with the provider (no-op). |
| sending.providerId | string | Provider-specific domain identifier, when reported. |
| sending.error | string | Why the provider call failed, when it did. |
An actual response, recorded from the conformance suite.
{
"status": "verified",
"checks": {
"mx": true,
"spf": true,
"dkim": true,
"dmarc": true
},
"checkedAt": 1769817600000,
"sending": {
"ok": true,
"enabled": true,
"dkimVerified": true
}
} {
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://api.mailkite.dev/v1/schemas/verify-domain-response.json",
"title": "Verify domain response",
"description": "POST /api/domains/:id/verify — fresh DNS check results, the resulting status, and the sending provider's enablement outcome.",
"type": "object",
"required": [
"status",
"checks",
"checkedAt",
"sending"
],
"properties": {
"status": {
"type": "string",
"enum": [
"verified",
"pending"
],
"description": "'verified' once MX resolves; SPF/DKIM/DMARC refine sending but don't gate inbound."
},
"checks": {
"type": "object",
"required": [
"mx",
"spf",
"dkim",
"dmarc"
],
"description": "Per-record verification results from this check.",
"properties": {
"mx": {
"type": "boolean",
"description": "MX resolves to MailKite."
},
"spf": {
"type": "boolean",
"description": "SPF TXT record is in place."
},
"dkim": {
"type": "boolean",
"description": "DKIM records are accepted by the sending provider."
},
"dmarc": {
"type": "boolean",
"description": "A DMARC policy is published."
}
}
},
"checkedAt": {
"type": "integer",
"description": "When this check ran, in Unix epoch milliseconds (UTC).",
"examples": [
1769731200000
]
},
"sending": {
"type": "object",
"required": [
"ok"
],
"description": "Outcome of enabling outbound sending with the domain's provider (attempted once SPF+DKIM pass).",
"properties": {
"ok": {
"type": "boolean",
"description": "The provider call succeeded."
},
"enabled": {
"type": "boolean",
"description": "Final enabled state reported by the provider."
},
"dkimVerified": {
"type": "boolean",
"description": "Whether the provider accepted the DKIM records (can lead `enabled`)."
},
"alreadyEnabled": {
"type": "boolean",
"description": "True when the domain was already registered with the provider (no-op)."
},
"providerId": {
"type": "string",
"description": "Provider-specific domain identifier, when reported."
},
"error": {
"type": "string",
"description": "Why the provider call failed, when it did."
}
}
}
}
} PUT /api/domains/:id/webhook Set or replace the domain's catch-all webhook.
Set or replace the domain's catch-all webhook.
Request body
set-webhook-request.json| Field | Type | Notes |
|---|---|---|
| url req | string |
The smallest body that makes this call — every other field is optional.
{
"url": "https://app.com/hooks/mailkite"
} {
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://api.mailkite.dev/v1/schemas/set-webhook-request.json",
"title": "Set webhook request body",
"type": "object",
"required": [
"url"
],
"additionalProperties": false,
"properties": {
"url": {
"type": "string"
}
}
} Response body
set-webhook-response.jsonPUT /api/domains/:id/webhook — the domain, the stored webhook, and this route's signing secret (returned here so the receiver can verify x-mailkite-signature without a second call).
| Field | Type | Notes |
|---|---|---|
| domain req | domain | The domain row (without webhook enrichment — the webhook you just set is alongside). |
| domain.id req | string | Domain id (dom_…). |
| domain.user_id req | string | Owning account (usr_…). |
| domain.domain req | string | The domain name, lowercased. |
| domain.status req | "pending" · "verified" · "failed" | Overall verification state. Inbound routing requires at least MX verification; `verified` follows the MX check. |
| domain.mx_verified req | 0 · 1 | 1 once the MX record resolves to MailKite — the gate for inbound mail, webhooks, and routes. |
| domain.spf_verified req | 0 · 1 | 1 once the SPF TXT record is in place. |
| domain.dkim_verified req | 0 · 1 | 1 once the DKIM records are accepted by the sending provider. |
| domain.dmarc_verified req | 0 · 1 | 1 once a DMARC policy is published. |
| domain.zero_retention | 0 · 1 | 1 when zero-retention passthrough is on: inbound mail is delivered to the webhook and never stored (no replay, no dashboard history). |
| domain.track_opens | 0 · 1 | Domain-wide default for outbound open tracking (a per-send trackOpens overrides it). |
| domain.enc_public_key | string · null | At-rest encryption public key (PEM SPKI, armored OpenPGP key, or X.509 cert). null = encryption off. |
| domain.enc_key_alg | string · null | Key algorithm recorded for the uploaded key ('RSA-OAEP-256', 'PGP', or 'S/MIME'). null = encryption off. |
| domain.enc_fingerprint | string · null | Fingerprint of the uploaded key (sha256 of the SPKI/cert DER, or the OpenPGP fingerprint). |
| domain.enc_format | string · null | Which client-decryptable standard the key is: 'pgp' or 'smime'. null = off or legacy custom envelope. |
| domain.provider req | string | Email sending provider type currently backing this domain (e.g. 'ses', 'cloudflare'). Assigned by MailKite, never chosen by the caller. |
| domain.email_provider_id | string · null | Registry id of the concrete provider instance backing the domain. |
| domain.dns_tokens | string · null | Provider-specific DNS tokens as a JSON string (e.g. SES DKIM CNAME targets). Feeds the dns record set; null until providers report them. |
| domain.last_checked_at | integer · null | When domain verification last ran, in Unix epoch milliseconds (UTC); null when it has never happened. |
| domain.created_at req | integer | When the domain was added, in Unix epoch milliseconds (UTC). |
| domain.webhookUrl | string · null | URL of the domain's catch-all webhook, or null when none is set. List/detail responses only. |
| domain.webhookAckMode | string · null | The webhook's acknowledgement mode: 'lenient' (any 2xx) or 'ack' (2xx + explicit ok). List/detail responses only. |
| domain.webhookStatus | object · null | Health of the webhook's most recent delivery, or null when it has never been exercised. List/detail responses only. |
| domain.webhookStatus.status | string · null | 'ok', 'fail', 'running', or 'timeout'. |
| domain.webhookStatus.message | string · null | Human-readable detail of the last outcome (e.g. 'HTTP 500'). |
| domain.webhookStatus.code | integer · null | HTTP status of the last delivery, when relevant. |
| domain.webhookStatus.at | integer · null | When the last outcome landed, in Unix epoch milliseconds (UTC); null when it has never happened. |
| domain.webhookStatus.consecutiveFailures | integer | Current failing streak (0 when healthy). |
| domain.webhookStatus.firstFailedAt | integer · null | When the current failing streak began, in Unix epoch milliseconds (UTC); null when it has never happened. |
| domain.webhookStatus.unhealthy | boolean | True once the failing streak passes the unhealthy threshold. |
| domain.dailyLimitReached | boolean | True when the owning account has hit its daily send cap. List/detail responses only. |
| webhookUrl req | string | The stored webhook URL. |
| ackMode req | "lenient" · "ack" | Acknowledgement strictness in effect. |
| routeId req | string | The catch-all route (rte_…) that carries this webhook. |
| signingSecret req | string | This route's signing secret (whsec_…). Store it server-side; verify every delivery. |
An actual response, recorded from the conformance suite.
{
"domain": {
"id": "dom_2VbXqTpN8rKw",
"user_id": "usr_7Fj3MnQw",
"domain": "myapp.ai",
"status": "verified",
"mx_verified": 1,
"spf_verified": 1,
"dkim_verified": 1,
"dmarc_verified": 1,
"zero_retention": 0,
"track_opens": 0,
"enc_public_key": null,
"enc_key_alg": null,
"enc_fingerprint": null,
"enc_format": null,
"provider": "ses",
"email_provider_id": "prov_ses_use2",
"dns_tokens": null,
"created_at": 1769731200000,
"last_checked_at": 1769817600000
},
"webhookUrl": "https://myapp.ai/hooks/mailkite",
"ackMode": "lenient",
"routeId": "rte_4Bn8XcVm",
"signingSecret": "whsec_9f2cQpVn4tLd8RtZ"
} {
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://api.mailkite.dev/v1/schemas/set-webhook-response.json",
"title": "Set webhook response",
"description": "PUT /api/domains/:id/webhook — the domain, the stored webhook, and this route's signing secret (returned here so the receiver can verify x-mailkite-signature without a second call).",
"type": "object",
"required": [
"domain",
"webhookUrl",
"ackMode",
"routeId",
"signingSecret"
],
"properties": {
"domain": {
"$ref": "domain.json",
"description": "The domain row (without webhook enrichment — the webhook you just set is alongside)."
},
"webhookUrl": {
"type": "string",
"description": "The stored webhook URL.",
"examples": [
"https://myapp.ai/hooks/mailkite"
]
},
"ackMode": {
"type": "string",
"enum": [
"lenient",
"ack"
],
"description": "Acknowledgement strictness in effect."
},
"routeId": {
"type": "string",
"description": "The catch-all route (rte_…) that carries this webhook.",
"examples": [
"rte_4Bn8XcVm"
]
},
"signingSecret": {
"type": "string",
"description": "This route's signing secret (whsec_…). Store it server-side; verify every delivery.",
"examples": [
"whsec_9f2cQpVn4tLd8RtZ"
]
}
}
} PUT /api/domains/:id/tracking-webhook Set or replace the domain's dedicated tracking-event webhook: an HTTPS endpoint that receives signed email.* engagement events (email.sent / email.bounced / email.complained / email.opened / email.clicked, shaped per the tracking-event schema) SEPARATELY from inbound mail. This is the split-endpoint override for consumers that keep engagement events away from their inbound webhook (e.g. django-anymail's paired views); most consumers should prefer setWebhookEvents, which delivers everything to the one inbound webhook. When both are configured, this URL wins for engagement events. Returns the signing secret (the same account secret as inbound deliveries).
Set or replace the domain's dedicated tracking-event webhook: an HTTPS endpoint that receives signed email.* engagement events (email.sent / email.bounced / email.complained / email.opened / email.clicked, shaped per the tracking-event schema) SEPARATELY from inbound mail. This is the split-endpoint override for consumers that keep engagement events away from their inbound webhook (e.g. django-anymail's paired views); most consumers should prefer setWebhookEvents, which delivers everything to the one inbound webhook. When both are configured, this URL wins for engagement events. Returns the signing secret (the same account secret as inbound deliveries).
Request body
set-tracking-webhook-request.json| Field | Type | Notes |
|---|---|---|
| url req | string | HTTPS endpoint to receive signed email.* engagement events (sent/bounced/complained/opened/clicked). Deliberately separate from the inbound-mail webhook, so inbound consumers never see event types they don't expect. |
The smallest body that makes this call — every other field is optional.
{
"url": "https://app.com/hooks/mailkite-events"
} {
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://api.mailkite.dev/v1/schemas/set-tracking-webhook-request.json",
"title": "Set tracking webhook request body",
"type": "object",
"required": [
"url"
],
"additionalProperties": false,
"properties": {
"url": {
"type": "string",
"description": "HTTPS endpoint to receive signed email.* engagement events (sent/bounced/complained/opened/clicked). Deliberately separate from the inbound-mail webhook, so inbound consumers never see event types they don't expect."
}
}
} Response body
set-tracking-webhook-response.json| Field | Type | Notes |
|---|---|---|
| domain req | object | The updated domain row, now carrying tracking_webhook_url. |
| signingSecret req | string | The account webhook secret events are signed with (same secret as inbound deliveries) — verify the x-mailkite-signature header against it. |
An actual response, recorded from the conformance suite.
{
"domain": {
"id": "dom_2VbXqTpN8rKw",
"domain": "myapp.ai",
"status": "verified",
"tracking_webhook_url": "https://app.com/hooks/mailkite-events"
},
"signingSecret": "whsec_9f2cQpVn4tLd8RtZ"
} {
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://api.mailkite.dev/v1/schemas/set-tracking-webhook-response.json",
"title": "Set tracking webhook response body",
"type": "object",
"required": [
"domain",
"signingSecret"
],
"properties": {
"domain": {
"type": "object",
"description": "The updated domain row, now carrying tracking_webhook_url."
},
"signingSecret": {
"type": "string",
"description": "The account webhook secret events are signed with (same secret as inbound deliveries) — verify the x-mailkite-signature header against it."
}
}
} DELETE /api/domains/:id/tracking-webhook Remove the domain's tracking-event webhook (engagement events stop).
Remove the domain's tracking-event webhook (engagement events stop).
Response body
ok-response.jsonThe uniform acknowledgement for deletes and other actions with nothing else to report.
| Field | Type | Notes |
|---|---|---|
| ok req | true | Always true — a non-2xx error body is returned otherwise. |
An actual response, recorded from the conformance suite.
{
"ok": true
} {
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://api.mailkite.dev/v1/schemas/ok-response.json",
"title": "OK",
"description": "The uniform acknowledgement for deletes and other actions with nothing else to report.",
"type": "object",
"required": [
"ok"
],
"properties": {
"ok": {
"type": "boolean",
"const": true,
"description": "Always true — a non-2xx error body is returned otherwise."
}
}
} PUT /api/domains/:id/webhook-events Opt the domain's inbound webhook into engagement events — one webhook, all events. Pass "all" or a list of email.* tracking types (email.sent / email.bounced / email.complained / email.opened / email.clicked) and MailKite delivers them to the same webhook route that receives the domain's inbound mail; consumers switch on the payload's `type` (inbound mail is type email.received, engagement events follow the tracking-event schema). Off by default, so existing inbound consumers never see event types they didn't opt into. Events at the inbound webhook are signed with that route's secret (account secret fallback) — the same key inbound deliveries already use. If a dedicated tracking webhook URL is set (setTrackingWebhook), engagement events go there instead.
Opt the domain's inbound webhook into engagement events — one webhook, all events. Pass "all" or a list of email.* tracking types (email.sent / email.bounced / email.complained / email.opened / email.clicked) and MailKite delivers them to the same webhook route that receives the domain's inbound mail; consumers switch on the payload's `type` (inbound mail is type email.received, engagement events follow the tracking-event schema). Off by default, so existing inbound consumers never see event types they didn't opt into. Events at the inbound webhook are signed with that route's secret (account secret fallback) — the same key inbound deliveries already use. If a dedicated tracking webhook URL is set (setTrackingWebhook), engagement events go there instead.
Request body
set-webhook-events-request.json| Field | Type | Notes |
|---|---|---|
| events req | "all" · "email.sent" · "email.bounced" · "email.complained" · "email.opened" · "email.clicked"[] | Which email.* engagement events the domain's inbound webhook also receives: the literal "all", or a non-empty list of event types. Consumers distinguish deliveries by the payload's `type` (inbound mail is email.received; engagement events follow the tracking-event schema). |
The smallest body that makes this call — every other field is optional.
{
"events": [
"email.bounced",
"email.complained"
]
} {
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://api.mailkite.dev/v1/schemas/set-webhook-events-request.json",
"title": "Set webhook events request body",
"type": "object",
"required": [
"events"
],
"additionalProperties": false,
"properties": {
"events": {
"description": "Which email.* engagement events the domain's inbound webhook also receives: the literal \"all\", or a non-empty list of event types. Consumers distinguish deliveries by the payload's `type` (inbound mail is email.received; engagement events follow the tracking-event schema).",
"oneOf": [
{
"const": "all"
},
{
"type": "array",
"minItems": 1,
"items": {
"enum": [
"email.sent",
"email.bounced",
"email.complained",
"email.opened",
"email.clicked"
]
}
}
]
}
}
} Response body
set-webhook-events-response.json| Field | Type | Notes |
|---|---|---|
| domain req | object | The updated domain row, now carrying webhook_events (a JSON-encoded "all" or event-type list). |
| signingSecret req | string | The account webhook secret (fallback signing key). Events delivered to the inbound webhook are signed with that route's own secret when it has one, falling back to this — the same keys inbound deliveries already use, so verification code needs no change. |
An actual response, recorded from the conformance suite.
{
"domain": {
"id": "dom_2VbXqTpN8rKw",
"domain": "myapp.ai",
"status": "verified",
"webhook_events": "[\"email.bounced\",\"email.complained\"]"
},
"signingSecret": "whsec_9f2cQpVn4tLd8RtZ"
} {
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://api.mailkite.dev/v1/schemas/set-webhook-events-response.json",
"title": "Set webhook events response body",
"type": "object",
"required": [
"domain",
"signingSecret"
],
"properties": {
"domain": {
"type": "object",
"description": "The updated domain row, now carrying webhook_events (a JSON-encoded \"all\" or event-type list)."
},
"signingSecret": {
"type": "string",
"description": "The account webhook secret (fallback signing key). Events delivered to the inbound webhook are signed with that route's own secret when it has one, falling back to this — the same keys inbound deliveries already use, so verification code needs no change."
}
}
} DELETE /api/domains/:id/webhook-events Opt the domain's inbound webhook back out of engagement events (inbound email.received only — the default).
Opt the domain's inbound webhook back out of engagement events (inbound email.received only — the default).
Response body
ok-response.jsonThe uniform acknowledgement for deletes and other actions with nothing else to report.
| Field | Type | Notes |
|---|---|---|
| ok req | true | Always true — a non-2xx error body is returned otherwise. |
An actual response, recorded from the conformance suite.
{
"ok": true
} {
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://api.mailkite.dev/v1/schemas/ok-response.json",
"title": "OK",
"description": "The uniform acknowledgement for deletes and other actions with nothing else to report.",
"type": "object",
"required": [
"ok"
],
"properties": {
"ok": {
"type": "boolean",
"const": true,
"description": "Always true — a non-2xx error body is returned otherwise."
}
}
} DELETE /api/domains/:id/webhook Remove the domain's webhook.
Remove the domain's webhook.
Response body
ok-response.jsonThe uniform acknowledgement for deletes and other actions with nothing else to report.
| Field | Type | Notes |
|---|---|---|
| ok req | true | Always true — a non-2xx error body is returned otherwise. |
An actual response, recorded from the conformance suite.
{
"ok": true
} {
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://api.mailkite.dev/v1/schemas/ok-response.json",
"title": "OK",
"description": "The uniform acknowledgement for deletes and other actions with nothing else to report.",
"type": "object",
"required": [
"ok"
],
"properties": {
"ok": {
"type": "boolean",
"const": true,
"description": "Always true — a non-2xx error body is returned otherwise."
}
}
} GET /api/domains/:id/webhook/secret Get this domain's webhook signing secret (whsec_…) — the per-route secret used to verify x-mailkite-signature on its inbound deliveries. Server-side only (SDK/REST); never exposed to MCP or browser client libraries.
Get this domain's webhook signing secret (whsec_…) — the per-route secret used to verify x-mailkite-signature on its inbound deliveries. Server-side only (SDK/REST); never exposed to MCP or browser client libraries.
Response body
webhook-secret-response.jsonGET /api/domains/:id/webhook/secret — this domain's webhook signing secret. Server-side only; never exposed to MCP or browser clients.
| Field | Type | Notes |
|---|---|---|
| secret req | string | The per-route signing secret (whsec_…) for the domain's catch-all webhook. |
An actual response, recorded from the conformance suite.
{
"secret": "whsec_9f2cQpVn4tLd8RtZ"
} {
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://api.mailkite.dev/v1/schemas/webhook-secret-response.json",
"title": "Webhook secret response",
"description": "GET /api/domains/:id/webhook/secret — this domain's webhook signing secret. Server-side only; never exposed to MCP or browser clients.",
"type": "object",
"required": [
"secret"
],
"properties": {
"secret": {
"type": "string",
"description": "The per-route signing secret (whsec_…) for the domain's catch-all webhook.",
"examples": [
"whsec_9f2cQpVn4tLd8RtZ"
]
}
}
} POST /api/domains/:id/webhook/test Send a signed test event to the domain's webhook.
Send a signed test event to the domain's webhook.
Response body
test-webhook-response.jsonPOST /api/domains/:id/webhook/test — outcome of the signed test delivery.
| Field | Type | Notes |
|---|---|---|
| ok req | boolean | The endpoint accepted the test event (2xx, and acknowledged when ackMode is 'ack'). |
| status req | integer | HTTP status the endpoint returned (0 when the request failed before a response). |
An actual response, recorded from the conformance suite.
{
"ok": true,
"status": 200
} {
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://api.mailkite.dev/v1/schemas/test-webhook-response.json",
"title": "Test webhook response",
"description": "POST /api/domains/:id/webhook/test — outcome of the signed test delivery.",
"type": "object",
"required": [
"ok",
"status"
],
"properties": {
"ok": {
"type": "boolean",
"description": "The endpoint accepted the test event (2xx, and acknowledged when ackMode is 'ack')."
},
"status": {
"type": "integer",
"description": "HTTP status the endpoint returned (0 when the request failed before a response).",
"examples": [
200
]
}
}
} GET /api/domains/register/check Check whether a domain is available to register, and at what price. Read-only — no charge.
Check whether a domain is available to register, and at what price. Read-only — no charge.
Response body
domain-availability-response.jsonGET /api/domains/register/check — whether a domain can be registered through MailKite, and at what price. Read-only; no charge.
| Field | Type | Notes |
|---|---|---|
| configured req | boolean | False when domain registration isn't available on this deployment — the other fields are then placeholders. |
| domain req | string | The normalized domain that was checked. |
| available req | boolean | True when the domain can be registered. |
| premium req | boolean | True for registry-premium names, which can't be registered here. |
| reason | string | Why an unavailable domain can't be registered (e.g. 'Already on MailKite'). |
| price | object | What you'd actually be charged (registrar cost grossed up with fees). Absent when unavailable. |
| price.amount req | number | Price in major units of `currency`. |
| price.currency req | string | ISO 4217 currency code. |
| price.period req | integer | How many periodUnits the amount covers. |
| price.periodUnit req | "y" · "m" | Unit of `period` — years or months. |
An actual response, recorded from the conformance suite.
{
"configured": true,
"domain": "myapp.ai",
"available": true,
"premium": false,
"price": {
"amount": 12.99,
"currency": "USD",
"period": 1,
"periodUnit": "y"
}
} {
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://api.mailkite.dev/v1/schemas/domain-availability-response.json",
"title": "Domain availability response",
"description": "GET /api/domains/register/check — whether a domain can be registered through MailKite, and at what price. Read-only; no charge.",
"type": "object",
"required": [
"configured",
"domain",
"available",
"premium"
],
"properties": {
"configured": {
"type": "boolean",
"description": "False when domain registration isn't available on this deployment — the other fields are then placeholders."
},
"domain": {
"type": "string",
"description": "The normalized domain that was checked.",
"examples": [
"myapp.ai"
]
},
"available": {
"type": "boolean",
"description": "True when the domain can be registered."
},
"premium": {
"type": "boolean",
"description": "True for registry-premium names, which can't be registered here."
},
"reason": {
"type": "string",
"description": "Why an unavailable domain can't be registered (e.g. 'Already on MailKite')."
},
"price": {
"type": "object",
"required": [
"amount",
"currency",
"period",
"periodUnit"
],
"description": "What you'd actually be charged (registrar cost grossed up with fees). Absent when unavailable.",
"properties": {
"amount": {
"type": "number",
"description": "Price in major units of `currency`.",
"examples": [
12.99
]
},
"currency": {
"type": "string",
"description": "ISO 4217 currency code.",
"examples": [
"USD"
]
},
"period": {
"type": "integer",
"description": "How many periodUnits the amount covers.",
"examples": [
1
]
},
"periodUnit": {
"type": "string",
"enum": [
"y",
"m"
],
"description": "Unit of `period` — years or months."
}
}
}
}
} POST /api/domains/register Register (buy) a domain on the customer's behalf; provisions mail DNS and adds it to the account in one call. Charges the registrar.
Register (buy) a domain on the customer's behalf; provisions mail DNS and adds it to the account in one call. Charges the registrar.
Request body
register-domain-request.json| Field | Type | Notes |
|---|---|---|
| domain req | string | |
| contact req | object | |
| contact.firstName req | string | |
| contact.lastName req | string | |
| contact.email req | string | |
| contact.phone req | string | E.164-ish, +<cc>.<number>, e.g. +1.4155551234 |
| contact.address req | string | |
| contact.city req | string | |
| contact.zip req | string | |
| contact.country req | string | ISO 3166-1 alpha-2, e.g. US |
| contact.state | string | ISO 3166-2 subdivision, e.g. US-CA |
| contact.organization | string | |
| contact.type | "individual" · "company" · "association" · "publicbody" | |
| years | integer | |
| dryRun | boolean |
The smallest body that makes this call — every other field is optional.
{
"domain": "acme.com",
"contact": {
"firstName": "Jane",
"lastName": "Doe",
"email": "jane@example.com",
"phone": "+1.4155551234",
"address": "123 Main St",
"city": "SF",
"zip": "94016",
"country": "US"
},
"years": 1
} {
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://api.mailkite.dev/v1/schemas/register-domain-request.json",
"title": "Register domain request body",
"type": "object",
"required": [
"domain",
"contact"
],
"additionalProperties": false,
"properties": {
"domain": {
"type": "string"
},
"contact": {
"type": "object",
"required": [
"firstName",
"lastName",
"email",
"phone",
"address",
"city",
"zip",
"country"
],
"additionalProperties": false,
"properties": {
"firstName": {
"type": "string"
},
"lastName": {
"type": "string"
},
"email": {
"type": "string"
},
"phone": {
"type": "string",
"description": "E.164-ish, +<cc>.<number>, e.g. +1.4155551234"
},
"address": {
"type": "string"
},
"city": {
"type": "string"
},
"zip": {
"type": "string"
},
"country": {
"type": "string",
"description": "ISO 3166-1 alpha-2, e.g. US"
},
"state": {
"type": "string",
"description": "ISO 3166-2 subdivision, e.g. US-CA"
},
"organization": {
"type": "string"
},
"type": {
"type": "string",
"enum": [
"individual",
"company",
"association",
"publicbody"
]
}
}
},
"years": {
"type": "integer",
"minimum": 1
},
"dryRun": {
"type": "boolean"
}
}
} Response body
register-domain-response.jsonPOST /api/domains/register — a dry run returns just { registration, dryRun }; a real registration returns the new domain, its DNS set (auto-provisioned on the registrar when we manage its DNS), the registration reference, and the charge.
| Field | Type | Notes |
|---|---|---|
| registration req | object | The registrar's view of the registration. |
| registration.status req | string | Registrar status (e.g. 'registered', 'pending'). |
| registration.reference | string · null | Registrar order/operation reference, when provided. |
| dryRun | true | Present (true) only on dry-run responses — nothing was charged or created. |
| domain | domain | The new domain row. Real registrations only. |
| domain.id req | string | Domain id (dom_…). |
| domain.user_id req | string | Owning account (usr_…). |
| domain.domain req | string | The domain name, lowercased. |
| domain.status req | "pending" · "verified" · "failed" | Overall verification state. Inbound routing requires at least MX verification; `verified` follows the MX check. |
| domain.mx_verified req | 0 · 1 | 1 once the MX record resolves to MailKite — the gate for inbound mail, webhooks, and routes. |
| domain.spf_verified req | 0 · 1 | 1 once the SPF TXT record is in place. |
| domain.dkim_verified req | 0 · 1 | 1 once the DKIM records are accepted by the sending provider. |
| domain.dmarc_verified req | 0 · 1 | 1 once a DMARC policy is published. |
| domain.zero_retention | 0 · 1 | 1 when zero-retention passthrough is on: inbound mail is delivered to the webhook and never stored (no replay, no dashboard history). |
| domain.track_opens | 0 · 1 | Domain-wide default for outbound open tracking (a per-send trackOpens overrides it). |
| domain.enc_public_key | string · null | At-rest encryption public key (PEM SPKI, armored OpenPGP key, or X.509 cert). null = encryption off. |
| domain.enc_key_alg | string · null | Key algorithm recorded for the uploaded key ('RSA-OAEP-256', 'PGP', or 'S/MIME'). null = encryption off. |
| domain.enc_fingerprint | string · null | Fingerprint of the uploaded key (sha256 of the SPKI/cert DER, or the OpenPGP fingerprint). |
| domain.enc_format | string · null | Which client-decryptable standard the key is: 'pgp' or 'smime'. null = off or legacy custom envelope. |
| domain.provider req | string | Email sending provider type currently backing this domain (e.g. 'ses', 'cloudflare'). Assigned by MailKite, never chosen by the caller. |
| domain.email_provider_id | string · null | Registry id of the concrete provider instance backing the domain. |
| domain.dns_tokens | string · null | Provider-specific DNS tokens as a JSON string (e.g. SES DKIM CNAME targets). Feeds the dns record set; null until providers report them. |
| domain.last_checked_at | integer · null | When domain verification last ran, in Unix epoch milliseconds (UTC); null when it has never happened. |
| domain.created_at req | integer | When the domain was added, in Unix epoch milliseconds (UTC). |
| domain.webhookUrl | string · null | URL of the domain's catch-all webhook, or null when none is set. List/detail responses only. |
| domain.webhookAckMode | string · null | The webhook's acknowledgement mode: 'lenient' (any 2xx) or 'ack' (2xx + explicit ok). List/detail responses only. |
| domain.webhookStatus | object · null | Health of the webhook's most recent delivery, or null when it has never been exercised. List/detail responses only. |
| domain.webhookStatus.status | string · null | 'ok', 'fail', 'running', or 'timeout'. |
| domain.webhookStatus.message | string · null | Human-readable detail of the last outcome (e.g. 'HTTP 500'). |
| domain.webhookStatus.code | integer · null | HTTP status of the last delivery, when relevant. |
| domain.webhookStatus.at | integer · null | When the last outcome landed, in Unix epoch milliseconds (UTC); null when it has never happened. |
| domain.webhookStatus.consecutiveFailures | integer | Current failing streak (0 when healthy). |
| domain.webhookStatus.firstFailedAt | integer · null | When the current failing streak began, in Unix epoch milliseconds (UTC); null when it has never happened. |
| domain.webhookStatus.unhealthy | boolean | True once the failing streak passes the unhealthy threshold. |
| domain.dailyLimitReached | boolean | True when the owning account has hit its daily send cap. List/detail responses only. |
| dns | dns-record[] | The mail DNS record set. Real registrations only. |
| dns[].type req | "MX" · "TXT" · "CNAME" · "A" | Record type. |
| dns[].name req | string | Full hostname (FQDN) the record lives at. |
| dns[].value req | string | Bare RDATA value. |
| dns[].priority | integer | MX priority — present on MX records only. |
| dnsProvisioned | boolean | True when the records were published automatically on the registrar's DNS. |
| charge | object | What was charged. Real registrations only. |
| charge.amount req | integer | Amount in minor units (cents). |
| charge.currency req | string | Lowercase ISO 4217 code. |
| charge.status req | "captured" · "capture_failed" | 'capture_failed' means the domain is registered and yours, but the payment capture needs follow-up. |
An actual response, recorded from the conformance suite.
{
"domain": {
"id": "dom_2VbXqTpN8rKw",
"user_id": "usr_7Fj3MnQw",
"domain": "myapp.ai",
"status": "verified",
"mx_verified": 1,
"spf_verified": 1,
"dkim_verified": 1,
"dmarc_verified": 1,
"zero_retention": 0,
"track_opens": 0,
"enc_public_key": null,
"enc_key_alg": null,
"enc_fingerprint": null,
"enc_format": null,
"provider": "cloudflare",
"email_provider_id": null,
"dns_tokens": null,
"created_at": 1769731200000,
"last_checked_at": 1769817600000
},
"dns": [
{
"type": "MX",
"name": "myapp.ai",
"value": "mx.mailkite.dev",
"priority": 10
},
{
"type": "TXT",
"name": "myapp.ai",
"value": "v=spf1 include:spf.mailkite.dev ~all"
},
{
"type": "TXT",
"name": "_dmarc.myapp.ai",
"value": "v=DMARC1; p=none; rua=mailto:dmarc@myapp.ai"
},
{
"type": "CNAME",
"name": "abc123._domainkey.myapp.ai",
"value": "abc123.dkim.amazonses.com"
}
],
"registration": {
"status": "registered",
"reference": "ord_81f4c2"
},
"dnsProvisioned": true,
"charge": {
"amount": 1299,
"currency": "usd",
"status": "captured"
}
} {
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://api.mailkite.dev/v1/schemas/register-domain-response.json",
"title": "Register domain response",
"description": "POST /api/domains/register — a dry run returns just { registration, dryRun }; a real registration returns the new domain, its DNS set (auto-provisioned on the registrar when we manage its DNS), the registration reference, and the charge.",
"type": "object",
"required": [
"registration"
],
"properties": {
"registration": {
"type": "object",
"required": [
"status"
],
"description": "The registrar's view of the registration.",
"properties": {
"status": {
"type": "string",
"description": "Registrar status (e.g. 'registered', 'pending').",
"examples": [
"registered"
]
},
"reference": {
"type": [
"string",
"null"
],
"description": "Registrar order/operation reference, when provided."
}
}
},
"dryRun": {
"type": "boolean",
"const": true,
"description": "Present (true) only on dry-run responses — nothing was charged or created."
},
"domain": {
"$ref": "domain.json",
"description": "The new domain row. Real registrations only."
},
"dns": {
"type": "array",
"description": "The mail DNS record set. Real registrations only.",
"items": {
"$ref": "dns-record.json"
}
},
"dnsProvisioned": {
"type": "boolean",
"description": "True when the records were published automatically on the registrar's DNS."
},
"charge": {
"type": "object",
"required": [
"amount",
"currency",
"status"
],
"description": "What was charged. Real registrations only.",
"properties": {
"amount": {
"type": "integer",
"description": "Amount in minor units (cents).",
"examples": [
1299
]
},
"currency": {
"type": "string",
"description": "Lowercase ISO 4217 code.",
"examples": [
"usd"
]
},
"status": {
"type": "string",
"enum": [
"captured",
"capture_failed"
],
"description": "'capture_failed' means the domain is registered and yours, but the payment capture needs follow-up."
}
}
}
}
} PUT /api/domains/:id/retention Toggle zero-retention passthrough for the domain ({ zeroRetention }). With it on, message bodies are relayed to your webhook and never stored.
Toggle zero-retention passthrough for the domain ({ zeroRetention }). With it on, message bodies are relayed to your webhook and never stored.
Request body
set-retention-request.json| Field | Type | Notes |
|---|---|---|
| zeroRetention req | boolean | true turns zero-retention passthrough ON: inbound mail for the domain is delivered to your webhook and never stored — no replay, no dashboard history, your endpoint holds the only copy. false restores normal retention. |
The smallest body that makes this call — every other field is optional.
{
"zeroRetention": true
} {
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://api.mailkite.dev/v1/schemas/set-retention-request.json",
"title": "Set retention request body",
"type": "object",
"required": [
"zeroRetention"
],
"additionalProperties": false,
"properties": {
"zeroRetention": {
"type": "boolean",
"description": "true turns zero-retention passthrough ON: inbound mail for the domain is delivered to your webhook and never stored — no replay, no dashboard history, your endpoint holds the only copy. false restores normal retention.",
"examples": [
true
]
}
}
} Response body
domain-response.jsonThe uniform write-response for domain settings (retention, encryption): the updated domain row.
| Field | Type | Notes |
|---|---|---|
| domain req | domain | The updated domain row (without webhook enrichment). |
| domain.id req | string | Domain id (dom_…). |
| domain.user_id req | string | Owning account (usr_…). |
| domain.domain req | string | The domain name, lowercased. |
| domain.status req | "pending" · "verified" · "failed" | Overall verification state. Inbound routing requires at least MX verification; `verified` follows the MX check. |
| domain.mx_verified req | 0 · 1 | 1 once the MX record resolves to MailKite — the gate for inbound mail, webhooks, and routes. |
| domain.spf_verified req | 0 · 1 | 1 once the SPF TXT record is in place. |
| domain.dkim_verified req | 0 · 1 | 1 once the DKIM records are accepted by the sending provider. |
| domain.dmarc_verified req | 0 · 1 | 1 once a DMARC policy is published. |
| domain.zero_retention | 0 · 1 | 1 when zero-retention passthrough is on: inbound mail is delivered to the webhook and never stored (no replay, no dashboard history). |
| domain.track_opens | 0 · 1 | Domain-wide default for outbound open tracking (a per-send trackOpens overrides it). |
| domain.enc_public_key | string · null | At-rest encryption public key (PEM SPKI, armored OpenPGP key, or X.509 cert). null = encryption off. |
| domain.enc_key_alg | string · null | Key algorithm recorded for the uploaded key ('RSA-OAEP-256', 'PGP', or 'S/MIME'). null = encryption off. |
| domain.enc_fingerprint | string · null | Fingerprint of the uploaded key (sha256 of the SPKI/cert DER, or the OpenPGP fingerprint). |
| domain.enc_format | string · null | Which client-decryptable standard the key is: 'pgp' or 'smime'. null = off or legacy custom envelope. |
| domain.provider req | string | Email sending provider type currently backing this domain (e.g. 'ses', 'cloudflare'). Assigned by MailKite, never chosen by the caller. |
| domain.email_provider_id | string · null | Registry id of the concrete provider instance backing the domain. |
| domain.dns_tokens | string · null | Provider-specific DNS tokens as a JSON string (e.g. SES DKIM CNAME targets). Feeds the dns record set; null until providers report them. |
| domain.last_checked_at | integer · null | When domain verification last ran, in Unix epoch milliseconds (UTC); null when it has never happened. |
| domain.created_at req | integer | When the domain was added, in Unix epoch milliseconds (UTC). |
| domain.webhookUrl | string · null | URL of the domain's catch-all webhook, or null when none is set. List/detail responses only. |
| domain.webhookAckMode | string · null | The webhook's acknowledgement mode: 'lenient' (any 2xx) or 'ack' (2xx + explicit ok). List/detail responses only. |
| domain.webhookStatus | object · null | Health of the webhook's most recent delivery, or null when it has never been exercised. List/detail responses only. |
| domain.webhookStatus.status | string · null | 'ok', 'fail', 'running', or 'timeout'. |
| domain.webhookStatus.message | string · null | Human-readable detail of the last outcome (e.g. 'HTTP 500'). |
| domain.webhookStatus.code | integer · null | HTTP status of the last delivery, when relevant. |
| domain.webhookStatus.at | integer · null | When the last outcome landed, in Unix epoch milliseconds (UTC); null when it has never happened. |
| domain.webhookStatus.consecutiveFailures | integer | Current failing streak (0 when healthy). |
| domain.webhookStatus.firstFailedAt | integer · null | When the current failing streak began, in Unix epoch milliseconds (UTC); null when it has never happened. |
| domain.webhookStatus.unhealthy | boolean | True once the failing streak passes the unhealthy threshold. |
| domain.dailyLimitReached | boolean | True when the owning account has hit its daily send cap. List/detail responses only. |
An actual response, recorded from the conformance suite.
{
"domain": {
"id": "dom_2VbXqTpN8rKw",
"user_id": "usr_7Fj3MnQw",
"domain": "myapp.ai",
"status": "verified",
"mx_verified": 1,
"spf_verified": 1,
"dkim_verified": 1,
"dmarc_verified": 1,
"zero_retention": 1,
"track_opens": 0,
"enc_public_key": null,
"enc_key_alg": null,
"enc_fingerprint": null,
"enc_format": null,
"provider": "ses",
"email_provider_id": "prov_ses_use2",
"dns_tokens": null,
"created_at": 1769731200000,
"last_checked_at": 1769817600000
}
} {
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://api.mailkite.dev/v1/schemas/domain-response.json",
"title": "Domain envelope",
"description": "The uniform write-response for domain settings (retention, encryption): the updated domain row.",
"type": "object",
"required": [
"domain"
],
"properties": {
"domain": {
"$ref": "domain.json",
"description": "The updated domain row (without webhook enrichment)."
}
}
} PUT /api/domains/:id/encryption Enable at-rest encryption for the domain with a public key ({ publicKey }). Stored bodies are encrypted to it; only the holder of the private key can read them back.
Enable at-rest encryption for the domain with a public key ({ publicKey }). Stored bodies are encrypted to it; only the holder of the private key can read them back.
Request body
set-encryption-request.json| Field | Type | Notes |
|---|---|---|
| publicKey req | string | The public key stored bodies will be encrypted to: a PEM (SPKI) RSA key, an armored OpenPGP public key, or an X.509 certificate (S/MIME). MailKite keeps only the public key and its fingerprint — never a private key. |
The smallest body that makes this call — every other field is optional.
{
"publicKey": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0K1w0Qy4\n-----END PUBLIC KEY-----"
} {
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://api.mailkite.dev/v1/schemas/set-encryption-request.json",
"title": "Set encryption request body",
"type": "object",
"required": [
"publicKey"
],
"additionalProperties": false,
"properties": {
"publicKey": {
"type": "string",
"description": "The public key stored bodies will be encrypted to: a PEM (SPKI) RSA key, an armored OpenPGP public key, or an X.509 certificate (S/MIME). MailKite keeps only the public key and its fingerprint — never a private key.",
"examples": [
"-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A…\n-----END PUBLIC KEY-----"
]
}
}
} Response body
domain-response.jsonThe uniform write-response for domain settings (retention, encryption): the updated domain row.
| Field | Type | Notes |
|---|---|---|
| domain req | domain | The updated domain row (without webhook enrichment). |
| domain.id req | string | Domain id (dom_…). |
| domain.user_id req | string | Owning account (usr_…). |
| domain.domain req | string | The domain name, lowercased. |
| domain.status req | "pending" · "verified" · "failed" | Overall verification state. Inbound routing requires at least MX verification; `verified` follows the MX check. |
| domain.mx_verified req | 0 · 1 | 1 once the MX record resolves to MailKite — the gate for inbound mail, webhooks, and routes. |
| domain.spf_verified req | 0 · 1 | 1 once the SPF TXT record is in place. |
| domain.dkim_verified req | 0 · 1 | 1 once the DKIM records are accepted by the sending provider. |
| domain.dmarc_verified req | 0 · 1 | 1 once a DMARC policy is published. |
| domain.zero_retention | 0 · 1 | 1 when zero-retention passthrough is on: inbound mail is delivered to the webhook and never stored (no replay, no dashboard history). |
| domain.track_opens | 0 · 1 | Domain-wide default for outbound open tracking (a per-send trackOpens overrides it). |
| domain.enc_public_key | string · null | At-rest encryption public key (PEM SPKI, armored OpenPGP key, or X.509 cert). null = encryption off. |
| domain.enc_key_alg | string · null | Key algorithm recorded for the uploaded key ('RSA-OAEP-256', 'PGP', or 'S/MIME'). null = encryption off. |
| domain.enc_fingerprint | string · null | Fingerprint of the uploaded key (sha256 of the SPKI/cert DER, or the OpenPGP fingerprint). |
| domain.enc_format | string · null | Which client-decryptable standard the key is: 'pgp' or 'smime'. null = off or legacy custom envelope. |
| domain.provider req | string | Email sending provider type currently backing this domain (e.g. 'ses', 'cloudflare'). Assigned by MailKite, never chosen by the caller. |
| domain.email_provider_id | string · null | Registry id of the concrete provider instance backing the domain. |
| domain.dns_tokens | string · null | Provider-specific DNS tokens as a JSON string (e.g. SES DKIM CNAME targets). Feeds the dns record set; null until providers report them. |
| domain.last_checked_at | integer · null | When domain verification last ran, in Unix epoch milliseconds (UTC); null when it has never happened. |
| domain.created_at req | integer | When the domain was added, in Unix epoch milliseconds (UTC). |
| domain.webhookUrl | string · null | URL of the domain's catch-all webhook, or null when none is set. List/detail responses only. |
| domain.webhookAckMode | string · null | The webhook's acknowledgement mode: 'lenient' (any 2xx) or 'ack' (2xx + explicit ok). List/detail responses only. |
| domain.webhookStatus | object · null | Health of the webhook's most recent delivery, or null when it has never been exercised. List/detail responses only. |
| domain.webhookStatus.status | string · null | 'ok', 'fail', 'running', or 'timeout'. |
| domain.webhookStatus.message | string · null | Human-readable detail of the last outcome (e.g. 'HTTP 500'). |
| domain.webhookStatus.code | integer · null | HTTP status of the last delivery, when relevant. |
| domain.webhookStatus.at | integer · null | When the last outcome landed, in Unix epoch milliseconds (UTC); null when it has never happened. |
| domain.webhookStatus.consecutiveFailures | integer | Current failing streak (0 when healthy). |
| domain.webhookStatus.firstFailedAt | integer · null | When the current failing streak began, in Unix epoch milliseconds (UTC); null when it has never happened. |
| domain.webhookStatus.unhealthy | boolean | True once the failing streak passes the unhealthy threshold. |
| domain.dailyLimitReached | boolean | True when the owning account has hit its daily send cap. List/detail responses only. |
An actual response, recorded from the conformance suite.
{
"domain": {
"id": "dom_2VbXqTpN8rKw",
"user_id": "usr_7Fj3MnQw",
"domain": "myapp.ai",
"status": "verified",
"mx_verified": 1,
"spf_verified": 1,
"dkim_verified": 1,
"dmarc_verified": 1,
"zero_retention": 0,
"track_opens": 0,
"enc_public_key": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0K1w0Qy4\n-----END PUBLIC KEY-----",
"enc_key_alg": "RSA-OAEP-256",
"enc_fingerprint": "9f2c8a41d6e37b05",
"enc_format": null,
"provider": "ses",
"email_provider_id": "prov_ses_use2",
"dns_tokens": null,
"created_at": 1769731200000,
"last_checked_at": 1769817600000
}
} {
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://api.mailkite.dev/v1/schemas/domain-response.json",
"title": "Domain envelope",
"description": "The uniform write-response for domain settings (retention, encryption): the updated domain row.",
"type": "object",
"required": [
"domain"
],
"properties": {
"domain": {
"$ref": "domain.json",
"description": "The updated domain row (without webhook enrichment)."
}
}
} DELETE /api/domains/:id/encryption Disable at-rest encryption for the domain. Messages stored while it was on stay encrypted.
Disable at-rest encryption for the domain. Messages stored while it was on stay encrypted.
Response body
domain-response.jsonThe uniform write-response for domain settings (retention, encryption): the updated domain row.
| Field | Type | Notes |
|---|---|---|
| domain req | domain | The updated domain row (without webhook enrichment). |
| domain.id req | string | Domain id (dom_…). |
| domain.user_id req | string | Owning account (usr_…). |
| domain.domain req | string | The domain name, lowercased. |
| domain.status req | "pending" · "verified" · "failed" | Overall verification state. Inbound routing requires at least MX verification; `verified` follows the MX check. |
| domain.mx_verified req | 0 · 1 | 1 once the MX record resolves to MailKite — the gate for inbound mail, webhooks, and routes. |
| domain.spf_verified req | 0 · 1 | 1 once the SPF TXT record is in place. |
| domain.dkim_verified req | 0 · 1 | 1 once the DKIM records are accepted by the sending provider. |
| domain.dmarc_verified req | 0 · 1 | 1 once a DMARC policy is published. |
| domain.zero_retention | 0 · 1 | 1 when zero-retention passthrough is on: inbound mail is delivered to the webhook and never stored (no replay, no dashboard history). |
| domain.track_opens | 0 · 1 | Domain-wide default for outbound open tracking (a per-send trackOpens overrides it). |
| domain.enc_public_key | string · null | At-rest encryption public key (PEM SPKI, armored OpenPGP key, or X.509 cert). null = encryption off. |
| domain.enc_key_alg | string · null | Key algorithm recorded for the uploaded key ('RSA-OAEP-256', 'PGP', or 'S/MIME'). null = encryption off. |
| domain.enc_fingerprint | string · null | Fingerprint of the uploaded key (sha256 of the SPKI/cert DER, or the OpenPGP fingerprint). |
| domain.enc_format | string · null | Which client-decryptable standard the key is: 'pgp' or 'smime'. null = off or legacy custom envelope. |
| domain.provider req | string | Email sending provider type currently backing this domain (e.g. 'ses', 'cloudflare'). Assigned by MailKite, never chosen by the caller. |
| domain.email_provider_id | string · null | Registry id of the concrete provider instance backing the domain. |
| domain.dns_tokens | string · null | Provider-specific DNS tokens as a JSON string (e.g. SES DKIM CNAME targets). Feeds the dns record set; null until providers report them. |
| domain.last_checked_at | integer · null | When domain verification last ran, in Unix epoch milliseconds (UTC); null when it has never happened. |
| domain.created_at req | integer | When the domain was added, in Unix epoch milliseconds (UTC). |
| domain.webhookUrl | string · null | URL of the domain's catch-all webhook, or null when none is set. List/detail responses only. |
| domain.webhookAckMode | string · null | The webhook's acknowledgement mode: 'lenient' (any 2xx) or 'ack' (2xx + explicit ok). List/detail responses only. |
| domain.webhookStatus | object · null | Health of the webhook's most recent delivery, or null when it has never been exercised. List/detail responses only. |
| domain.webhookStatus.status | string · null | 'ok', 'fail', 'running', or 'timeout'. |
| domain.webhookStatus.message | string · null | Human-readable detail of the last outcome (e.g. 'HTTP 500'). |
| domain.webhookStatus.code | integer · null | HTTP status of the last delivery, when relevant. |
| domain.webhookStatus.at | integer · null | When the last outcome landed, in Unix epoch milliseconds (UTC); null when it has never happened. |
| domain.webhookStatus.consecutiveFailures | integer | Current failing streak (0 when healthy). |
| domain.webhookStatus.firstFailedAt | integer · null | When the current failing streak began, in Unix epoch milliseconds (UTC); null when it has never happened. |
| domain.webhookStatus.unhealthy | boolean | True once the failing streak passes the unhealthy threshold. |
| domain.dailyLimitReached | boolean | True when the owning account has hit its daily send cap. List/detail responses only. |
An actual response, recorded from the conformance suite.
{
"domain": {
"id": "dom_2VbXqTpN8rKw",
"user_id": "usr_7Fj3MnQw",
"domain": "myapp.ai",
"status": "verified",
"mx_verified": 1,
"spf_verified": 1,
"dkim_verified": 1,
"dmarc_verified": 1,
"zero_retention": 0,
"track_opens": 0,
"enc_public_key": null,
"enc_key_alg": null,
"enc_fingerprint": null,
"enc_format": null,
"provider": "ses",
"email_provider_id": "prov_ses_use2",
"dns_tokens": null,
"created_at": 1769731200000,
"last_checked_at": 1769817600000
}
} {
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://api.mailkite.dev/v1/schemas/domain-response.json",
"title": "Domain envelope",
"description": "The uniform write-response for domain settings (retention, encryption): the updated domain row.",
"type": "object",
"required": [
"domain"
],
"properties": {
"domain": {
"$ref": "domain.json",
"description": "The updated domain row (without webhook enrichment)."
}
}
}