Email DNS

Plan the MX, SPF, DKIM and DMARC records for your email providers and check the mail records of a zone.

On this page

The email assistant computes the records a zone needs for the chosen mailbox and sending providers (one merged SPF, MX, DKIM, DMARC) and checks what the public DNS shows. Nothing is applied by these endpoints; apply a plan with Batch records.

OperationEndpoint
List email providersGET /v1/orgs/{org}/zones/{zone}/email/providers
Plan email recordsPOST /v1/orgs/{org}/zones/{zone}/email/plan
Check email recordsGET /v1/orgs/{org}/zones/{zone}/email/check

List email providers

GET /v1/orgs/{org}/zones/{zone}/email/providers

Lists the provider templates of the email assistant, mailbox (mailbox, e.g. Google Workspace, Microsoft 365, Zoho Mail, Proton Mail, Fastmail), sending (sending, e.g. Amazon SES, Resend, Mailgun, SendGrid, Brevo) and other, with the parameters each one asks for (defaults already computed for the zone).

Required role: readonly or higher.

Parameters

NameInTypeRequiredDescription
orgpathstring (uuid)YesOrganization id. Every route checks that it is the token's organization (otherwise 404).
zonepathstring (uuid)YesZone id (see List zones).

Response

FieldTypeDescription
items[].idstringId.
items[].namestringName.
items[].kindstringOne of mailbox, sending, other.
items[].docsstring
items[].spfarray of stringMechanisms it adds to the domain's SPF record.
items[].paramsarray of object
items[].params[].keystring
items[].params[].kindstringOne of text, dkim, cname, select, list.
items[].params[].requiredboolean
items[].params[].defaultstring
items[].params[].selectorstringDKIM selector where the value goes (kind dkim/cname).
items[].params[].optionsarray of string
items[].params[].placeholderstring

Errors

StatusDescription
404Not found, or not visible to the token (not_found).

Example

curl "https://dash.openwaf.dev/v1/orgs/$OPENWAF_ORG/zones/6f1d2c3b-8a4e-4b7d-9c21-5e3f7a8b9c0d/email/providers" \
  -H "Authorization: Bearer $OPENWAF_TOKEN"
200
{
  "items": [
    {
      "id": "example",
      "name": "example.com",
      "kind": "mailbox",
      "docs": "example",
      "spf": [
        "example"
      ],
      "params": [
        {}
      ]
    }
  ]
}

Plan email records

POST /v1/orgs/{org}/zones/{zone}/email/plan

Computes the email records the zone needs for the chosen providers, compared with its current records (the public DNS for an external zone): one SPF record merging the providers' mechanisms with the existing ones (keeping the existing ~all/-all), the MX records of the mailbox provider, the DKIM keys pasted by the user (validated), verification records, sending subdomains and DMARC (p=none by default). It also counts the DNS lookups of the resulting SPF record (more than 10 is an error).

Nothing is applied. Problems are listed in issues (errors leave batch null). Send batch as is to Batch records, first with dry_run=1. In external zones applicable is false and the actions are manual.

Required role: readonly or higher.

Parameters

NameInTypeRequiredDescription
orgpathstring (uuid)YesOrganization id. Every route checks that it is the token's organization (otherwise 404).
zonepathstring (uuid)YesZone id (see List zones).

Request body

Content type application/json. Schema: EmailPlanInput.

FieldTypeRequiredDescription
providersarray of objectYes
providers[].idstringYesId.
providers[].paramsmap of stringNo
dmarcobject or nullNoNull keeps the existing DMARC record (or adds p=none if there is none).
dmarc.policystringNoOne of none, quarantine, reject.
dmarc.ruastringNoAddress for aggregate reports: absent keeps the one of the existing record, empty removes it.
spf_allstringNoOne of "", ~all, -all.

Response

FieldTypeDescription
zonestring
dns_modestringOne of delegated, external.
applicableboolean
recordsarray of object
records[].typestring
records[].namestringRelative to the zone (@ = the zone).
records[].fqdnstring
records[].contentstring
records[].priorityintegerMX and SRV priority (null for other types).
records[].purposestringOne of mx, spf, dkim, dmarc, verification, autoconfig, return_path, tracking.
records[].providerstring
records[].actionstringOne of create, update, keep, manual.
records[].record_idstring
records[].currentstring
deletearray of object
delete[].record_idstring
delete[].typestring
delete[].namestringName.
delete[].contentstring
delete[].priorityintegerMX and SRV priority (null for other types).
delete[].reasonstringReason of the change. One of replaced_mx, duplicate_spf, replaced_dmarc, replaced_record.
batchobject or nullBody for POST …/records/batch.
issuesarray of object
issues[].severitystringOne of error, warning.
issues[].codestring
issues[].fieldstring
issues[].messagestring
issues[].paramsmap of string
spfobject or null
spf.valuestring
spf.lookupsinteger-1 if they were not counted.
spf.errorstring
dmarcstring

Errors

