Skip to main content

API reference

Account & API keys

Manage your account information, credits, and API keys programmatically.

Get account info

GET/v1/account

Retrieve your account details, credit balance, and usage statistics.

Response
{
  "success": true,
  "data": {
    "account": {
      "user_id": "user_a1b2c3d4e5f60718293a4b5c",
      "email": "user@example.com",
      "name": "John Doe",
      "plan": "professional",
      "batch_size": 5000,
      "status": "active",
      "created_at": "2025-06-15T10:00:00Z",
      "is_admin": false,
      "email_delivery": {
        "deliverable": true
      }
    },
    "credits": {
      "balance": 24500,
      "used_this_month": 500,
      "monthly_allowance": 25000,
      "next_reset": "2026-03-01T00:00:00.000Z",
      "allowance_basis": "billing_period"
    },
    "usage": {
      "current_month": {
        "jobs_count": 12,
        "emails_verified": 4500,
        "emails_valid": 3800,
        "emails_invalid": 450,
        "emails_risky": 180,
        "emails_unknown": 70
      },
      "period": { "start": "2026-02-01", "end": "2026-03-01" },
      "coverage": {
        "requested_days": 3,
        "covered_days": 3,
        "covered_from": "2026-02-01T00:00:00.000Z",
        "covered_to": "2026-02-03T14:36:00.000Z",
        "retention_days": 90,
        "complete": true,
        "reason": null
      }
    },
    "api_keys": [
      {
        "key_id": "key_1a2b3c4d5e6f7a8b",
        "name": "Production",
        "status": "active",
        "created_at": "2025-06-15T10:00:00Z",
        "last_used_at": "2026-02-03T14:30:00Z",
        "key_preview": "ev_****cdEf",
        "scopes": null
      }
    ],
    "recent_jobs": [
      {
        "job_id": "9f8f6d2e-4c1b-4f6e-9a3b-2d1e5c7a8b90",
        "status": "completed",
        "total_emails": 1000,
        "processed_emails": 1000,
        "progress_percent": 100,
        "created_at": "2026-02-03T14:30:00Z",
        "completed_at": "2026-02-03T14:35:42Z"
      }
    ],
    "rate_limits": {
      "requests_per_minute": 300,
      "enforced": true
    }
  }
}

monthly_allowance reflects your plan's credit grant: 100 (Free), 5,000 (Starter), 25,000 (Professional), 100,000 (Business), 500,000 (Enterprise).

account.batch_size is the most addresses one batch or file job accepts for this account; a longer list is refused with 413 BATCH_SIZE_EXCEEDED before anything is charged. The response can also carry onboarding, the dashboard's setup progress, which is informational.

credits.next_reset is present only on a paid plan with a current billing period. It is the end of that period. Free accounts receive 100 credits at signup and another 100 each time a paid subscription ends and the account returns to Free, with no monthly renewal or billing period, so the key is absent there; usage.period then covers the calendar month.

usage.period is two UTC dates, and what end means depends on the account. With a billing period, start and end are the dates the period starts and ends, as in the example: end is the date of next_reset when there is one, and the period stops at that moment rather than at the end of that day. Without one (Free, a paid plan with no billing period on file, or a white-label sub-account), usage.period is the calendar month and end is its last day, included: 2026-02-01 to 2026-02-28. While a renewal is pending (see below), end is today's date.

next_reset is also absent when the plan is set to end at the close of the period. Nothing refills it then: the account moves to Free and the unspent allowance is removed. usage.period.end still says when the period ends.

For a short time after a renewal, until the renewal payment is recorded, used_this_month has not been reset: it holds the previous period's spend and anything spent since. While it does, usage.period and current_month cover the same stretch, from the last reset up to now, and next_reset is the moment the previous period ended: the reset is due and lands when the payment is recorded. That is so even if the plan has since been set to end, because that payment still refills the allowance.

credits.allowance_basis says whether the allowance renews. Only billing_period does, at next_reset. With one_time_grant (the Free plan) or no_billing_period (a paid plan with no billing period on file), nothing resets used_this_month, so it is a running total rather than this month's spend.

usage.coverage says how much of usage.period the current_month counts cover. They are counted from stored results, which your plan keeps for retention_days, so on a long period they can start after period.start. When they do, complete is false and reason is plan_retention.

Sections a key cannot read

A key limited to particular scopes can call this endpoint when it has account:read, but may not see every section. withheld names the sections its scopes do not cover: usage needs analytics:read, api_keys needs security:read and recent_jobs needs verification:read. A withheld list comes back empty, so an empty api_keys beside "api_keys": true does not mean the account has no keys. Withheld usage comes back as "current_month": null, not as zeros, and credits.used_this_month is left out. The profile, the balance and the allowance are always there. withheld is absent when the key can read everything.

