Search

Find a business address

Works out a person's work address from their name and their company's mail domain, and checks the candidates with the domain's mail server. Charges 5 credits.

Only verified: true is a confirmed address. It is set on a valid verdict alone: the mail server accepted the mailbox, and the domain does not accept mail for every address. A result with verified: false can still carry an email (catch_all, risky, role): that is the best guess for this person, the candidate the finder ranked highest. On catch_all the domain accepts every address, so nothing was confirmed, and the message says whether the guess follows a pattern an address at this domain was confirmed with before or is only the most common pattern; on risky the server showed the mailbox exists (it accepted it, or reported it full), but risk signals remain. A role address (such as info@) can be a mailbox the server accepted, but it reaches a shared inbox, so it is not confirmed as this person's. Do not count it as found.

Needs the Starter plan or higher. On the Free plan the request is refused with 403 PLAN_UPGRADE_REQUIRED before anything is charged.

status is the verdict on the address it settled on: valid, risky, role, catch_all or not_found. unknown is no longer returned (an inconclusive check is the 503 below), nor is invalid_name (an unusable name is a 400). A catch_all outcome still charges, and it is INCONCLUSIVE rather than negative: the domain accepts every recipient, so no address could be confirmed either way. It may come back with a best-guess email (unverified) or with email: null (both shapes occur), so branch on status, never on whether email is present.

A name over 64 characters is a 400 before the charge: RFC 5321 caps a local part at 64 octets, so no candidate could exist. So is a name that spells to more than 64 letters in a-z (ß is written ss).

Names are spelled in a-z before the candidates are built. Accents are dropped, and letters with no accent to drop are written the usual way: ø as o, æ as ae, ß as ss, ł as l, đ and ð as d, þ as th, ı as i, œ as oe, so Søren is searched as soren. A name that also contains a letter with no single a-z spelling (such as ə, ʒ or ʃ, or a letter from another script next to Latin ones) is a 400 with nothing charged, rather than a search for a misspelled name.

A catch_all status does not guarantee an address. When the domain accepts every recipient and no candidate clears the confidence threshold, the paid result comes back with email: null and confidence: 0: you have bought the search and learned that no address can be confirmed.

A special-use domain is a 400 before the charge. .example, .test, .invalid, .localhost, .local, .onion, .alt, .internal, .corp, .home, .mail, and home.arpa with the other special-use .arpa names are never answered for in the public DNS, so no address under them can exist and every candidate could only fail. So are RFC 2606's documentation domains example.com, example.net and example.org and any name under them: they resolve, but publish a null MX, and the worker refuses them before SMTP. They used to be searched and billed (5 credits) for a not_found that was certain before the search began.

A domain that cannot receive mail is a 400 before the charge. Its mail servers are read the way GET /v1/domain/{domain} reads them: a domain that does not exist, publishes a null MX (RFC 7505), or has neither an MX record nor an A or AAAA address is a 400 saying which, with nothing searched and nothing charged. A domain with only an address record receives mail there (RFC 5321's implicit MX) and is searched. A lookup that fails is a 503 with retry_after_seconds, also uncharged.

A public mailbox provider is a 400 before the charge. gmail.com, yahoo.com, outlook.com and the other domains POST /v1/verify reports free_provider: true for hold addresses their owners chose, not ones built from a name, so a person's address there cannot be found from their name: any address returned could belong to someone else.

Both refusals apply to new work only: a repeat under an Idempotency-Key whose earlier attempt was paid for is still answered from its receipt.

An incomplete negative search is a 503, not a not_found. When the search produces no usable candidate and some generated addresses remain unanswered (a probe timed out or the search budget ended), it cannot establish that the other addresses are absent. This includes searches where some candidates were rejected and others could not be checked, and probes that settled nothing about the mailbox: unknown, a policy block of the sending server, greylisting, a temporary or connection failure, or risky without evidence that the mailbox exists. Such a probe is never returned as the address. It answers a retryable 503 SERVICE_UNAVAILABLE and charges nothing; with an Idempotency-Key the charge is refunded, so a retry under the same key is a new paid search rather than a replay. A not_found is billed only when every generated candidate was answered.

post/v1/find

Request

  • Base URL: https://api.kawaa.com
  • URL: https://api.kawaa.com/v1/find
  • 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:write)

Headers

Idempotency-Keystring

Makes a charging call safe to retry: a repeat under the same key replays the original outcome instead of buying the work again. The window depends on the route: the receipt behind it expires, and after that the same key is a new paid request. It is 30 days for a deliverability check, at least 30 days for a single find, 24 hours for an activity lookup, and 15 minutes for a bulk find. What a replay returns also differs: a batch replays its JOB, an activity lookup replays its RESULT. A different request under the same key is 409 IDEMPOTENCY_KEY_CONFLICT, before any charge.

