---
title: "Find a business address"
method: POST
path: "/v1/find"
tags: ["Search"]
---

# Find a business address

`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`)

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.

## Headers

- `Idempotency-Key` string

## Request body

- FindRequest
  - `first_name` string, 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_name` string, 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.
  - `domain` string, 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 `200`

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

- object
  - `success` unknown, required
  - `data` object, required
    - `first_name` string, required
    - `last_name` string, required
    - `email` string, email, nullable, required: The address settled on, or `null`. Null is NOT the same as 'no address exists': on a catch-all domain nothing could be confirmed either way. And an address is not the same as a find: with `verified: false` it is a best guess, not an address confirmed as this person's.
    - `confidence` number, required
    - `pattern` string, nullable: The pattern `email` was built from, e.g. `first.last`. It says something about the domain only when `verified` is true. On a best guess (`verified: false`) it is just the pattern of the candidate the finder ranked highest, not one the domain was seen to use. `null` when there is no `email`.
    - `verified` boolean: True only on a `valid` verdict: the mail server accepted this mailbox, and the domain does not accept mail for every address. False on every other status, including those that still return an `email`: on a catch-all result the server accepts everything, so nothing was confirmed; on `unknown` the check could not conclude; on `risky` the address may receive mail but risk signals or incomplete proof remain; a `role` address reaches a shared mailbox (the server may have accepted it), so it is not confirmed as this person's. An `email` with `verified: false` is a best guess for this person.
    - `status` string, required: The verdict on the address returned: `valid`, `risky` (the mail server showed the mailbox exists, with risk signals), `role`, `catch_all` (inconclusive rather than negative), or `not_found`. An inconclusive check is never a status here: it is a `503`.
    - `message` string
    - `credits_used` number, required: 5 for a search. Still 5 on a de-duplicated repeat; read `idempotent_replay` to tell a second charge from a free one.
    - `credits_remaining` number
    - `idempotent_replay` boolean: Present and true when this repeated a search already charged for, and therefore charged nothing. With an `Idempotency-Key`, the repeat replays the stored result during the plan's retention period; the receipt stays at least 30 days and refuses an expired result with 410. Without a key, an identical search is still settled on the original charge inside a fixed two-minute bucket.

## Other responses

- `400`: The request cannot be searched, and nothing was charged. A malformed request or a field this endpoint does not accept; a name with no a-z letter, with a letter that has no single a-z spelling, or longer than 64 characters or letters once spelled; a special-use or RFC 2606 domain; a domain that cannot receive mail (it does not exist in DNS, publishes a null MX, or has no MX record and no A or AAAA address); or a public mailbox provider. The message says which, and `nothing_charged` is true. 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).
- `402`: The account cannot pay for this request. The message carries the numbers and where to top up. Nothing was charged.
- `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.
- `409`: One of two codes, and this answer adds no charge in either case. `IDEMPOTENCY_KEY_CONFLICT`: the `Idempotency-Key` was used before for a different search on this route. A repeat whose names spell the same in a-z letters (differing only in case, accents, letters such as ø or ß, punctuation or spaces), at the same domain apart from case or a leading `www.`, is the same search and is replayed, not refused. Repeat the original request, or use a new key. `CONFLICT` with `retryable: true`: a copy of this same request under the same key was charged and is still searching. Wait, then repeat it with the SAME key, which replays that search's stored result (`idempotent_replay: true`) without charging again. A new key buys the search a second time. The bulk finder answers the same state the same way.
- `410`: `IDEMPOTENT_JOB_EXPIRED`, not retryable. The prior job for this `Idempotency-Key` has reached the end of the data-retention period of the plan it was submitted under (7 days on Free) while the key's receipt, kept at least 30 days, still stands. The job cannot be replayed or restarted from that receipt. This retry was not charged. Send a new key to run the request again.
- `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.
- `500`: The search failed in a way the API could not classify. This answer covers a failure in the cached pattern lookup, the search itself or the credit debit, so **the billing state is genuinely unknown**: an exception during the debit can leave the charge applied with no answer to show for it. Retry the SAME search with the SAME `Idempotency-Key`: a repeat under it replays the stored result, or finishes the paid search, without charging again. The receipt lasts at least 30 days; after the result expires, a repeat returns 410 without another charge. Without a key, an identical repeat is settled on the original charge only inside a fixed two-minute bucket, and the answer carries `idempotent_replay: true` when it was. Check `GET /v1/usage` before waiting.
- `503`: The domain's mail servers could not be looked up (nothing was searched or charged; retry after `retry_after_seconds`), the credit debit could not be completed because the balance was being changed concurrently, or the search produced no usable candidate and some generated addresses remained unanswered or answered only inconclusively (a policy block, greylisting, a temporary failure). Even if other candidates were rejected, this incomplete search is not a `not_found`. The debit refusal charges nothing; an incomplete keyed search is refunded. If that refund itself fails, the message says the charge is still unsettled, and a retry under the same `Idempotency-Key` finishes the paid search without charging again. Treat the request as uncharged only when the response confirms it. Retry; a refusal for lack of credits is `402`, which is permanent until the balance changes.
- `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)
