---
title: "List verification jobs"
method: GET
path: "/v1/jobs"
tags: ["Jobs"]
---

# List verification jobs

`GET /v1/jobs`

## Request

- Base URL: `https://api.kawaa.com`
- URL: `https://api.kawaa.com/v1/jobs`
- 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: `verification:read`)

Newest first. `total` counts every job matching the listing, not just the page. Paginate on `has_more` and `next_offset`.

`total_is_approximate: true` means the count stopped at its scan bound, so `total` is a floor.

## Query parameters

- `limit` integer
- `status` 'pending' | 'processing' | 'completed' | 'failed' | 'cancelled' | 'enqueue_unknown'
- `offset` string
- `type` 'batch'

## Response `200`

A page of jobs.

- object
  - `success` unknown, required
  - `data` JobPage, required
    - `jobs` JobListItem[], required: Each row is a `Job` plus `filename`, which is how a file upload is told from a submitted list. The per-job endpoint does not carry it.
      - `job_id` string, uuid, required
      - `status` 'pending' | 'processing' | 'completed' | 'failed' | 'cancelled' | 'enqueue_unknown', required: The job's state. `enqueue_unknown` means some sends could not be confirmed and credits are held pending reconciliation. Poll until it resolves. **`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. **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. Terminal 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. **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` integer
      - `cache_discount_pending` boolean: 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` integer: 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` boolean: 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` object: 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`.
        - `total_submitted` integer, required: Rows read from the file, including the invalid ones. A header row is not counted.
        - `invalid_emails_skipped` integer, required: Rows that were not addresses. Skipped, not charged for, and not in the job.
        - `duplicates_removed` integer, required: Repeats of an earlier address (compared case-insensitively). Removed before charging.
        - `unique_emails` integer, required: Distinct addresses the file yielded: what the job was created to verify.
      - `processed_emails` integer
      - `progress_percent` integer
      - `summary` JobSummary: 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.
        - `valid` integer: Exactly `valid`.
        - `invalid` integer: Rows whose verdict is `invalid` OR `disposable`. Building a suppression list from this bucket therefore removes disposable addresses too, which is usually intended.
        - `risky` integer: Rows whose verdict is `risky`, `catch_all`, `role` or `spam_trap`. Mixed: `catch_all` is inconclusive, not bad.
        - `unknown` integer: Rows whose verdict is `unknown`, plus any row with no stored status at all.
      - `results` VerificationResult[]
        - `job_id` string, uuid
        - `batch_job_id` string, uuid: 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` string, email, required
        - `status` 'valid' | 'invalid' | 'risky' | 'unknown' | 'catch_all' | 'disposable' | 'role' | 'spam_trap', required: 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.
        - `sub_status` string: 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.
        - `quality_score` integer, required
        - `verification` object, required
          - `syntax_valid` boolean, required
          - `domain_exists` boolean, required
          - `mx_found` boolean, required
          - `mx_records` string[]
          - `smtp_check` boolean
          - `catch_all` boolean: 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` boolean, required
          - `smtp_code` integer, nullable
          - `smtp_message` string, nullable
          - `mailbox_exists` boolean: `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` boolean: 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.
        - `flags` object, required: Independent of the verdict: an address can be valid and also role_account.
          - `disposable` boolean, required
          - `role_account` boolean, required
          - `free_provider` boolean, required
          - `spam_trap` boolean, required
          - `abuse_email` boolean, required: 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. A match on the address itself, not a finding about what the mailbox is used for or anything its owner has done.
          - `honeypot` boolean, required
          - `complainer` boolean, required: 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` boolean, required
        - `domain_info` object: 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.
          - `domain` string
          - `provider_type` string
          - `auth_score` integer
          - `auth_grade` string
          - `has_spf` boolean
          - `spf_policy` string, nullable
          - `has_dmarc` boolean
          - `dmarc_policy` string, nullable
          - `has_dkim` boolean
        - `ai` object: 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.
          - `typo_suggestion` string, nullable: 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` number, nullable: How confident the correction is, when one was made.
          - `typo_method` string, nullable: 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` number, nullable: 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.
        - `suggestion` string: 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. The 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` number
        - `credits_remaining` number
        - `processing_time_ms` integer
        - `verified_at` string, date-time
        - `from_cache` boolean
        - `request_id` string
        - `idempotent_replay` boolean: `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` ActivityData
          - `level` 'active' | 'inactive' | 'dormant' | 'unknown', required: 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` number, required
          - `last_activity_at` string, date-time, nullable, required
          - `engagement` object, required
            - `open_count` integer, required
            - `click_count` integer, required
            - `reply_count` integer, required: 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.
          - `deliverability` object, required
            - `bounce_count` integer, nullable, required
            - `complaint_count` integer, nullable, required
          - `confidence` number, required: 0 means your account has recorded no activity for this address, not that it is inactive.
          - `data_points` integer, required
          - `message` string: Present only when no activity data is available for a single-address lookup.
        - `domain_reputation` object, nullable: 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` object, nullable: 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.
          - `first_name` string, nullable
          - `last_name` string, nullable
          - `full_name` string, nullable
          - `gender` string, nullable
          - `country` string, nullable
          - `country_code` string, nullable
        - `cache_age_seconds` integer: 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` integer: 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.
      - `next_key` string, nullable: Cursor for the next page of results.
      - `has_more` boolean
      - `credits_used` number
      - `credits_refunded` number
      - `billing_status` string
      - `created_at` string, date-time
      - `updated_at` string, date-time
      - `completed_at` string, date-time, nullable: 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` string, date-time, nullable: When `POST /v1/jobs/{id}/cancel` stopped the job. Absent on every other job.
      - `error_message` string, nullable: 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.
    - `limit` integer: The page size that was applied.
    - `total` integer: Every job matching the listing, not just this page.
    - `total_is_approximate` boolean: True when the count stopped at its scan bound, so `total` is a floor.
    - `has_more` boolean, required
    - `next_offset` string, nullable

## Other responses

- `400`: The request is malformed, or names a field this endpoint does not accept. Retrying it unchanged will fail identically.
- `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.
- `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)
