{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://artifacts.apis.io/kawaa-com/json-schema/kawaa-com-sub-account-schema.json",
  "title": "SubAccount",
  "description": "What `GET /v1/account` returns when the key belongs to a white-label sub-account. A sub-account has no plan or allowance of its own: it spends a pool its parent allocated, so the credit fields are `allocated` and `credits_used` rather than `monthly_allowance` and `used_this_month`. Their absence is the shape of this account, not missing billing data.",
  "x-generated": "2026-10-31",
  "x-method": "derived",
  "x-generator": "derive-json-schema.py",
  "x-source": "openapi/kawaa-com-openapi.json#/components/schemas/SubAccount",
  "type": "object",
  "properties": {
    "account": {
      "type": "object",
      "properties": {
        "user_id": {
          "type": "string",
          "description": "The sub-account's own id."
        },
        "email": {
          "type": "string",
          "format": "email"
        },
        "name": {
          "type": "string"
        },
        "plan": {
          "const": "sub_account",
          "description": "Always this. The parent's tier decides the limits; see `GET /v1/usage/limits`, whose `plan_note` says where the inherited numbers come from."
        },
        "limits_plan": {
          "type": "string",
          "enum": [
            "free",
            "starter",
            "professional",
            "business",
            "enterprise"
          ],
          "description": "The plan whose features and limits this sub-account has: its parent's, as it stood when the request was made. A sub-account never gets more than its parent's plan includes."
        },
        "batch_size": {
          "type": "integer",
          "minimum": 1,
          "description": "The most distinct, well-formed addresses one bulk job accepts for this sub-account: the batch size of its parent's plan (`limits_plan`). A sub-account cannot buy credit packs. Absent from responses by builds that predate it."
        },
        "status": {
          "type": "string"
        },
        "created_at": {
          "description": "The sub-account creation time. A sub-account created before this time was recorded returns the empty string; no timestamp is invented.",
          "anyOf": [
            {
              "type": "string",
              "format": "date-time"
            },
            {
              "const": ""
            }
          ]
        },
        "parent_user_id": {
          "type": "string",
          "description": "The account that provisioned this one and owns its credit pool."
        }
      },
      "required": [
        "user_id",
        "email",
        "plan",
        "status",
        "parent_user_id"
      ]
    },
    "credits": {
      "type": "object",
      "properties": {
        "balance": {
          "type": "number",
          "description": "What is left of the allocation. Fractional: a cached verification costs 0.5."
        },
        "allocated": {
          "type": "number",
          "description": "What the parent has granted in total. `0` for a sub-account that has never been allocated any."
        },
        "credits_used": {
          "type": "number",
          "description": "Spend since provisioning: exactly `allocated - balance`, which is why it is published even for a key without the analytics scope: both operands are already here. This says nothing about `usage.current_month`, which is still withheld and still flagged."
        }
      },
      "required": [
        "balance",
        "allocated"
      ]
    },
    "usage": {
      "type": "object",
      "description": "`current_month` and `period`, as on the parent shape. `current_month` is `null` when it was not measured (`withheld.usage` or `partial.usage`)."
    },
    "api_keys": {
      "type": "array",
      "description": "The sub-account's own API keys, as `GET /v1/api-keys` lists them for the same key: those its provider issued it and any it created. Empty when `withheld.api_keys` or `partial.api_keys` is set.",
      "items": {
        "$ref": "#/$defs/ApiKeySummary"
      }
    },
    "recent_jobs": {
      "type": "array",
      "items": {
        "$ref": "#/$defs/Job"
      }
    },
    "rate_limits": {
      "type": "object",
      "properties": {
        "requests_per_minute": {
          "type": "integer",
          "description": "The sub-account's own ceiling, inherited from the parent's tier."
        },
        "enforced": {
          "type": "boolean"
        }
      }
    },
    "partial": {
      "type": "object",
      "description": "Names sections that could NOT BE LOADED. Their values are placeholders, not facts: `api_keys` and `recent_jobs` come back empty and `usage.current_month` comes back `null`.",
      "properties": {
        "usage": {
          "const": true
        },
        "api_keys": {
          "const": true
        },
        "recent_jobs": {
          "const": true
        }
      }
    },
    "withheld": {
      "type": "object",
      "description": "Names sections this API key's SCOPES DO NOT COVER. `usage: true` means `usage.current_month` is `null` (not measured); `api_keys: true` means the key list comes back empty because this key may not read it.",
      "properties": {
        "usage": {
          "const": true
        },
        "api_keys": {
          "const": true
        },
        "recent_jobs": {
          "const": true
        }
      }
    }
  },
  "required": [
    "account",
    "credits"
  ],
  "$defs": {
    "ActivityData": {
      "type": "object",
      "properties": {
        "level": {
          "type": "string",
          "enum": [
            "active",
            "inactive",
            "dormant",
            "unknown"
          ],
          "description": "How recently this address was seen active. `inactive` sits between the active and dormant windows; `unknown` means there was nothing to judge from, not that the address is inactive."
        },
        "score": {
          "type": "number"
        },
        "last_activity_at": {
          "type": [
            "string",
            "null"
          ],
          "format": "date-time"
        },
        "engagement": {
          "type": "object",
          "properties": {
            "open_count": {
              "type": "integer"
            },
            "click_count": {
              "type": "integer"
            },
            "reply_count": {
              "type": "integer",
              "description": "Counts replies recorded by Kawaa email warmup campaigns only. It is not a measurement of replies to your own mail, which Kawaa cannot see. Warmup is switched off, so no new replies are recorded, and new opens and clicks come only from your own tracking pixels and links."
            }
          },
          "required": [
            "open_count",
            "click_count",
            "reply_count"
          ]
        },
        "deliverability": {
          "type": "object",
          "properties": {
            "bounce_count": {
              "type": [
                "integer",
                "null"
              ]
            },
            "complaint_count": {
              "type": [
                "integer",
                "null"
              ]
            }
          },
          "required": [
            "bounce_count",
            "complaint_count"
          ]
        },
        "confidence": {
          "type": "number",
          "description": "0 means your account has recorded no activity for this address, not that it is inactive."
        },
        "data_points": {
          "type": "integer"
        },
        "message": {
          "type": "string",
          "description": "Present only when no activity data is available for a single-address lookup."
        }
      },
      "required": [
        "level",
        "score",
        "last_activity_at",
        "engagement",
        "deliverability",
        "confidence",
        "data_points"
      ]
    },
    "ApiKeySummary": {
      "type": "object",
      "properties": {
        "key_id": {
          "type": "string"
        },
        "name": {
          "type": "string"
        },
        "status": {
          "type": "string"
        },
        "created_at": {
          "type": "string",
          "format": "date-time"
        },
        "last_used_at": {
          "type": [
            "string",
            "null"
          ],
          "format": "date-time"
        },
        "key_preview": {
          "type": "string",
          "description": "`ev_****` and the key's last four characters, never the secret. An older key that has no stored preview shows `ev_••••`, with no characters from the key."
        },
        "scopes": {
          "type": [
            "array",
            "null"
          ],
          "items": {
            "type": "string",
            "enum": [
              "verification:read",
              "verification:write",
              "lists:read",
              "lists:write",
              "deliverability:read",
              "deliverability:write",
              "compose:read",
              "compose:write",
              "exports:read",
              "exports:write",
              "webhooks:read",
              "webhooks:write",
              "integrations:read",
              "integrations:write",
              "analytics:read",
              "analytics:write",
              "account:read",
              "account:write",
              "account:delete",
              "billing:read",
              "billing:write",
              "team:read",
              "team:write",
              "security:read",
              "security:write",
              "white_label:read",
              "white_label:write"
            ]
          },
          "description": "What this key may reach. **`null` means UNRESTRICTED**: every key created without a `scopes` array is, and that is the default. Always present, so `null` is a fact about the key rather than a field an older build omitted."
        }
      },
      "required": [
        "key_id",
        "name",
        "status",
        "scopes"
      ]
    },
    "Job": {
      "type": "object",
      "properties": {
        "job_id": {
          "type": "string",
          "format": "uuid"
        },
        "job_type": {
          "type": "string",
          "description": "On a `GET /v1/jobs` row and an `account.recent_jobs` entry: `single_verification` for a job created by `POST /v1/verify`. Batch and uploaded-file jobs usually have none, and an older one can carry another label, so read any other value, or none, as a batch. Not returned by `GET /v1/jobs/{id}`."
        },
        "status": {
          "type": "string",
          "enum": [
            "pending",
            "processing",
            "completed",
            "failed",
            "cancelled",
            "enqueue_unknown"
          ],
          "description": "The job's state. `enqueue_unknown` means some sends could not be confirmed and credits are held pending reconciliation. Poll until it resolves.\n\n**`cancelled` is what `POST /v1/jobs/{id}/cancel` produces.** Stopping a batch leaves the addresses already checked in place, billed, and readable; the credits held for the rest are refunded. Because the refund waits for in-flight addresses to finish, a job can read `cancelled` with `billing_status: \"charged\"` for a few minutes before it reads `refunded`. That is the refund draining, not a failure, and nothing is required of you.\n\n**Cancelled or not, handle it as terminal, and do so immediately.** A cancelled job's verdicts are final the moment the status says so: every result is written together with the job's progress counter in one conditional write requiring the job to still be running, so the cancel closes the result set as it lands. What is NOT final yet is the bill: the refund for the addresses it never checked waits for the in-flight ones to time out. That is what `completed_at` marks, and the Node.js and Python SDKs expose it as `isSettled` / `is_settled`. Read it if you are reconciling the charge; do not make your results wait on it. What must not happen is treating `cancelled` as still-running and polling for ever.\n\nTerminal does NOT mean empty: such a job can carry a **partial** result set, and `GET /v1/jobs/{id}?include_results=true` returns it: the API gates on `processed_emails > 0`, not on status, so the addresses it did check come back. Read `processed_emails` against `total_emails` to see how far it got, and do not discard those results. `GET /v1/jobs/{id}/download` exports exactly those addresses too. Read progress as `processed_emails` of `requested_emails`: a job that stopped early has `total_emails` collapsed onto what it actually checked and billed.\n\n**A cancel is a brake, not a substitute for sizing the batch.** The submission-time limits still apply and still refuse first: the plan's per-batch ceiling (`413 BATCH_SIZE_EXCEEDED`), the 10,000 global cap, and the credit balance (`402`). Cancelling after the fact returns the unprocessed credits, never the ones already spent."
        },
        "total_emails": {
          "type": "integer"
        },
        "cache_discount_pending": {
          "type": "boolean",
          "description": "Present, and only ever `true`, when this read tried to apply the per-address cache discount and could not, almost always because the results index had not caught up, so the cached count could not be trusted yet. The next read retries it. It exists because nothing else on the job tells \"checked, nothing owed\" apart from \"could not check\": both leave a `completed` or `cancelled` job's credits unchanged. If you are reconciling the charge, you have one more read to do while this is true."
        },
        "requested_emails": {
          "type": "integer",
          "description": "The list size you submitted. Present only when some of it will never have a result: a job that was cancelled or stopped early, or one where some addresses could not be queued when you submitted it or never came back from the queue. `total_emails` is then lowered to the addresses actually checked and billed, and the rest are refunded. That is how the platform records that the job owes nothing more for them. Read progress as `processed_emails` of this. The same figure as `requested_emails` on the `job.completed` webhook."
        },
        "partial": {
          "type": "boolean",
          "description": "Present, and only ever `true`, exactly when `requested_emails` is: `requested_emails` minus `total_emails` addresses have no result and never will, and their credits were refunded. A `completed` job can carry it. The same signal as `partial` on the `job.completed` webhook, so polling this endpoint after a missed delivery tells you what the delivery would have. Absent while `total_emails` still counts every address you submitted: a cancelled job gains it when the refund for the addresses it never checked is applied, which `completed_at` marks."
        },
        "file_counts": {
          "type": "object",
          "description": "What the uploaded file held, as `POST /v1/verify/file` counted it when it created the job: the same four figures its acceptance response reported, under the same names. Present on the single job, as `GET /v1/jobs/{id}` and `POST /v1/jobs/{id}/cancel` return it, when the job was created from a file; never on the listing, and absent on a job submitted as a list and on a file job recorded before these counts were stored. `total_emails` is only the addresses verified, so this is the one place the rows that were skipped or merged are still accounted for. `total_submitted` = `unique_emails` + `duplicates_removed` + `invalid_emails_skipped`.",
          "properties": {
            "total_submitted": {
              "type": "integer",
              "description": "Rows read from the file, including the invalid ones. A header row is not counted."
            },
            "invalid_emails_skipped": {
              "type": "integer",
              "description": "Rows that were not addresses. Skipped, not charged for, and not in the job."
            },
            "duplicates_removed": {
              "type": "integer",
              "description": "Repeats of an earlier address (compared case-insensitively). Removed before charging."
            },
            "unique_emails": {
              "type": "integer",
              "description": "Distinct addresses the file yielded: what the job was created to verify."
            }
          },
          "required": [
            "total_submitted",
            "invalid_emails_skipped",
            "duplicates_removed",
            "unique_emails"
          ]
        },
        "processed_emails": {
          "type": "integer"
        },
        "progress_percent": {
          "type": "integer"
        },
        "summary": {
          "$ref": "#/$defs/JobSummary"
        },
        "results": {
          "type": "array",
          "items": {
            "$ref": "#/$defs/VerificationResult"
          }
        },
        "next_key": {
          "type": [
            "string",
            "null"
          ],
          "description": "Cursor for the next page of results."
        },
        "has_more": {
          "type": "boolean"
        },
        "credits_used": {
          "type": "number"
        },
        "credits_refunded": {
          "type": "number"
        },
        "billing_status": {
          "type": "string"
        },
        "created_at": {
          "type": "string",
          "format": "date-time"
        },
        "updated_at": {
          "type": "string",
          "format": "date-time"
        },
        "completed_at": {
          "type": [
            "string",
            "null"
          ],
          "format": "date-time",
          "description": "When the job's books closed for the CANCELLATION refund. Null while the job is running and (on a `cancelled` one) until the refund for the unprocessed addresses commits, or the platform settles a job that owed nothing. It says nothing about the results, which are final as soon as the status is terminal. It is not the whole bill either: an ordinary `completed` batch has this set and can still owe its per-address cache discount, and so can a `cancelled` one (its discount on the addresses it did check is applied right after this refund), which `cache_discount_pending` reports. Reconciling a charge means reading both."
        },
        "cancelled_at": {
          "type": [
            "string",
            "null"
          ],
          "format": "date-time",
          "description": "When `POST /v1/jobs/{id}/cancel` stopped the job. Absent on every other job."
        },
        "error_message": {
          "type": [
            "string",
            "null"
          ],
          "description": "Why a terminal job ended as it did, in Kawaa's own customer-safe words, present on failures and on reconciliation outcomes. Without it, `status: \"failed\"` is the whole story a client can tell."
        }
      },
      "required": [
        "job_id",
        "status"
      ]
    },
    "JobSummary": {
      "type": "object",
      "description": "Four buckets, and they are BUCKETS rather than verdicts. The whole rule: `valid` is exactly `valid`; `invalid` is `invalid` AND `disposable`; `risky` is `risky`, `catch_all`, `role` and `spam_trap`; `unknown` is `unknown` and any row with no stored status. A per-address verdict is in the result row, not here, and `catch_all` inside `risky` is the one worth remembering, because it is inconclusive rather than bad.",
      "properties": {
        "valid": {
          "type": "integer",
          "description": "Exactly `valid`."
        },
        "invalid": {
          "type": "integer",
          "description": "Rows whose verdict is `invalid` OR `disposable`. Building a suppression list from this bucket therefore removes disposable addresses too, which is usually intended."
        },
        "risky": {
          "type": "integer",
          "description": "Rows whose verdict is `risky`, `catch_all`, `role` or `spam_trap`. Mixed: `catch_all` is inconclusive, not bad."
        },
        "unknown": {
          "type": "integer",
          "description": "Rows whose verdict is `unknown`, plus any row with no stored status at all."
        }
      }
    },
    "SubStatus": {
      "type": "string",
      "description": "Why the verdict came out as it did. New values can be added without a version change; treat an unfamiliar one as informational. Deliberately NOT a closed enum: the list above is what this build emits, and a new reason ships without a version bump. A generated validator built from a closed set would reject an otherwise valid verification result the day one is added, which is the opposite of the forward compatibility this field promises."
    },
    "VerificationResult": {
      "type": "object",
      "description": "A completed verification.",
      "properties": {
        "job_id": {
          "type": "string",
          "format": "uuid"
        },
        "batch_job_id": {
          "type": "string",
          "format": "uuid",
          "description": "The batch or file job this result belongs to, when it belongs to one: on a batch row of `GET /v1/jobs/{id}` it is the same id as `job_id`. Omitted from a single verification (`POST /v1/verify`, and its job read back through `GET /v1/jobs/{id}`), which belongs to no batch."
        },
        "email": {
          "type": "string",
          "format": "email"
        },
        "status": {
          "$ref": "#/$defs/VerificationStatus"
        },
        "sub_status": {
          "$ref": "#/$defs/SubStatus"
        },
        "quality_score": {
          "type": "integer",
          "minimum": 0,
          "maximum": 100
        },
        "verification": {
          "type": "object",
          "properties": {
            "syntax_valid": {
              "type": "boolean"
            },
            "domain_exists": {
              "type": "boolean",
              "description": "`true` when DNS shows the domain exists. `false` is not always a DNS answer: it is also the value when DNS was not read (a syntax error) or did not answer (`sub_status: \"temporary_failure\"`). Only `status: \"invalid\"` with `sub_status: \"domain_not_found\"` says DNS found no such domain."
            },
            "mx_found": {
              "type": "boolean",
              "description": "`true` when the domain publishes MX records. `false` when it publishes none, when it publishes a null MX (`MX 0 .`, RFC 7505) and takes no mail, or when DNS was not read or did not answer (a syntax error, `sub_status: \"temporary_failure\"`). A domain with no MX record at all can still take mail at its address record, so read `false` with `status`: when the mail server was checked (`smtp_check` present), `status` says what it answered."
            },
            "mx_records": {
              "type": "array",
              "items": {
                "type": "string"
              }
            },
            "smtp_check": {
              "type": "boolean"
            },
            "catch_all": {
              "type": "boolean",
              "description": "The domain accepts mail for any address, so an accepted mailbox proves nothing on its own. Omitted when the catch-all probe did not measure it: the check ended before any probe (a disposable or spam-trap verdict, a domain with no usable mail server), the mail server refused or never answered for the mailbox so the probe never ran, or the probe could not settle it (that result is `risky`). Also `true` beside `provider_accept_all`, where the mail provider accepts addresses it does not have. Absent is not `false`."
            },
            "greylisted": {
              "type": "boolean"
            },
            "smtp_code": {
              "type": [
                "integer",
                "null"
              ]
            },
            "smtp_message": {
              "type": [
                "string",
                "null"
              ]
            },
            "mailbox_exists": {
              "type": "boolean",
              "description": "`true` when the mail server accepted the mailbox at RCPT TO, on a domain that does not accept every address. `false` when the server answered RCPT TO without accepting it: read it as \"not confirmed\", not \"confirmed absent\". It is usually a refusal of the mailbox (`status: \"invalid\"`), but it is also `false` when the answer was temporary (`status: \"unknown\"`, `sub_status` `greylisted` or `temporary_failure`) or the server refused the sender rather than the mailbox (`sub_status: \"policy_block\"`), so read it with `status` and `sub_status`. Omitted when no server answered RCPT TO (no probe ran, the connection failed, or the conversation was refused before RCPT, for example at the greeting), when an acceptance proves nothing (`catch_all`, `provider_accept_all` or a `risky` result), and when the server said the mailbox is full. Absent is not `false`."
            },
            "provider_accept_all": {
              "type": "boolean",
              "description": "The PROVIDER accepts recipient probes too broadly for an individual mailbox to be confirmed. Not the same as `catch_all`, which is the domain's own configuration. Present when the worker could tell. SMTP acceptance here is not evidence the mailbox exists, and a client that treats it as such publishes a deliverability claim nothing measured."
            }
          },
          "required": [
            "syntax_valid",
            "domain_exists",
            "mx_found",
            "greylisted"
          ]
        },
        "flags": {
          "type": "object",
          "description": "Independent of the verdict: an address can be valid and also role_account.",
          "properties": {
            "disposable": {
              "type": "boolean"
            },
            "role_account": {
              "type": "boolean"
            },
            "free_provider": {
              "type": "boolean"
            },
            "spam_trap": {
              "type": "boolean"
            },
            "abuse_email": {
              "type": "boolean",
              "description": "The local part matched an abuse, complaint-handling, feedback-loop, legal or blocklist-removal name or pattern, at any domain, such as abuse@, postmaster@, feedback@, legal@, spam-alerts@ or abuse1@. Every address with complainer set has it too. With AI analysis on, the model can also raise it for a local part containing abuse, spam or complaint, at a domain that can receive mail. A match on the address itself, not a finding about what the mailbox is used for or anything its owner has done."
            },
            "honeypot": {
              "type": "boolean"
            },
            "complainer": {
              "type": "boolean",
              "description": "A narrower abuse_email: the address is a complaint-handling mailbox, such as abuse@, postmaster@, fbl@ or feedback@, at a reviewed anti-spam, feedback-loop, sender-reputation or consumer-protection organization, such as spamhaus.org, spamcop.net or ftc.gov. The same local part at any other domain, and any other mailbox at those organizations, does not raise it. A match on the address itself, not complaint history: verification does not derive it from any record of who has marked mail as spam."
            },
            "bot_generated": {
              "type": "boolean"
            }
          },
          "required": [
            "disposable",
            "role_account",
            "free_provider",
            "spam_trap",
            "abuse_email",
            "honeypot",
            "complainer",
            "bot_generated"
          ]
        },
        "domain_info": {
          "type": "object",
          "description": "The domain's provider type and email-authentication posture. The SPF, DKIM and DMARC fields (`auth_score`, `auth_grade`, `has_spf`, `spf_policy`, `has_dmarc`, `dmarc_policy`, `has_dkim`) are each omitted when their lookup failed or did not run. The lookup runs only when `options.include_ai` is on (the default); with it off, they are present only when a recent check of the same domain already measured them.",
          "properties": {
            "domain": {
              "type": "string"
            },
            "provider_type": {
              "type": "string"
            },
            "auth_score": {
              "type": "integer"
            },
            "auth_grade": {
              "type": "string"
            },
            "has_spf": {
              "type": "boolean"
            },
            "spf_policy": {
              "type": [
                "string",
                "null"
              ]
            },
            "has_dmarc": {
              "type": "boolean"
            },
            "dmarc_policy": {
              "type": [
                "string",
                "null"
              ]
            },
            "has_dkim": {
              "type": "boolean"
            }
          }
        },
        "ai": {
          "type": "object",
          "properties": {
            "typo_suggestion": {
              "type": [
                "string",
                "null"
              ],
              "description": "A corrected address when one looks likely; omitted when there is none. The same value as the top-level `suggestion`. This is the field name the API uses; there is no `suggested_email`."
            },
            "typo_confidence": {
              "type": [
                "number",
                "null"
              ],
              "description": "How confident the correction is, when one was made."
            },
            "typo_method": {
              "type": [
                "string",
                "null"
              ],
              "description": "How the suggestion was arrived at, as the detector names it. Present whenever `typo_suggestion` is. Treat it as an open set: it is the detector's own label, and a new detector adds a new one."
            },
            "catch_all_confidence": {
              "type": [
                "number",
                "null"
              ],
              "minimum": 0,
              "maximum": 100,
              "description": "0-100, and only for a domain that accepts everything: how likely this particular mailbox is real despite the catch-all. It is the one piece of evidence that separates two `catch_all` addresses, and it is absent on every other result."
            }
          },
          "description": "Present when the worker has something to report: it is built when there is a typo suggestion or a catch-all confidence, and omitted otherwise. That holds whether or not `include_ai` is set: without it the built-in rules produce both; with it, AI analysis is tried first and the rules are the fallback. The four fields below are every field it can carry: it never includes a `fraud_score` or `patterns` field."
        },
        "suggestion": {
          "type": "string",
          "description": "The corrected address when the domain looks like a misspelling of a well-known one, such as `jane@gmail.com` for `jane@gmial.com`. The same value as `ai.typo_suggestion`, and present exactly when that is. It is advice only: `status` and every check describe the address that was sent, not the correction.\n\nThe typo check runs whether or not `include_ai` is set; with it, AI analysis can also catch misspellings the built-in list does not. Omitted (never `null`) when there is no correction."
        },
        "credits_used": {
          "type": "number"
        },
        "credits_remaining": {
          "type": "number"
        },
        "processing_time_ms": {
          "type": "integer"
        },
        "verified_at": {
          "type": "string",
          "format": "date-time"
        },
        "from_cache": {
          "type": "boolean"
        },
        "request_id": {
          "type": "string"
        },
        "idempotent_replay": {
          "type": "boolean",
          "description": "`POST /v1/verify` only. True when this request, sent without an `Idempotency-Key`, joined an identical request whose verification was still in flight and was answered from it: `credits_used` is `0` and nothing was charged. Absent when this request paid for the verdict."
        },
        "activity": {
          "$ref": "#/$defs/ActivityData",
          "description": "Engagement history, attached only when the request asked for it with `include_activity`. Read from your own account's tracking only, never another account's. When your tracking has recorded nothing for the address it is still an object, with `level: \"unknown\"`, `score: 0`, `last_activity_at: null`, zero counts and `data_points: 0`; that is not a signal about the mailbox. Absent when the request did not ask for it, and when the activity could not be read."
        },
        "domain_reputation": {
          "type": [
            "object",
            "null"
          ],
          "description": "Reserved for a stored numeric domain reputation score when `include_activity` is true. Kawaa does not compute one today, so current responses omit this field."
        },
        "enrichment": {
          "type": [
            "object",
            "null"
          ],
          "description": "Attached when the request set `options.enrich` and the plan includes data enrichment. Null when nothing is known, which is not a judgement about the address.",
          "properties": {
            "first_name": {
              "type": [
                "string",
                "null"
              ]
            },
            "last_name": {
              "type": [
                "string",
                "null"
              ]
            },
            "full_name": {
              "type": [
                "string",
                "null"
              ]
            },
            "gender": {
              "type": [
                "string",
                "null"
              ]
            },
            "country": {
              "type": [
                "string",
                "null"
              ]
            },
            "country_code": {
              "type": [
                "string",
                "null"
              ]
            }
          }
        },
        "cache_age_seconds": {
          "type": "integer",
          "description": "How old the cached result is, in seconds. Present on every cache hit, beside `from_cache: true`. Without it a result reused from days ago reads exactly like one measured this second."
        },
        "classifier_version": {
          "type": "integer",
          "description": "Which verdict classifier produced this result. Stamped by the worker onto every newly produced result. When the classifier changes a cached result from an older version is never reused (the address is verified afresh), so two results with different values here were not decided by the same rules."
        }
      },
      "required": [
        "email",
        "status",
        "quality_score",
        "verification",
        "flags"
      ]
    },
    "VerificationStatus": {
      "type": "string",
      "description": "The verdict. `catch_all` means the domain accepts mail for every recipient, so this particular mailbox could not be checked: it is neither a yes nor a no. `unknown` means the check could not conclude.",
      "enum": [
        "valid",
        "invalid",
        "risky",
        "unknown",
        "catch_all",
        "disposable",
        "role",
        "spam_trap"
      ]
    }
  }
}