Response to a key with only account:read
{
  "success": true,
  "data": {
    "account": {
      "user_id": "user_a1b2c3d4e5f60718293a4b5c",
      "email": "user@example.com",
      "name": "John Doe",
      "plan": "professional",
      "batch_size": 5000,
      "status": "active",
      "created_at": "2025-06-15T10:00:00Z",
      "is_admin": false,
      "email_delivery": {
        "deliverable": true
      }
    },
    "credits": {
      "balance": 24500,
      "monthly_allowance": 25000,
      "next_reset": "2026-03-01T00:00:00.000Z",
      "allowance_basis": "billing_period"
    },
    "withheld": { "usage": true, "api_keys": true, "recent_jobs": true },
    "usage": {
      "current_month": null,
      "period": { "start": "2026-02-01", "end": "2026-03-01" },
      "coverage": {
        "requested_days": 3,
        "covered_days": 3,
        "covered_from": "2026-02-01T00:00:00.000Z",
        "covered_to": "2026-02-03T14:36:00.000Z",
        "retention_days": 90,
        "complete": true,
        "reason": null
      }
    },
    "api_keys": [],
    "recent_jobs": [],
    "rate_limits": {
      "requests_per_minute": 300,
      "enforced": true
    }
  }
}

partial takes the same keys and names sections that could not be loaded this time, whatever the key's scopes. Their values are placeholders, not facts: the lists are empty and current_month is null. Ask again shortly to get them. partial is absent when everything loaded.

account.email_delivery reports whether Kawaa can currently email the account address. deliverable is always present; when it is false the object also carries reason (for example hard_bounce), since (ISO 8601) and a provider detail string. A suppressed address receives no job, credit or security emails until it is fixed in the dashboard:

email_delivery when undeliverable
"email_delivery": {
  "deliverable": false,
  "reason": "hard_bounce",
  "since": "2026-08-06T16:31:11.000Z",
  "detail": "OnAccountSuppressionList"
}

List API keys

GET/v1/api-keys

List all API keys associated with your account.

Response
{
  "success": true,
  "data": {
    "api_keys": [
      {
        "key_id": "key_9f8e7d6c5b4a3f2e",
        "name": "Agent",
        "status": "active",
        "created_at": "2025-07-01T12:00:00Z",
        "last_used_at": null,
        "key_preview": "ev_****Xy12",
        "scopes": ["verification:write", "verification:read", "account:read", "analytics:read"]
      },
      {
        "key_id": "key_1a2b3c4d5e6f7a8b",
        "name": "Production",
        "status": "active",
        "created_at": "2025-06-15T10:00:00Z",
        "last_used_at": "2026-02-03T14:30:00Z",
        "key_preview": "ev_****cdEf",
        "scopes": null
      }
    ],
    "available_scopes": [
      {
        "scope": "verification:read",
        "description": "Read verification results and jobs this account has already paid for. Spends nothing.",
        "sensitive": false
      },
      {
        "scope": "verification:write",
        "description": "Anything that spends verification credits: verifying an address, a batch or a file, finding a business email, and the domain and address-history lookups, which are charged too.",
        "sensitive": true
      }
    ],
    "recommended_agent_scopes": ["verification:write", "verification:read", "account:read", "analytics:read"]
  }
}

Keys are listed newest first. key_preview is ev_**** and the key's last four characters; an older key with no stored preview shows ev_•••• instead. A key's scopes is the list of permissions it is limited to, or null for a key that can do everything your account can. available_scopes lists every scope with what it allows and whether it is sensitive (it spends money, deletes data or changes who has access); the example shows only the first two. recommended_agent_scopes is a suggested set for an AI agent that verifies addresses and reads its own results.

Create API key

POST/v1/api-keys

Request body

ParameterTypeRequiredDescription
namestringRequiredA name to identify this key
scopesstring[]OptionalLimit the key to these permissions. Omit for a key that can do everything your account can: omit it, don't send null. GET /v1/api-keys lists every scope in available_scopes, and a suggested set for AI agents in recommended_agent_scopes.

Important: The full API key is only shown once in the response. Store it securely.

API keys use your account's access and plan limits and do not expire automatically. Creation accepts name and an optional scopes array; other fields, including permissions, expires_at and rate_limit, return HTTP 400. So do scopes: null and scopes: [], the first because omitting the field is how you ask for an unrestricted key and we will not guess, the second because it would issue a key that can call nothing. Revoke keys you no longer need.

201 Created
HTTP/1.1 201 Created

{
  "success": true,
  "data": {
    "api_key": {
      "key": "ev_aB3dE5fG7hJ9kL1mN3pQ5rS7tU9vW1xY",
      "key_id": "key_2b3c4d5e6f7a8b9c",
      "name": "Mobile App",
      "created_at": "2026-02-03T15:00:00Z",
      "scopes": null,
      "warning": "Save this API key now. You will not be able to see it again."
    }
  }
}

Revoke API key

DELETE/v1/api-keys/{key_id}

Permanently revoke an API key. This action cannot be undone.

Response
{
  "success": true,
  "data": {
    "message": "API key revoked successfully",
    "key_id": "key_2b3c4d5e6f7a8b9c"
  }
}

Credit balance

Monitor your credit usage to ensure uninterrupted service.

