Deliverability

Check a domain's email authentication

Ten credits for a fresh check; a check of the same domain within 24 hours is served from cache and costs nothing.

Send Idempotency-Key. The DNS work can run for up to 120 seconds while the API stops waiting at about 29, so a client that times out cannot tell whether it paid. With a key, the identical request replays the original result for 30 days and charges nothing; without one, the retry is a new 10-credit check. A key already in flight answers 409, and a receipt that cannot be read answers 503. In both cases retry the identical request rather than starting a fresh one.

A lookup that fails is not a missing record. Only NXDOMAIN or NODATA counts as "no record". An SPF, DKIM, DMARC, MX or PTR lookup that fails (SERVFAIL, REFUSED, a timeout) is named in not_measured, shown as not_measured in authentication, and left out of score rather than scored as missing.

No grade without SPF, DKIM and DMARC. When any of those three is not measured, grade and spam_assessment.spam_likelihood are null and score is a partial figure over what did answer: a letter over the rest would grade records nobody read. A check where only MX or reverse DNS failed is still graded, over the rest. Either way the check is charged, because it measured something; it is refunded (503) only when every lookup timed out or nothing at all could be scored.

post/v1/deliverability/check

Request

  • Base URL: https://api.kawaa.com
  • URL: https://api.kawaa.com/v1/deliverability/check
  • Auth: one of:
    • API key in header X-Api-Key
    • HTTP bearer
    • OAuth 2 (authorization code, authorization URL https://api.kawaa.com/oauth/authorize, token URL https://api.kawaa.com/oauth/token, scopes: deliverability:write)

Headers

Idempotency-Keystring

Makes a charging call safe to retry: a repeat under the same key replays the original outcome instead of buying the work again. The window depends on the route: the receipt behind it expires, and after that the same key is a new paid request. It is 30 days for a deliverability check, at least 30 days for a single find, 24 hours for an activity lookup, and 15 minutes for a bulk find. What a replay returns also differs: a batch replays its JOB, an activity lookup replays its RESULT. A different request under the same key is 409 IDEMPOTENCY_KEY_CONFLICT, before any charge.

How long a key stays safe differs per route, and for the verification routes it depends on the plan. POST /v1/verify, /v1/verify/batch and /v1/verify/file keep their receipts for the job's data-retention period, never less than 30 days, so longer on plans that keep results longer. The job itself is kept only for the plan's data-retention period, which is 7 days on Free: a repeat that arrives after the job's retention period has ended but while its receipt is still live is refused with 410 IDEMPOTENT_JOB_EXPIRED and charged nothing, rather than bought a second time; send a new key to run it again. Once the receipt expires the same key claims a fresh target and creates another paid job. POST /v1/deliverability/check keeps its receipt 30 days. POST /v1/find keeps its receipt at least 30 days and its stored address only for the plan's retention period; a repeat after the address expires but before the receipt does return 410 IDEMPOTENT_JOB_EXPIRED without charging. The activity lookups keep receipts about 24 hours, and POST /v1/find/bulk 15 minutes. Without a key, GET /v1/activity/{email} and GET /v1/activity/domain/{domain} still answer an identical repeat from the same account free for 15 minutes after the lookup that was charged (POST /v1/find/bulk does the same for its content); a key buys a fresh read inside that window. An unkeyed POST /v1/verify that arrives while an identical one from the same account is still being verified (at most 15 minutes after its charge) joins it and is not charged; once that verification has its answer, a repeat is charged as usual. Only a key makes a later retry free.

Retry with the key you have rather than minting a new one. A key is not a refund (it prevents a second charge, it does not undo the first), but it stays good far longer than the shortest advertised retention suggests, and dropping one early is how a retry becomes a second paid job.

The characters are checked after trimming. Surrounding whitespace is trimmed from the header first; the resulting key must be 1-255 printable ASCII characters (0x21-0x7e) other than a comma, with no internal spaces or anything outside that range. Leading and trailing whitespace is accepted and is not part of the key. Because OpenAPI constraints apply to the raw header rather than the post-trim value, this schema deliberately has no minLength, maxLength, or pattern; those constraints would make generated clients reject values the API accepts. Invalid post-trim values get 400 before the route starts. A UUID is the obvious choice. Sending the header twice with different raw values is also a 400. A key may not contain a comma, because a header sent more than once reaches the API joined by commas; a comma is refused with the same 400, before anything is charged. A receipt claimed under a comma-bearing key before 2026-10-03 still replays for its window.

Request body

domainstring required

A bare registrable domain name. Normalized before it is validated, which is why no format: hostname or pattern is declared here: the API trims, lower-cases, punycodes an internationalized domain and then strips ONE leading www., in that order, because IDNA maps the Unicode label separators (。, ., 。) to ., so www。münchen.de only grows its ASCII dot during the fold. Only then is the format checked. So " WWW.Example.COM " and münchen.de are both accepted; the latter is checked as xn--mnchen-3ya.de, the same fold /v1/verify applies to the domain half of an address. A scheme, a path or a port is NOT stripped and is a 400 Invalid domain format; send the host on its own. At least two labels are required, each label 1–63 characters, and the TLD either letters only and at least two (com, de) or a decodable xn-- A-label of an internationalized one (пример.рф is checked as xn--e1afmkfd.xn--p1ai). The A-label must decode AND be the canonical spelling of what it decodes to, not merely carry the prefix: example.xn--abc decodes to nothing, and example.xn---7a decodes to ¡ whose one canonical form is xn--7a. Neither can name a TLD any root zone holds, so both are refused rather than charged for. A special-use name the public DNS never answers for, such as .example, .test, .invalid, .localhost, .local, .onion, .alt, .internal, .corp, .home, .mail, or home.arpa and the other special-use .arpa names, is also a 400 before any charge: every lookup could only fail.

sending_ipstring

The IPv4 address mail is actually sent from, when that is not the domain's own mail host. Leading and trailing whitespace is trimmed before the IPv4 rule is applied, so the raw request string deliberately has no format or pattern constraint. IPv4 only: after trimming, an IPv6 literal or a hostname is a 400 before the check runs, not a fallback. Only the blocklist lookup falls back to an MX-derived address. Reverse DNS is looked up for the normalized sending_ip alone and reports status: "skipped" when there is none, so without one reverse DNS is not measured at all and server.reverse_dns is null. It must be a public address: the one receiving mail servers actually see. Private (10/8, 172.16/12, 192.168/16), loopback (127/8), link-local (169.254/16), shared/CGNAT (100.64/10), documentation (192.0.2/24, 198.51.100/24, 203.0.113/24), 0/8, benchmarking (198.18/15), the deprecated 6to4 relay anycast block (192.88.99/24), multicast (224/4) and reserved (240/4) addresses are a 400 before the cache lookup or any charge: no public reverse-DNS or blocklist zone answers for them, and the resolver this check runs behind answers reverse DNS for private space itself, which once graded a private network name as a PTR pass. Documentation ranges included: an address copied from an example is refused, not checked.

refreshboolean

Ignore the 24-hour cache and measure again. Costs the full 10 credits, like any other fresh check. Without it a customer who has just fixed their SPF or DMARC record gets the identical old grade back and reasonably concludes the fix did not work. There is no other way to ask for current evidence.

Example request

{
  "domain": "example.com"
}

Response

The check. cached and idempotent_replay say whether it was measured now or reused; credits_used is what was actually charged.

successunknown required

Example response

{
  "data": {
    "checks": {
      "dmarc": {
        "policy": "none"
      }
    },
    "issues": [
      {
        "check": "spf"
      }
    ],
    "not_measured": {
      "dkim": "DKIM lookup failed for 51 of 51 selectors (ESERVFAIL), so DKIM was not measured and is left out of the score."
    }
  }
}

Changes

No recorded changes to this operation across all 1 revision of this API.