Skip to main content

API reference

White-label sub-accounts

Give each of your customers their own sub-account, with its own API key and a credit pool you fund from your balance. Send your own API key in the X-Api-Key header on every request.

Before you start

The white-label API is part of the Enterprise plan. An account that has never had white-label gets 403 PLAN_UPGRADE_REQUIRED from every route on this page; one that had it and still owns sub-accounts keeps the few routes that wind them down (below). Sub-accounts are managed through these routes only: the dashboard's Settings → White-label page sets your branding, not your sub-accounts.

Sub-accounts depend on your white-label access. If your account loses it (for example when an Enterprise subscription ends), every sub-account's key is refused with 401 until you have it again, and then works unchanged, with no new keys to hand out. While it is off you can still list and read your sub-accounts, suspend one, and delete one that holds no credits. Nothing moves credits in that time: allocating, reclaiming and deleting a sub-account that still holds credits answer 403 PLAN_UPGRADE_REQUIRED, and its credits stay with it until access is restored.

  • A sub-account's key works like any Kawaa API key: send it in X-Api-Key to the verification endpoints, and its requests spend the sub-account's credits, never yours. The same key signs the sub-account into the dashboard with the sign-in page's API key option.
  • Reading sub-accounts needs the white_label:read scope; changing, deleting and funding them needs white_label:write. The usage routes need white_label:read and analytics:read. Creating a sub-account and resetting its key both return a new API key, so they need an unrestricted key or a signed-in session; a scoped key gets 403 INSUFFICIENT_SCOPE.
  • Each change you make here is in your account's audit log: creating, changing or deleting a sub-account, allocating or reclaiming credits, resetting a sub-account's key, and changing your white-label settings. An entry names the sub-account by its ID and the API key, session or connected app that made the change, never a sub-account's API key or email address. A key reset is also in the sub-account's own audit log, which names your account only as the account that manages it.
  • You can have up to 100 sub-accounts at a time unless your account has a different limit. The limit holds for creates sent in parallel too: past it, each one answers 400 and nothing is created or charged.
  • A sub-account gets what your plan includes and never more: the same features, batch size, result retention and other limits as your own account, following your plan when it changes. Its requests per minute (its rate_limit times your rate-limit multiplier) stop at your own plan's limit, and a rate_limit above what fits answers 400 with the most you can set.

How sub-account credits work

  • Credits come out of your balance. Allocating credits to a sub-account, when you create it or later with Allocate credits, moves them from your own balance into the sub-account's pool. Reclaiming moves unused credits back, and deleting a sub-account returns whatever is left in its pool.
  • Allocations count as credits you used. Credits you move into a sub-account count toward your own used credits (credits_used) for your current billing period, the same as credits you spend yourself. Reclaiming them or deleting the sub-account takes them back off, up to what you moved in during that period.
  • Every move is on both statements. Each allocation, reclaim and returned balance is a transfer row in credit transactions (GET /v1/usage/credits/transactions): on yours, naming the sub-account's email with its ID in reference_id; on the sub-account's own, read with its key. Both rows are written in the same step as the two balances, each with the balance_after it left, so either ledger adds up to its balance. ?type=transfer lists only these.
  • The pool never resets. A sub-account has no monthly allowance of its own. When its balance runs out, its verification requests are refused with 402 INSUFFICIENT_CREDITS until you allocate more.
  • You are told before it runs out. When a sub-account's balance falls to 20% of the credits allocated to it, and again when it drops below 1 credit (the price of a fresh check), we email your account address (if your low-credit and credits-exhausted email notifications are on) and send credits.low or credits.exhausted to your registered webhooks, with "account_type": "sub_account" and the sub-account's ID and name in the payload.

Create a sub-account

POST/v1/white-label/sub-accounts

Request body

ParameterTypeRequiredDescription
emailstringRequiredThe sub-account's email address, up to 254 characters. Stored lower-cased, and must not already belong to another of your active sub-accounts. Sub-accounts of other Kawaa accounts do not count, so a customer can have a sub-account with more than one reseller.
namestringRequiredA display name, up to 200 characters, stored with surrounding spaces removed; one that is blank once trimmed answers 400. It also names the sub-account in the credit emails you receive.
credits_allocatedintegerRequiredCredits to move from your balance into the new sub-account. 0 is allowed: create it empty and fund it later.
rate_limitintegerOptionalRequests per minute for this sub-account, a whole number of at least 1. Your account’s white-label rate-limit multiplier is applied on top, and the result cannot go over your own plan’s limit, so the most you can set is your own plan’s requests-per-minute limit divided by your multiplier, rounded down. A higher value answers 400 with the most you can set. Defaults to 100, or to the most you can set when that is lower. It applies to the sub-account’s dashboard sessions as well as its API key.