How long a key stays safe differs per route, and for the verification routes it depends on the plan. POST /v1/verify, /v1/verify/batch and /v1/verify/file keep their receipts for the job's data-retention period, never less than 30 days, so longer on plans that keep results longer. The job itself is kept only for the plan's data-retention period, which is 7 days on Free: a repeat that arrives after the job's retention period has ended but while its receipt is still live is refused with 410 IDEMPOTENT_JOB_EXPIRED and charged nothing, rather than bought a second time; send a new key to run it again. Once the receipt expires the same key claims a fresh target and creates another paid job. POST /v1/deliverability/check keeps its receipt 30 days. POST /v1/find keeps its receipt at least 30 days and its stored address only for the plan's retention period; a repeat after the address expires but before the receipt does return 410 IDEMPOTENT_JOB_EXPIRED without charging. The activity lookups keep receipts about 24 hours, and POST /v1/find/bulk 15 minutes. Without a key, GET /v1/activity/{email} and GET /v1/activity/domain/{domain} still answer an identical repeat from the same account free for 15 minutes after the lookup that was charged (POST /v1/find/bulk does the same for its content); a key buys a fresh read inside that window. An unkeyed POST /v1/verify that arrives while an identical one from the same account is still being verified (at most 15 minutes after its charge) joins it and is not charged; once that verification has its answer, a repeat is charged as usual. Only a key makes a later retry free.

Retry with the key you have rather than minting a new one. A key is not a refund (it prevents a second charge, it does not undo the first), but it stays good far longer than the shortest advertised retention suggests, and dropping one early is how a retry becomes a second paid job.

The characters are checked after trimming. Surrounding whitespace is trimmed from the header first; the resulting key must be 1-255 printable ASCII characters (0x21-0x7e) other than a comma, with no internal spaces or anything outside that range. Leading and trailing whitespace is accepted and is not part of the key. Because OpenAPI constraints apply to the raw header rather than the post-trim value, this schema deliberately has no minLength, maxLength, or pattern; those constraints would make generated clients reject values the API accepts. Invalid post-trim values get 400 before the route starts. A UUID is the obvious choice. Sending the header twice with different raw values is also a 400. A key may not contain a comma, because a header sent more than once reaches the API joined by commas; a comma is refused with the same 400, before anything is charged. A receipt claimed under a comma-bearing key before 2026-10-03 still replays for its window.

Request body

first_namestring required

Must contain at least one letter AFTER normalization, which spells the name in a-z: accents are dropped and letters such as ø, æ, ß, ł and þ are written o, ae, ss, l and th; then only a-z is kept. José, Ñuñez and Søren are fine, 李, 2024 and !! are not. A name that normalizes to nothing generates no candidate addresses, so the API answers 400 first_name and last_name must contain letters (a-z) and charges nothing. A name with a letter that has no single a-z spelling (such as ə or ʒ) next to letters that do is also a 400 that charges nothing, and so is one that spells to more than 64 letters. Deliberately not a pattern here: the check runs on the normalized form, and a regex over the raw string would refuse names this API accepts.

last_namestring required

Must contain at least one letter AFTER normalization, which spells the name in a-z: accents are dropped and letters such as ø, æ, ß, ł and þ are written o, ae, ss, l and th; then only a-z is kept. José, Ñuñez and Søren are fine, 李, 2024 and !! are not. A name that normalizes to nothing generates no candidate addresses, so the API answers 400 first_name and last_name must contain letters (a-z) and charges nothing. A name with a letter that has no single a-z spelling (such as ə or ʒ) next to letters that do is also a 400 that charges nothing, and so is one that spells to more than 64 letters. Deliberately not a pattern here: the check runs on the normalized form, and a regex over the raw string would refuse names this API accepts.

domainstring required

The company's mail domain, as a bare name. A special-use name (.example, .test, .invalid, .localhost, .local, .onion, .alt, .internal, .corp, .home, .mail, home.arpa and the other special-use .arpa names) and RFC 2606's example.com / .net / .org with any name under them are a 400 before the charge: no mailbox can exist there. So are a domain that cannot receive mail (no such domain, a null MX, or neither an MX nor an address record) and a public mailbox provider such as gmail.com.

Response

The search. Branch on status, not on whether email is present, and treat an address as found only when verified is true.

successunknown required

Example response

{
  "data": {
    "status": "valid"
  }
}

Changes

No recorded changes to this operation across all 1 revision of this API.