When the balance runs out, a paid call answers 402 INSUFFICIENT_CREDITS and charges nothing. Buy more under Buy credits in the dashboard's billing settings (https://app.kawaa.com/settings/billing/?tab=credits). The 402 message also names POST /v1/billing/purchase-credits, which starts the same purchase from code: send the package_id of a credit pack (the billing catalogue in the OpenAPI document lists them) with a key that holds billing:write, then open the checkout_url it returns in a browser to pay. Starting a purchase closes the account's earlier unpaid credit checkout pages, so normally only the newest page can be paid; a page already paid is never changed. If Stripe cannot be reached to close an earlier page, the new page still opens and the earlier one stays payable until it expires, an hour after it was opened, so do not treat an older checkout_url as closed. A white-label sub-account cannot buy credits: its 402 message says that the account that provisioned it allocates them, and every billing route answers it 403 SUB_ACCOUNT_MANAGED. Ask that account to allocate more (see the white-label API).

Credit consumption

1 credit:
Single email verification (cache miss)
0.5 credits:
Cached result (within 24 hours)
1 credit:
Each email in batch verification. A result served from the cache is billed 0.5, and the difference is refunded when the job finishes.
0.25 – 1 credit:
Email activity: 0.5 per address (GET /v1/activity/{email}), 0.25 per address in a batch lookup (POST /v1/activity/batch), 1 per domain (GET /v1/activity/domain/{domain}). Each is charged even when no activity is found.
5 credits:
A blacklist or DMARC check, whether run on its own, triggered on an existing monitor, or as the initial check that runs when you add a monitor. Scheduled re-checks on a monitor you already own are included.
5 credits:
Email finder: each search, and each different person with a usable name in a bulk search (a person listed more than once in one bulk search is searched and charged once). A search is not billed when it finds no usable address and some candidate addresses got no answer: a single search answers 503 and charges nothing (one sent with an Idempotency-Key is charged and then refunded), and a bulk search refunds that person.
10 credits:
A deliverability check. Repeating a check of the same domain and sending IP within 24 hours costs 0 credits and returns the earlier result with cached: true, unless you send refresh: true.
10 credits:
A domain search. A domain searched in full in the last 7 days costs 0 credits (a search that left some addresses unchecked is kept for 15 minutes), and polling a search in progress is free.
Free:
Job status checks, account info

Credit transactions

GET/v1/usage/credits/transactions

Your credit ledger, newest first: the charges and refunds on your balance, each with the balance it left. After a failure that carries retry_may_charge_again it shows whether a debit of that amount landed around that moment, which the balance alone cannot. A row carries no request id and no address, so that settles the question only when nothing else on the account was spending at the same time; on a route that accepts one, an Idempotency-Key is the reliable answer. Free; needs the analytics:read scope.

Entries before 2 September 2026 show usage and refunds only. Credits added before then, from a plan or a credit pack, have no row, so type=purchase is empty for that period even if you bought credits then. From that date every paid-plan allowance and credit-pack purchase has a purchase row; the one-time Free signup grant is the exception: it is credited to the balance directly and has no row. Subscription payments from before it are listed under Settings → Billing → Invoices.

Query parameters

ParameterDescription
limitRows per page, 1 to 100 (default: 50). Any other value, including 0, returns HTTP 400 rather than being clamped
offsetHow many rows to skip (default: 0)
typeOnly rows of this type: purchase, usage, refund, bonus, rollover, adjustment or transfer (credits moved between a white-label account and one of its sub-accounts). Any other non-empty value is a 400 naming those seven; an empty type= is the same as leaving it out and returns every row
cursorThe previous page's next_cursor, to read the page after it. Use it to page a long ledger: it reaches any depth, while an offset above 10,000 is a 400. Not combined with an offset; one that is not a cursor this route gave you is a 400 INVALID_CURSOR

Response

200 OK
{
  "success": true,
  "data": {
    "transactions": [
      {
        "id": "tx_2026-02-03T14:22:14.870Z_3f2c9a7e-1b4d-4c8a-9e6f-5a2b7c1d0e93",
        "transaction_id": "tx_2026-02-03T14:22:14.870Z_3f2c9a7e-1b4d-4c8a-9e6f-5a2b7c1d0e93",
        "type": "usage",
        "amount": -1,
        "balance_after": 4998,
        "description": "Email verification",
        "reference_id": null,
        "created_at": "2026-02-03T14:22:14.870Z"
      }
    ],
    "total": 1,
    "limit": 50,
    "offset": 0,
    "has_more": false,
    "next_cursor": null
  }
}

amount is negative for a debit and positive for credit coming back. id and transaction_id are the same value. A scoped key without all of verification:read, integrations:read, white_label:read and billing:read gets a generic description for each row's type, reference_id: null and "detail_withheld": true, because a description can name the address or list the credits were spent on.

has_more says whether another row follows this page, and next_cursor is where it starts (null on the last page). Pass it back as cursor to read the next page. Rows written while you page appear on a fresh first page, and a cursor walk does not repeat or skip the rows it pages through.