Any other field answers 400 and creates nothing, so a misspelled rate_limit cannot quietly fall back to the default.

Request
curl -X POST https://api.kawaa.com/v1/white-label/sub-accounts \
  -H "X-Api-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "ops@northwind.example",
    "name": "Northwind Retail EU",
    "credits_allocated": 5000
  }'
201 Created
{
  "success": true,
  "data": {
    "sub_account": {
      "sub_account_id": "sub_3f9a1c7e5b2d8a64",
      "email": "ops@northwind.example",
      "name": "Northwind Retail EU",
      "api_key": "ev_aB3dE5fG7hJ9kL1mN3pQ5rS7tU9vW1xY",
      "credits_allocated": 5000,
      "credits_balance": 5000,
      "rate_limit": 100,
      "status": "active",
      "created_at": "2026-09-21T10:15:30.412Z"
    },
    "warning": "Save the API key now. You will not be able to see it again."
  }
}

The API key is shown once. Store it before you do anything else. If it is lost, reset the key.

If your balance is smaller than credits_allocated, nothing is created and the request answers 402 INSUFFICIENT_CREDITS.

List sub-accounts

GET/v1/white-label/sub-accounts

Every sub-account you have not deleted, suspended ones included.

Response
{
  "success": true,
  "data": {
    "sub_accounts": [
      {
        "sub_account_id": "sub_3f9a1c7e5b2d8a64",
        "email": "ops@northwind.example",
        "name": "Northwind Retail EU",
        "status": "active",
        "credits_allocated": 5000,
        "credits_used": 4210,
        "credits_balance": 790,
        "rate_limit": 100,
        "created_at": "2026-09-21T10:15:30.412Z",
        "updated_at": "2026-09-21T10:15:30.412Z"
      }
    ],
    "total": 1
  }
}

credits_allocated is the running total you have moved in, less anything you reclaimed. credits_balance is what the sub-account can still spend, and credits_used is what it has spent.

Get a sub-account

GET/v1/white-label/sub-accounts/{subAccountId}

The same fields as the list, plus last_reset_at (when the sub-account was created).

Update a sub-account

PUT/v1/white-label/sub-accounts/{subAccountId}

Send at least one of these fields. Credits never move through this route; use Allocate credits.

ParameterTypeRequiredDescription
namestringOptionalA new display name. Must not be empty.
statusstringOptionalactive or suspended. A suspended sub-account's key is refused with 401, and credits cannot be allocated to it or reclaimed from it until it is active again.
rate_limitintegerOptionalRequests per minute, a whole number of at least 1 and at most your own plan’s requests-per-minute limit divided by your multiplier, rounded down. A higher value answers 400 with the most you can set. Sending back the value the sub-account already has is never refused.

The read-only fields a GET returns (email, the credit fields, timestamps) are accepted and ignored, so you can send back an object you read. Any other field answers 400 and changes nothing. A sub-account deleted while the update was in flight answers 404 and stays deleted.

Request
curl -X PUT https://api.kawaa.com/v1/white-label/sub-accounts/sub_3f9a1c7e5b2d8a64 \
  -H "X-Api-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"status": "suspended"}'
Response
{
  "success": true,
  "data": {
    "sub_account": {
      "sub_account_id": "sub_3f9a1c7e5b2d8a64",
      "email": "ops@northwind.example",
      "name": "Northwind Retail EU",
      "status": "suspended",
      "credits_allocated": 5000,
      "credits_used": 4210,
      "credits_balance": 790,
      "rate_limit": 100,
      "updated_at": "2026-09-21T11:02:08.905Z"
    },
    "message": "Sub-account updated"
  }
}

Allocate credits

POST/v1/white-label/sub-accounts/{subAccountId}/allocate

Move credits from your balance into an active sub-account's pool. The credits leave your balance and arrive in the sub-account in the same step, so neither side can change without the other. A negative number moves unused credits the other way, from the sub-account back to your balance.

ParameterTypeRequiredDescription
creditsnumberRequiredA positive whole number allocates that many credits from your balance. A negative number reclaims that many from the sub-account, and may be fractional in steps of 0.25 (-0.5, -2.25): a cached verification costs 0.5 credits, so what a sub-account has left often is not whole. 0, a fractional allocation and a finer reclaim answer 400, as does any other field.
Request
curl -X POST https://api.kawaa.com/v1/white-label/sub-accounts/sub_3f9a1c7e5b2d8a64/allocate \
  -H "X-Api-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"credits": 2000}'
Response
{
  "success": true,
  "data": {
    "sub_account": {
      "sub_account_id": "sub_3f9a1c7e5b2d8a64",
      "credits_allocated": 7000,
      "credits_balance": 2790
    },
    "message": "Allocated 2000 credits"
  }
}