StatusDescription
400Malformed request (bad_request).
404Not found, or not visible to the token (not_found).
502dns_unavailable: the public DNS did not answer (external zone).
503The check is not available right now (external zone), or busy: too many checks in progress.

Example

curl -X POST "https://dash.openwaf.dev/v1/orgs/$OPENWAF_ORG/zones/6f1d2c3b-8a4e-4b7d-9c21-5e3f7a8b9c0d/email/plan" \
  -H "Authorization: Bearer $OPENWAF_TOKEN" \
  -H "Content-Type: application/json" \
  --data '{
    "providers": [
      {
        "id": "google"
      }
    ],
    "dmarc": {
      "policy": "none"
    }
  }'
200
{
  "zone": "example.com",
  "dns_mode": "delegated",
  "applicable": false,
  "records": [
    {
      "type": "A",
      "name": "example.com",
      "fqdn": "example",
      "content": "203.0.113.10",
      "priority": 0,
      "purpose": "mx",
      "provider": "example",
      "action": "block",
      "record_id": "aa747ff3-b956-4f78-aa3e-6502aeccf60a",
      "current": "example"
    }
  ],
  "delete": [
    {
      "record_id": "aa747ff3-b956-4f78-aa3e-6502aeccf60a",
      "type": "A",
      "name": "example.com",
      "content": "203.0.113.10",
      "priority": 0,
      "reason": "Point www at the new origin"
    }
  ],
  "batch": {},
  "issues": [
    {
      "severity": "error",
      "code": "example",
      "field": "example",
      "message": "",
      "params": {
        "key": "example"
      }
    }
  ],
  "spf": {
    "value": "4f1c2e9a7b3d",
    "lookups": 0,
    "error": ""
  },
  "dmarc": "example"
}

Check email records

GET /v1/orgs/{org}/zones/{zone}/email/check

Checks the mail records of the zone on public resolvers, MX present and consistent, a single SPF record and its DNS lookups (at most 10 including nested includes and redirects), DKIM keys, DMARC, proxied mail names, CNAME conflicts and, in delegated zones, whether the world sees the zone's records. Each check is ok, warn, fail or info with a stable code and its params. The result is cached for 60 seconds.

Required role: readonly or higher. refresh=true needs tech.

Parameters

NameInTypeRequiredDescription
orgpathstring (uuid)YesOrganization id. Every route checks that it is the token's organization (otherwise 404).
zonepathstring (uuid)YesZone id (see List zones).
selectorsquerystringNoAdditional comma-separated DKIM selectors (at most 10).
refreshquerybooleanNoCheck again without using the cached result (tech role).

Response

FieldTypeDescription
zonestring
dns_modestringOne of delegated, external.
checked_atstring (date-time)Last check.
summaryobject
summary.statusstringOne of ok, warn, fail.
summary.okinteger
summary.warninteger
summary.failinteger
summary.infointeger
providersarray of string
checksarray of object
checks[].idstringId. One of mx, spf_record, spf_lookups, spf_policy, dkim, dmarc, proxied, conflicts, world.
checks[].statusstringOne of ok, warn, fail, info.
checks[].codestring
checks[].subjectstring
checks[].messagestring
checks[].paramsmap of string
publicobject
public.mxarray of string
public.spfarray of string
public.dmarcarray of string
public.dkimarray of object
spfobject or null
spf.lookupsinteger
spf.void_lookupsinteger
spf.truncatedbooleanThe count stopped at 30 lookups.
spf.allstring
spf.treearray of object
spf.problemsarray of object

Errors

StatusDescription
403refresh=1 needs the tech role.
404Not found, or not visible to the token (not_found).
503The check is not available right now, or busy: too many checks in progress.

Example

curl "https://dash.openwaf.dev/v1/orgs/$OPENWAF_ORG/zones/6f1d2c3b-8a4e-4b7d-9c21-5e3f7a8b9c0d/email/check" \
  -H "Authorization: Bearer $OPENWAF_TOKEN"
200
{
  "zone": "example.com",
  "dns_mode": "delegated",
  "checked_at": "2026-10-27T09:30:00Z",
  "summary": {
    "status": "ok",
    "ok": 0,
    "warn": 0,
    "fail": 0,
    "info": 0
  },
  "providers": [
    "example"
  ],
  "checks": [
    {
      "id": "mx",
      "status": "ok",
      "code": "example",
      "subject": "example",
      "message": "",
      "params": {
        "key": "example"
      }
    }
  ],
  "public": {
    "mx": [
      "example"
    ],
    "spf": [
      "example"
    ],
    "dmarc": [
      "example"
    ],
    "dkim": [
      {
        "selector": "google",
        "name": "example.com",
        "value": "4f1c2e9a7b3d",
        "cname": "example"
      }
    ]
  },
  "spf": {
    "lookups": 0,
    "void_lookups": 0,
    "truncated": false,
    "all": "example",
    "tree": [
      {}
    ],
    "problems": []
  }
}

Last updated:

↑↓ to navigate↵ to openesc to close