---
title: "One past deliverability check"
method: GET
path: "/v1/deliverability/checks/{id}"
tags: ["Deliverability"]
---

# One past deliverability check

`GET /v1/deliverability/checks/{id}`

## Request

- Base URL: `https://api.kawaa.com`
- URL: `https://api.kawaa.com/v1/deliverability/checks/{id}`
- 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:read`)

A check that was already run and paid for. Free: this is a read of stored evidence, not a new check.

## Path parameters

- `id` string, required

## Response `200`

The stored check.

- object
  - `success` unknown, required
  - `data` DeliverabilityCheck, required
    - `domain` string, required
    - `sendingIp` string, nullable: The `sending_ip` this check ran against, or null when none was supplied. Part of the raw scoring result, published alongside the snake_case projection in its original camelCase spelling; kept because existing integrations read it.
    - `score` integer, required: 0-100, over the mechanisms that were measured. One whose lookup failed is left out rather than scored as missing, so read `not_measured` before treating this as the whole picture. When `grade` is null, SPF, DKIM or DMARC was not measured and this is a partial score over what did answer: report it as partial, not as the domain's result.
    - `grade` string, nullable, required: `score` as a letter: A (90-100), B (80-89), C (70-79), D (60-69) or F. **null when SPF, DKIM or DMARC could not be looked up** (named in `not_measured`): a letter over the rest would grade records nobody read, and it used to, so a domain whose DKIM and DMARC lookups both failed could read A. A stored check from before this rule that carries such a letter is answered with null too, from its own `not_measured`. A failed MX or reverse-DNS lookup still leaves a grade, over the rest; see `not_measured`.
    - `checkedAt` string, date-time: When the check ran, as recorded in the raw scoring result. Prefer `checked_at`: on a cached or replayed result the stored row's `checked_at` is authoritative, and an older stored result may carry no `checkedAt` at all.
    - `durationMs` integer: How long the DNS work took, in milliseconds, for the run that produced this result. A cached or replayed answer repeats the original run's figure; it is not the latency of this request.
    - `checks` object: The raw per-mechanism scoring detail. One entry per mechanism REPORTED, and they do NOT share a shape: `dkim` carries the selectors it found, `mx` the records, `blacklists` the zones and the listings, and `mx` and `ptr` carry no `issues` array at all. Read the named property, not a common map value. **Optional, and absent on an older stored result.** A stored check without it is served as it is, with nothing made up in its place, so a check stored before the current shape answers 200 with the projected fields and no `checks`. Read `authentication`, `dns`, `reputation` and `server`, which the projection always produces, and treat this as the raw detail when it happens to be there. **BIMI and MTA-STS are checked, and they are unscored.** Both initialise `maxScore: 0`, and the score counts a mechanism only when `maxScore > 0`, so neither can move `score` or `grade` in either direction. They also have no entry in this object. A missing MTA-STS raises nothing at all; a missing BIMI produces a recommendation only when DMARC is already `reject`.
      - `spf` object: SPF: whether a record exists, parses, and is not over the lookup limit.
        - `status` 'pass' | 'weak' | 'missing' | 'invalid' | 'error' | 'found' | 'listed' | 'skipped', required: `missing` is an answer: the name has no such record (NXDOMAIN or NODATA). `error` means the lookup itself failed (SERVFAIL, REFUSED, a timeout), so nothing is known about the record: the mechanism is not measured, has `maxScore` 0, is named in `not_measured` and is left out of the score; when it is SPF, DKIM or DMARC the check has no grade. `found`, `listed` and `skipped` are used by some mechanisms only.
        - `score` number, required: What this mechanism contributed to the total, in raw points out of `maxScore`. The projected sections restate the same measurement as a percentage: `authentication.<mechanism>.score`, `dns.mx_score` and `reputation.score` are `round(score / maxScore × 100)`, so `checks.dkim` at 11 of 15 appears as `authentication.dkim.score: 73`. Two scales, one measurement.
        - `maxScore` number, required: The most it could have contributed. A mechanism whose lookup failed (status `error`, named in `not_measured`) has `maxScore` 0 and is left out of the grade rather than scored as missing, the way an unanswered blocklist section is. For SPF, DKIM or DMARC there is then no grade at all (`grade: null`). Until 2026-10-01 a failed SPF, DKIM, DMARC, MX or PTR lookup scored 0 against its full weight, so a DKIM SERVFAIL took a perfect domain from 100 (A) to 81 (B).
        - `record` string, nullable: The SPF record as published, or null when there is none.
        - `issues` object[]
          - `severity` 'error' | 'warning' | 'info': How serious the finding is. `error` is what a missing SPF, DMARC, MX or PTR record produces, the common case on a paid check. A lookup that failed is a `warning` saying the mechanism was not measured, never an `error`.
          - `message` string
      - `dkim` object: DKIM: which selectors published a key.
        - `status` 'pass' | 'weak' | 'missing' | 'invalid' | 'error' | 'found' | 'listed' | 'skipped', required: `missing` is an answer: the name has no such record (NXDOMAIN or NODATA). `error` means the lookup itself failed (SERVFAIL, REFUSED, a timeout), so nothing is known about the record: the mechanism is not measured, has `maxScore` 0, is named in `not_measured` and is left out of the score; when it is SPF, DKIM or DMARC the check has no grade. `found`, `listed` and `skipped` are used by some mechanisms only.
        - `score` number, required: What this mechanism contributed to the total, in raw points out of `maxScore`. The projected sections restate the same measurement as a percentage: `authentication.<mechanism>.score`, `dns.mx_score` and `reputation.score` are `round(score / maxScore × 100)`, so `checks.dkim` at 11 of 15 appears as `authentication.dkim.score: 73`. Two scales, one measurement.
        - `maxScore` number, required: The most it could have contributed. A mechanism whose lookup failed (status `error`, named in `not_measured`) has `maxScore` 0 and is left out of the grade rather than scored as missing, the way an unanswered blocklist section is. For SPF, DKIM or DMARC there is then no grade at all (`grade: null`). Until 2026-10-01 a failed SPF, DKIM, DMARC, MX or PTR lookup scored 0 against its full weight, so a DKIM SERVFAIL took a perfect domain from 100 (A) to 81 (B).
        - `selectors` string[]: The selectors a key was found under. Empty means none of the selectors tried resolved, not that DKIM is unused, because Kawaa can only probe the common ones. With `status: "error"` no key was found and some selectors could not be looked up, so DKIM was not measured.
        - `issues` object[]
          - `severity` 'error' | 'warning' | 'info': How serious the finding is. `error` is what a missing SPF, DMARC, MX or PTR record produces, the common case on a paid check. A lookup that failed is a `warning` saying the mechanism was not measured, never an `error`.
          - `message` string
      - `dmarc` object: DMARC: the published policy and record.
        - `status` 'pass' | 'weak' | 'missing' | 'invalid' | 'error' | 'found' | 'listed' | 'skipped', required: `missing` is an answer: the name has no such record (NXDOMAIN or NODATA). `error` means the lookup itself failed (SERVFAIL, REFUSED, a timeout), so nothing is known about the record: the mechanism is not measured, has `maxScore` 0, is named in `not_measured` and is left out of the score; when it is SPF, DKIM or DMARC the check has no grade. `found`, `listed` and `skipped` are used by some mechanisms only.
        - `score` number, required: What this mechanism contributed to the total, in raw points out of `maxScore`. The projected sections restate the same measurement as a percentage: `authentication.<mechanism>.score`, `dns.mx_score` and `reputation.score` are `round(score / maxScore × 100)`, so `checks.dkim` at 11 of 15 appears as `authentication.dkim.score: 73`. Two scales, one measurement.
        - `maxScore` number, required: The most it could have contributed. A mechanism whose lookup failed (status `error`, named in `not_measured`) has `maxScore` 0 and is left out of the grade rather than scored as missing, the way an unanswered blocklist section is. For SPF, DKIM or DMARC there is then no grade at all (`grade: null`). Until 2026-10-01 a failed SPF, DKIM, DMARC, MX or PTR lookup scored 0 against its full weight, so a DKIM SERVFAIL took a perfect domain from 100 (A) to 81 (B).
        - `record` string, nullable
        - `policy` string, nullable: The `p=` value: none, quarantine or reject.
        - `issues` object[]
          - `severity` 'error' | 'warning' | 'info': How serious the finding is. `error` is what a missing SPF, DMARC, MX or PTR record produces, the common case on a paid check. A lookup that failed is a `warning` saying the mechanism was not measured, never an `error`.
          - `message` string
      - `mx` object: MX: whether the domain can receive mail at all. No `issues` array on this one.
        - `status` 'pass' | 'weak' | 'missing' | 'invalid' | 'error' | 'found' | 'listed' | 'skipped', required: `missing` is an answer: the name has no such record (NXDOMAIN or NODATA). `error` means the lookup itself failed (SERVFAIL, REFUSED, a timeout), so nothing is known about the record: the mechanism is not measured, has `maxScore` 0, is named in `not_measured` and is left out of the score; when it is SPF, DKIM or DMARC the check has no grade. `found`, `listed` and `skipped` are used by some mechanisms only.
        - `score` number, required: What this mechanism contributed to the total, in raw points out of `maxScore`. The projected sections restate the same measurement as a percentage: `authentication.<mechanism>.score`, `dns.mx_score` and `reputation.score` are `round(score / maxScore × 100)`, so `checks.dkim` at 11 of 15 appears as `authentication.dkim.score: 73`. Two scales, one measurement.
        - `maxScore` number, required: The most it could have contributed. A mechanism whose lookup failed (status `error`, named in `not_measured`) has `maxScore` 0 and is left out of the grade rather than scored as missing, the way an unanswered blocklist section is. For SPF, DKIM or DMARC there is then no grade at all (`grade: null`). Until 2026-10-01 a failed SPF, DKIM, DMARC, MX or PTR lookup scored 0 against its full weight, so a DKIM SERVFAIL took a perfect domain from 100 (A) to 81 (B).
        - `records` object[]: The MX hosts, in the order DNS returned them.
          - `priority` integer, required
          - `host` string, required
      - `ptr` object: PTR: reverse DNS for `sending_ip`. Only meaningful when `sending_ip` was sent. No `issues` array on this one.
        - `status` 'pass' | 'weak' | 'missing' | 'invalid' | 'error' | 'found' | 'listed' | 'skipped', required: `missing` is an answer: the name has no such record (NXDOMAIN or NODATA). `error` means the lookup itself failed (SERVFAIL, REFUSED, a timeout), so nothing is known about the record: the mechanism is not measured, has `maxScore` 0, is named in `not_measured` and is left out of the score; when it is SPF, DKIM or DMARC the check has no grade. `found`, `listed` and `skipped` are used by some mechanisms only.
        - `score` number, required: What this mechanism contributed to the total, in raw points out of `maxScore`. The projected sections restate the same measurement as a percentage: `authentication.<mechanism>.score`, `dns.mx_score` and `reputation.score` are `round(score / maxScore × 100)`, so `checks.dkim` at 11 of 15 appears as `authentication.dkim.score: 73`. Two scales, one measurement.
        - `maxScore` number, required: The most it could have contributed. A mechanism whose lookup failed (status `error`, named in `not_measured`) has `maxScore` 0 and is left out of the grade rather than scored as missing, the way an unanswered blocklist section is. For SPF, DKIM or DMARC there is then no grade at all (`grade: null`). Until 2026-10-01 a failed SPF, DKIM, DMARC, MX or PTR lookup scored 0 against its full weight, so a DKIM SERVFAIL took a perfect domain from 100 (A) to 81 (B).
        - `record` string, nullable: The reverse-DNS name for the sending IP, when one was given. Null when no `sending_ip` was supplied or the lookup found nothing.
      - `blacklists` object: Public blocklists. Read `checked` and `inconclusive` before treating an empty `listed` as a clean bill of health.
        - `status` 'pass' | 'weak' | 'missing' | 'invalid' | 'error' | 'found' | 'listed' | 'skipped', required
        - `score` number, required: What this mechanism contributed to the total, in raw points out of `maxScore`. The projected sections restate the same measurement as a percentage: `authentication.<mechanism>.score`, `dns.mx_score` and `reputation.score` are `round(score / maxScore × 100)`, so `checks.dkim` at 11 of 15 appears as `authentication.dkim.score: 73`. Two scales, one measurement.
        - `maxScore` number, required: The most it could have contributed. A mechanism whose lookup failed (status `error`, named in `not_measured`) has `maxScore` 0 and is left out of the grade rather than scored as missing, the way an unanswered blocklist section is. For SPF, DKIM or DMARC there is then no grade at all (`grade: null`). Until 2026-10-01 a failed SPF, DKIM, DMARC, MX or PTR lookup scored 0 against its full weight, so a DKIM SERVFAIL took a perfect domain from 100 (A) to 81 (B).
        - `listed` object[]: The zones that answered "listed". Empty is the good case.
          - `name` string, required
          - `zone` string, required
          - `severity` 'critical' | 'high' | 'medium' | 'low'
        - `checked` integer: Zones that gave a definitive answer either way.
        - `inconclusive` integer: Zones that were asked and answered nothing usable: a timeout, a refusal, a SERVFAIL. NOT a pass: an unanswered zone is not evidence of a clean record, and it contributes neither a pass nor its share of `maxScore`.
        - `skipped` integer: Zones that were never asked, e.g. IP zones when no `sending_ip` was given.
        - `total` integer: Zones in the list. `checked + inconclusive + skipped`.
        - `issues` object[]
          - `severity` 'error' | 'warning' | 'info': How serious the finding is. `error` is what a missing SPF, DMARC, MX or PTR record produces, the common case on a paid check. A lookup that failed is a `warning` saying the mechanism was not measured, never an `error`.
          - `message` string
    - `check_id` string: The saved check. Fetch it again with `GET /v1/deliverability/checks/{id}`: free, and the only way back to a result whose response was lost.
    - `credits_used` number: What this call actually charged: 10 for a fresh check, 0 for a cached one or an idempotent replay. Read this rather than the price list.
    - `credits_remaining` number: Balance after the charge. Omitted when it could not be read; its absence is not a zero balance.
    - `cached` boolean: Present and true when this reused a check from the last 24 hours, which costs nothing. Send `refresh: true` to measure again.
    - `idempotent_replay` boolean: Present and true when this returned an earlier call's result under the same `Idempotency-Key`, and charged nothing. Distinct from `cached`: a replay can return a FRESH check that was already paid for.
    - `issues` object[]: Every finding, across every mechanism, severity-sorted. This is the list to render: the per-mechanism `issues` arrays do not carry MX, PTR or blocklist findings at all, so rebuilding this from `checks` loses exactly the problems that stop mail being delivered.
      - `check` string: Which mechanism produced it.
      - `severity` 'error' | 'warning' | 'info', required
      - `message` string, required
    - `recommendations` object[], required: What to change, in the order worth doing it. The remediation half of the 10 credits: a score with no recommendations is a grade without an answer. Empty on a domain with nothing to fix.
      - `priority` 'high' | 'medium' | 'low', required: Do the `high` ones first: they are the missing records that cause mail to be rejected outright, rather than the weak ones that cost reputation slowly.
      - `category` string, required: Which part of the setup this is about, e.g. `Authentication`, `Reputation`, `Infrastructure`.
      - `issue` string, required: What is wrong, in one line.
      - `fix` string, required: What to change, concretely: usually the record to publish and roughly what to put in it.
    - `not_measured` object: The scored mechanisms whose DNS lookup failed (SERVFAIL, REFUSED, a timeout), keyed by `spf`, `dkim`, `dmarc`, `mx` or `ptr`, each with a sentence saying why. Present only when at least one did; absent when every lookup answered, and on checks stored before 2026-10-01. A mechanism named here is unknown, not missing: its raw `checks.<mechanism>.status` is `error` with `maxScore` 0, its projected status is `not_measured`, it is left out of `score`, and it produces no recommendation. When it is `spf`, `dkim` or `dmarc`, `grade` and `spam_assessment.spam_likelihood` are null. The check is still charged, like any check that got real answers, with or without a grade; it is refunded (503) only when every lookup timed out or nothing at all could be scored. The 24-hour cache serves the stored result, so send `refresh: true` to measure again.
    - `checked_at` string, date-time, required: When the check ran. Falls back to the Unix epoch on an older cached row that stored none. A timestamp of `1970-01-01` means "not recorded", not "checked in 1970".
    - `dns` object, required: The mail exchangers, as the projection reports them.
      - `mx_records` object[], required
        - `priority` integer, required
        - `host` string, required
        - `ip` string, nullable, required: Null unless the host was resolved during the check; usually null.
      - `has_valid_mx` boolean, required: True only when the MX check passed AND at least one record came back. Also false when the MX lookup failed; `not_measured.mx` then says it was not measured rather than absent.
      - `mx_score` number, required: The MX contribution, normalized to 100. 0 when the MX lookup failed, which `not_measured.mx` names; read it as unknown, not as no MX.
    - `authentication` object, required: SPF, DKIM and DMARC as three comparable results. This is the half of the answer a sender acts on; `recommendations` says what to do about it.
      - `spf` object, required
        - `status` 'pass' | 'fail' | 'missing' | 'not_measured', required: The projection's status: `pass`, `fail`, `missing` or `not_measured`. `missing` means no record was found and is not the same as `fail`, which means one was found and did not hold up. `not_measured` means the lookup failed (SERVFAIL, REFUSED, a timeout), so nothing is known about the record; it is named in `not_measured` with the reason, left out of `score`, and the check has no `grade`. Added 2026-10-01: before then such a lookup read `missing`. A stored check from before then never carries it.
        - `record` string, nullable, required: The published record, or null when none was found.
        - `score` number, required: This mechanism's contribution, already normalized to 100: `round(checks.spf.score / checks.spf.maxScore × 100)`, 0 when nothing could be scored, including when it was `not_measured`. `checks.spf.score` is the same measurement in raw points (for example 11 of 15 there is 73 here); the two are not meant to agree numerically.
        - `issues` string[], required: What is wrong with this record, in sentences. Empty when nothing is.
      - `dkim` object, required
        - `status` 'pass' | 'fail' | 'missing' | 'not_measured', required: The projection's status: `pass`, `fail`, `missing` or `not_measured`. `missing` means no record was found and is not the same as `fail`, which means one was found and did not hold up. `not_measured` means the lookup failed (SERVFAIL, REFUSED, a timeout), so nothing is known about the record; it is named in `not_measured` with the reason, left out of `score`, and the check has no `grade`. Added 2026-10-01: before then such a lookup read `missing`. A stored check from before then never carries it.
        - `selectors_found` string[], required: Selectors that answered. Empty is the ordinary shape for a domain with no DKIM: the check probes a fixed list and cannot enumerate selectors.
        - `score` number, required: This mechanism's contribution, already normalized to 100: `round(checks.dkim.score / checks.dkim.maxScore × 100)`, 0 when nothing could be scored, including when it was `not_measured`. `checks.dkim.score` is the same measurement in raw points (for example 11 of 15 there is 73 here); the two are not meant to agree numerically.
        - `issues` string[], required: What is wrong with this record, in sentences. Empty when nothing is.
      - `dmarc` object, required
        - `status` 'pass' | 'fail' | 'missing' | 'not_measured', required: The projection's status: `pass`, `fail`, `missing` or `not_measured`. `missing` means no record was found and is not the same as `fail`, which means one was found and did not hold up. `not_measured` means the lookup failed (SERVFAIL, REFUSED, a timeout), so nothing is known about the record; it is named in `not_measured` with the reason, left out of `score`, and the check has no `grade`. Added 2026-10-01: before then such a lookup read `missing`. A stored check from before then never carries it.
        - `record` string, nullable, required
        - `policy` string, nullable, required: The `p=` value: `none`, `quarantine` or `reject`. `none` is published-but-not-enforcing, which scores worse than `quarantine` and better than nothing at all.
        - `score` number, required: This mechanism's contribution, already normalized to 100: `round(checks.dmarc.score / checks.dmarc.maxScore × 100)`, 0 when nothing could be scored, including when it was `not_measured`. `checks.dmarc.score` is the same measurement in raw points (for example 11 of 15 there is 73 here); the two are not meant to agree numerically.
        - `issues` string[], required: What is wrong with this record, in sentences. Empty when nothing is.
    - `reputation` object, required: Blacklist standing. The counts are separate on purpose: a zone that did not answer is not a zone that cleared you.
      - `score` number, nullable, required: **Null when fewer than half of `blacklists_total` gave a verdict and none listed the domain** (none at all, or, say, only the domain zones because no sending IP is known): there is no verdict to score, a 100 from the few that answered would read as a clean record, and a zero would read as "listed everywhere". A listing is scored whatever the coverage.
      - `blacklists_checked` integer, required: Zones that returned a usable answer.
      - `blacklists_inconclusive` integer, required: Zones that answered with something that could not be read as listed or not listed. **They are removed from the grade rather than scored against it**: both the section score and its maximum are scaled by the proportion of zones consulted, so an unreadable zone neither penalises the domain nor silently counts as clean; the reputation section simply carries less weight. That is why `score` here can be null when nothing answered at all.
      - `blacklists_skipped` integer, required: Zones not queried on this run.
      - `blacklists_total` integer, required: Zones in the list. `checked + inconclusive + skipped` should account for it.
      - `blacklists_listed` integer, required: Zones that reported the domain or its IP as listed.
      - `listed_on` object[], required: Which ones, and how much each matters. Empty when `blacklists_listed` is 0.
        - `name` string, required
        - `severity` 'critical' | 'high' | 'medium' | 'low', required: How much a listing there costs, worst first.
    - `server` object, required: What a probe of the receiving server would have found. **Four of these five are always null**: `accepts_mail`, `response_time_ms`, `supports_tls` and `tls_version` are hard-coded null in the projection: no SMTP banner, TLS or acceptance probe runs on this route, with or without a `sending_ip`. Only `reverse_dns` is ever populated, from the PTR check, and only when a `sending_ip` was supplied. Null here means "not measured", never "no". The fields exist because the shape is shared with a probe that may be added later; today they are placeholders and must not be reported as evidence about the server.
      - `accepts_mail` boolean, nullable, required: Always null today: no acceptance probe runs. Not "the server refuses mail".
      - `response_time_ms` number, nullable, required: Always null today: nothing is timed.
      - `supports_tls` boolean, nullable, required: Always null today. Not "no TLS": nothing connected to find out.
      - `tls_version` string, nullable, required: Always null today, for the same reason as `supports_tls`.
      - `reverse_dns` string, nullable, required: The PTR record for the `sending_ip`, and the only field here that is ever populated. Null when no `sending_ip` was supplied, because reverse DNS is looked up for that address alone and is skipped without one; it never resolves an address from the MX records.
    - `spam_assessment` object, required: The same evidence again, expressed as how likely mail from this domain is to be filtered.
      - `score` number, required: How likely mail from this domain is to be filtered, 0-100, where **higher is worse**. It is the INVERSE of the top-level `score`: `100 - score`, clamped. A domain scoring 90 for deliverability carries a spam score of 10. Do not compare it against the top-level number or apply the same thresholds to it.
      - `spam_likelihood` 'low' | 'medium' | 'high' | 'null', nullable, required: A band, not a prediction about any one message. **Derived from the DELIVERABILITY score, not from `score` beside it**: `low` at 80 and above, `medium` from 60, `high` below. Because that sibling is the inverse, applying these numbers to it reads every domain backwards: a spam score of 10 is the best case, not the worst. Read `spam_likelihood` and leave the arithmetic alone. **null when `grade` is null**: the band is read off the same partial score, so it is withheld with the grade.
      - `factors` object[], required: What pushed the score each way, including the things that helped.
        - `factor` string, required: What was looked at, e.g. `SPF`, `DKIM`, `DMARC`, `MX`.
        - `impact` 'positive' | 'negative' | 'neutral', required: Which way it pushed. `neutral` means it changed nothing: it was assessed with no effect, skipped (reverse DNS without a `sending_ip`, or too few blocklists answered to give a verdict), or not measured because its lookup failed. The `description` says which.
        - `description` string, required: Why, in one sentence.

## Other responses

- `401`: No credential, or one that is not accepted. `code` distinguishes `API_KEY_SUSPENDED` (subscription ended; restartable) from `API_KEY_REVOKED`, `SESSION_EXPIRED` and `OAUTH_TOKEN_EXPIRED` (a connected app's one-hour access token lapsed: refresh it and retry, or sign in again if no refresh token was issued or the refresh is refused).
- `403`: Refused, and **the code says which of three problems it is**, and they need different remedies: - `INSUFFICIENT_SCOPE`: the credential is valid but not scoped for this route. With an API key, issue one that carries the scope named in the message. With an OAuth access token, connect the app again asking for that scope as well as the ones it has, unless the message says a connected app can never be given it; then use an API key with every scope the message names, or have the account owner do it in the dashboard. - `PLAN_UPGRADE_REQUIRED`: the account's plan does not include the feature. **No key of any scope reaches it**, so issuing another is wasted work; the remedy is a plan change. Returned by the verification routes for `options.enrich`, and by the finder, the monitors, the audit log, auto-clean and the white-label routes. - `PLAN_LIMIT_EXCEEDED`: the plan includes the feature and the account is at its ceiling (webhook endpoints, team seats, integrations, auto-clean lists). Delete one or upgrade. - `FORBIDDEN`: refused for a reason none of the above covers. `ErrorCode` is an open set, so branch on the code with a default rather than exhaustively.
- `404`: No such resource on this account.
- `429`: Too many requests. When the server knows how long to wait, `Retry-After` says so and `error.retry_after_seconds` carries the same number; the `X-RateLimit-*` headers say where you stand.
- `default`: Any status this operation does not list. Kawaa's own errors carry the error envelope. The gateway in front of the API can answer any operation with its own bare body (`GatewayError`): a `503` after about 29 seconds, when the request may still have run and been charged, a `413` for a body over about 6 MB, or a `400` for a path with malformed percent-encoding.

---

[API](https://skmtc.dev/kawaa/apis/kawaa-api.md) · [All operations](https://skmtc.dev/kawaa/apis/kawaa-api/llms.txt) · [OpenAPI document](https://skmtc.dev/kawaa/apis/kawaa-api/revisions/d27c9c33f6ba?raw)
