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 the sub-account and usage routes; one that had it and still owns sub-accounts keeps the few routes that wind them down (below). The custom domain routes answer by their own rules, under Custom domain API. 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.

Custom API domain

Serve the API from a host you own, such as api.yourdomain.com, so the integrations you and your customers build call your domain instead of api.kawaa.com. It is part of white-label.

Custom API domains are not available yet. No account can add one: the Custom domain card in the dashboard, under Settings, then White-label, says so, and the API below refuses to add one. Your API keeps working at https://api.kawaa.com/v1. The rest of this section describes how a custom domain is set up and runs once they are available.

Set it up

  1. Add the host. Use a subdomain you control, such as api.yourdomain.com. A domain's root name (yourdomain.com) cannot be used, because the certificate and routing records below are CNAME records. Each account has one custom domain, and a host can belong to only one Kawaa account.
  2. Create the ownership record. A TXT record named _kawaa-challenge.your-host with a value only your setup is given. It shows that you control the domain: nothing is routed until Kawaa finds it, and each new setup of a host gets a new value. You can delete it once the domain is set up.
  3. Create the certificate validation record. Kawaa requests a TLS certificate for your host from Amazon's certificate authority and shows a CNAME record whose name starts with an underscore. Create it at your DNS provider exactly as shown. The certificate is usually issued within 30 minutes of the record becoming visible. Keep the record in place afterwards: it is also how the certificate renews. If your domain has CAA records, they must allow amazon.com.
  4. Create the routing record. Once the certificate is issued and the ownership record found, Kawaa sets up your domain and shows a second CNAME: your host, pointing at an address that ends in execute-api.us-east-1.amazonaws.com. Turn off any proxy for this record (in Cloudflare, set it to DNS only): Kawaa serves the certificate for your host, so a proxy in front of it breaks the connection.
  5. Check the status. Select Check status, or wait: Kawaa also checks every 15 minutes. The domain is Active once https://your-host/v1/health answers as the Kawaa API. From then on, use https://your-host/v1 as your base URL.

Some DNS providers add your domain to a record name automatically. If yours does, enter only the part of the name before your domain.

Statuses

  • Requesting certificate: the validation record appears within a minute.
  • Waiting for validation record: create the ownership and certificate validation records. The card shows whether public DNS shows them yet.
  • Certificate issued: Kawaa is setting up your domain, once it has found the ownership record.
  • Waiting for routing record: create the routing record. The card shows whether public DNS shows it, and why the host did not answer at the last check.
  • Active: your host serves the API.
  • Suspending: white-label is not active on your account, and Kawaa is stopping requests to your host.
  • Suspended: white-label is not active on your account, so requests to your host are not routed (see below).
  • Failed: the certificate could not be issued or the host could not be set up, for the reason shown, for example CAA records that do not allow Amazon, or a validation record that was not found within 72 hours. Remove the domain, fix the cause, and add it again. A failed domain is removed automatically after 30 days, or 72 hours after it was added if its ownership record was never found, so it never holds your host for long.
  • Removing: removal has started and finishes on its own, usually within 15 minutes.

What works on your domain

  • Every API route you call with an API key works on your domain exactly as on api.kawaa.com, with the same keys, rate limits and credits. https://api.kawaa.com/v1 keeps working too.
  • Sign-in for connected apps and agents (OAuth) and the MCP server stay on api.kawaa.com: their discovery documents name that host, and clients check it.
  • Responses carry the same security headers, including Strict-Transport-Security: browsers remember for a year that your host, and names under it, are reached over HTTPS only.

Removing it, and when white-label ends

  • Remove stops routing your host and deletes its certificate, usually at once and otherwise within about 15 minutes. If a removal says it could not be completed on Kawaa's side, it stays unfinished until Kawaa fixes the cause: contact support. Once the domain is gone you can delete its DNS records. The same host can be added again a few minutes after it is removed, with a new ownership record.
  • If your account loses white-label access, requests to your domain stop being routed and it shows Suspended. The certificate and your DNS records are kept, and routing resumes on its own when white-label is active again. You can still remove the domain in the meantime.
  • When a Kawaa account is deleted, its custom domain and certificate are removed.
  • An account can add a custom domain at most 5 times in 30 days, since each one requests a new certificate.
  • A host can be set up by one Kawaa account at a time. A setup whose ownership record is not found within 72 hours of adding the domain is removed, whatever the state of the account, so the host is free again.

Custom domain API

The dashboard card calls these routes, and you can call them yourself. Each answers { available, domain }: available says whether custom domains are open to your account. It needs white-label access, and while custom domains are not available it is false unless your account already has a domain set up. Once they are, an add can still be refused, for example after 5 in 30 days. domain is your domain, or null when there is none. In domain, status is one of the statuses above, message says what is happening and what to do next, and dns_records lists the records to create, each with its purpose (ownership, certificate_validation or routing) and a check of what public DNS showed (found, missing, mismatch or not_checked). Reading needs the white_label:read scope, and the other three routes white_label:write. Reading and removing work whatever your plan. Adding answers 503 WHITE_LABEL_CUSTOM_DOMAIN_UNAVAILABLE while custom domains are not available to your account, and 403 PLAN_UPGRADE_REQUIRED without white-label. Checking answers 404 when you have no domain, reports a failed domain or finishes a removal whatever your plan, and otherwise answers as adding does, except that a domain already set up can still be checked while new setups are closed.

Get your custom domain

GET/v1/white-label/custom-domain