A reclaim answers "message": "Reclaimed 500 credits" and lowers both credits_allocated and credits_balance. You cannot reclaim more than the sub-account's current balance; to empty it, reclaim exactly that, or delete the sub-account.

Allocation is not idempotent. This route does not read an Idempotency-Key header, so repeating a request moves the credits again. If a request times out, get the sub-account and check credits_balance before you retry.

  • Your balance smaller than credits: 402 INSUFFICIENT_CREDITS, and nothing moves.
  • A suspended sub-account: 400. Set it back to active first, or delete it to get its balance back.
  • Suspended or deleted while the allocation ran: 409 CONFLICT, and your balance is untouched.

Reset a sub-account's key

POST/v1/white-label/sub-accounts/{subAccountId}/reset-key

Revoke every active key the sub-account has and issue a new one. Requests with the old key stop working straight away. Needs an unrestricted key or a signed-in session. One reset runs at a time for each sub-account: a second request sent while the first is still running (a retry of a slow response, say) answers 409 CONFLICT and changes nothing. Retry it once the first has finished, and use the key the latest successful reset returned. The route reads no request body; one carrying any field answers 400 and resets nothing.

Response
{
  "success": true,
  "data": {
    "sub_account_id": "sub_3f9a1c7e5b2d8a64",
    "api_key": "ev_Zx8Wv6Ut4Sr2Qp0On8Ml6Kj4Ih2Gf0Ed",
    "warning": "Save this API key now. You will not be able to see it again."
  }
}

Delete a sub-account

DELETE/v1/white-label/sub-accounts/{subAccountId}

Close the sub-account and return its unused credits to your balance in the same step. Its key stops working, and its email address can be used for a new sub-account. A refund that comes later for work it was charged for before the delete, such as a domain search that finishes with some addresses unchecked, is added to your balance too. The route reads no request body; one carrying any field answers 400 and deletes nothing.

Response
{
  "success": true,
  "data": {
    "message": "Sub-account deleted",
    "sub_account_id": "sub_3f9a1c7e5b2d8a64",
    "credits_returned": 790
  }
}

Sub-account usage

GET/v1/white-label/usage

Current credit figures for every sub-account, with totals. GET /v1/white-label/analytics returns the same thing. These are balances right now, not a time series, so a period query parameter answers 400.

Response
{
  "success": true,
  "data": {
    "summary": {
      "total_sub_accounts": 1,
      "total_credits_allocated": 7000,
      "total_credits_used": 4210,
      "total_credits_balance": 2790
    },
    "sub_accounts": [
      {
        "sub_account_id": "sub_3f9a1c7e5b2d8a64",
        "name": "Northwind Retail EU",
        "email": "ops@northwind.example",
        "credits_allocated": 7000,
        "credits_used": 4210,
        "credits_balance": 2790,
        "usage_percentage": 60
      }
    ]
  }
}

One sub-account

GET/v1/white-label/usage/{subAccountId}

The same figures for one sub-account, as top-level fields, plus last_reset_at. usage_percentage is credits_used as a share of credits_allocated, rounded to a whole number.

Errors

Errors use the standard envelope described in Error codes. The ones these routes return:

StatusCodeWhen
400BAD_REQUESTA missing or invalid field, a field the route does not read (the message names the ones it does), a blank name, an ID that does not start with sub_, an email one of your other active sub-accounts already uses, the sub-account limit reached, an allocation to a suspended sub-account, or a reclaim larger than its balance.
401UNAUTHORIZEDThe API key is missing or not valid, including the key of a suspended or deleted sub-account, or of any sub-account while your account has no white-label access. A key revoked by you, or one suspended because a subscription ended, carries a more specific code such as API_KEY_REVOKED.
402INSUFFICIENT_CREDITSYour own balance is smaller than the credits you asked to move into a sub-account.
403PLAN_UPGRADE_REQUIREDWhite-label is not enabled on your account, for any route other than the wind-down routes an account that still owns sub-accounts keeps (see Before you start).
403SUB_ACCOUNT_MANAGEDThe request was made with a sub-account’s own key or session. A sub-account cannot manage sub-accounts or branding; every route on this page answers it this way.
403INSUFFICIENT_SCOPEA scoped API key without the scope the route needs, or any scoped key on a route that returns a new sub-account key.
404NOT_FOUNDNo sub-account of yours has that ID: there is none, it was deleted, or it belongs to another account. All three get this same answer, so an ID says nothing about sub-accounts that are not yours.
409CONFLICTThe sub-account changed while the request ran (it was suspended or deleted during an allocation), a balance kept changing while credits were being moved (verifications on your account or the sub-account, several times over), or another key reset for it is still running. Nothing was moved. Read it again and retry.