Your domain as Kawaa last recorded it. An account without white-label access gets available: false here rather than an error. It looks nothing up, so a record's check here is what Kawaa already knows, not a fresh lookup: usually not_checked, found for the routing record of an active domain, and missing for a validation record the certificate authority last reported it could not renew with. The check route returns fresh results.

Response
{
  "success": true,
  "data": {
    "available": true,
    "domain": {
      "domain": "api.acme-corp.io",
      "display_domain": "api.acme-corp.io",
      "status": "active",
      "message": "api.acme-corp.io serves the Kawaa API. Use https://api.acme-corp.io/v1 as your API base URL. Keep both DNS records in place.",
      "failure_reason": null,
      "dns_records": [
        {
          "purpose": "certificate_validation",
          "type": "CNAME",
          "name": "_3a7f9c1e5b2d8f4a6c0e2b4d6f8a1c3e.api.acme-corp.io",
          "value": "_9b1d3f5a7c9e1b3d5f7a9c1e3b5d7f9a.xlfgrmvvlj.acm-validations.aws",
          "check": "not_checked"
        },
        {
          "purpose": "routing",
          "type": "CNAME",
          "name": "api.acme-corp.io",
          "value": "d-4k7m2p9q1x.execute-api.us-east-1.amazonaws.com",
          "check": "found"
        }
      ],
      "api_base_url": "https://api.acme-corp.io/v1",
      "created_at": "2026-10-12T09:00:00.000Z",
      "updated_at": "2026-10-12T09:41:12.000Z",
      "last_checked_at": "2026-10-12T09:41:12.000Z",
      "active_since": "2026-10-12T09:41:12.000Z"
    }
  }
}

Add a custom domain

POST/v1/white-label/custom-domain

The body is {"domain": "api.yourdomain.com"} and nothing else. Kawaa reserves the host for your account and requests its certificate, and answers 201 with the records to create: the ownership record at once, and the certificate validation record as soon as the certificate authority has made it, usually in the same answer. If the answer has only the ownership record (status requesting_certificate, or pending_validation without the validation record), check again in a minute to get the other one. While custom domains are open to your account, sending the same host again, for example after a response that never arrived, answers 200 with the domain as it stands and requests nothing new. A host another account has set up answers 409, as does a second domain on the same account: remove the first one before adding another.

201 Created
{
  "success": true,
  "data": {
    "available": true,
    "domain": {
      "domain": "api.acme-corp.io",
      "display_domain": "api.acme-corp.io",
      "status": "pending_validation",
      "message": "Add the ownership record and the certificate validation record below at your DNS provider. The certificate is usually issued within 30 minutes of the record becoming visible. Keep the certificate validation record in place afterwards: it also renews the certificate. If the ownership record is not found within 72 hours of adding the domain, the domain is removed.",
      "failure_reason": null,
      "dns_records": [
        {
          "purpose": "ownership",
          "type": "TXT",
          "name": "_kawaa-challenge.api.acme-corp.io",
          "value": "kawaa-domain-verification=6f1c0a9e4b7d2c8a5e3f1b9d7c5a3e1f",
          "check": "not_checked"
        },
        {
          "purpose": "certificate_validation",
          "type": "CNAME",
          "name": "_3a7f9c1e5b2d8f4a6c0e2b4d6f8a1c3e.api.acme-corp.io",
          "value": "_9b1d3f5a7c9e1b3d5f7a9c1e3b5d7f9a.xlfgrmvvlj.acm-validations.aws",
          "check": "not_checked"
        }
      ],
      "api_base_url": null,
      "created_at": "2026-10-12T09:00:00.000Z",
      "updated_at": "2026-10-12T09:00:03.000Z",
      "last_checked_at": "2026-10-12T09:00:03.000Z",
      "active_since": null
    }
  }
}

Check your custom domain

POST/v1/white-label/custom-domain/verify

Looks your records up in public DNS, asks the certificate authority, and takes the domain as far as it can go, then answers with it. This is what Check status in the dashboard does, and Kawaa also does it every 15 minutes. The route reads no request body. If a step could not finish, for example because a service was briefly unreachable, it answers 503 and your domain stays where it was: try again in a minute. For a domain being removed, a check carries the removal on instead and answers with it: 200 with removing while it is not finished, domain: null once it is, or the same 503 as removing does when Kawaa is blocked on its side.

Remove your custom domain

DELETE/v1/white-label/custom-domain

Stops routing your host and deletes its certificate. It answers 200 with domain: null once everything is gone, or 202 with the domain at removing when a step has not finished yet (the certificate authority can need a few minutes, or a service was briefly unreachable). The removal then finishes on its own, usually within 15 minutes, and until it does your host may still answer. If Kawaa is blocked on its side, it answers 503 WHITE_LABEL_CUSTOM_DOMAIN_UNAVAILABLE: the removal has started but is not finished, so contact support if it stays that way. You can remove a domain whatever your plan. The route reads no request body.

Response
{
  "success": true,
  "data": {
    "available": true,
    "domain": null
  }
}

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). The custom domain routes answer it in the cases Custom domain API describes.
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. For a custom domain: checking or removing one when your account has none.
409CONFLICTFor a custom domain: a host another Kawaa account has set up, a second custom domain on your account, or a domain that changed or was removed while the request ran. For a sub-account: it 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.
429RATE_LIMITEDA custom domain added more than 5 times in 30 days. Retry-After says when the next one is possible.
503WHITE_LABEL_CUSTOM_DOMAIN_UNAVAILABLECustom domains are not available to your account yet, or a custom domain step could not be completed on Kawaa’s side. Your domain is kept where it was.
503SERVICE_UNAVAILABLEA custom domain check or DNS lookup could not finish because a service was briefly unreachable. Try again in a minute.