# API reference

> Every MobileValidate API endpoint, from the OpenAPI document. Interactive version (with the public sandbox key): https://mobilevalidate.com/docs/api-reference

Canonical: https://mobilevalidate.com/docs/api-reference

Base URL: `https://api.mobilevalidate.com/v1` · OpenAPI 1.0.0 document: https://mobilevalidate.com/openapi.yaml

## Lookups

### POST /lookup

Check up to 100 numbers and e-mail addresses

Runs every requested check (see GET /v1/services) for every identifier of the check's input type: phone checks for `numbers`, e-mail checks for `emails`. Result rows come back in order: numbers first, then e-mails. The call waits up to `wait` seconds (default 10) for all answers; if some are still running it answers `202` with `status: pending` and a `next.poll_url` (or use a webhook). Checks without real-time support are refused with 403 service_disabled (param `checks[i]`) — send them in a bulk job. E-mail checks answer only whether an account or mailbox exists — never names, photos or profiles.

- `Idempotency-Key` (header): Any unique string (1–255 printable ASCII) for safe retries: a retry with the same key and body within 24 hours returns the stored response (header Idempotent-Replayed: true) instead of running the request again.

Responses: 200 All checks completed.; 202 Some checks are still running; poll `next.poll_url` (the Location header) or wait for lookup.completed.; default Error. Branch on `error.code`; `suggestion` (optional) says how to fix the request.

### GET /lookups/{id}

Get a lookup

Returns a lookup created by POST /v1/lookup, optionally waiting up to `wait` seconds for pending checks. `input` is masked on later reads; `e164` and `email` are returned. Lookups expire after the account's retention period (404 not_found afterwards).

- `id` (path, required): Object id (`lkp_…`, `job_…` or `we_…`).
- `wait` (query): Seconds (0–30) to wait for pending checks before answering.

Responses: 200 The lookup (completed or still pending).; default Error. Branch on `error.code`; `suggestion` (optional) says how to fix the request.

## Bulk jobs

### POST /jobs/estimate

Estimate a bulk job for free

Validates and counts a list without storing it, calling a data source or charging: valid, invalid and duplicate rows, cached answers, and the maximum possible cost (`max_cost`). Same body as POST /v1/jobs.

Responses: 200 The estimate.; default Error. Branch on `error.code`; `suggestion` (optional) says how to fix the request.

### POST /jobs

Create a bulk job

Checks up to 50,000 numbers and e-mail addresses (JSON body, or a CSV upload as multipart/form-data). The maximum possible cost is reserved up front; cached, inconclusive, invalid and duplicate rows are released when the job finishes. Poll GET /v1/jobs/{id} (or subscribe to job.completed), then page through GET /v1/jobs/{id}/results or download the whole file. Test-key jobs complete immediately.

- `Idempotency-Key` (header): Any unique string (1–255 printable ASCII) for safe retries: a retry with the same key and body within 24 hours returns the stored response (header Idempotent-Replayed: true) instead of running the request again.

Responses: 201 Job created. `estimate` shows how the list was counted.; default Error. Branch on `error.code`; `suggestion` (optional) says how to fix the request.

### GET /jobs/{id}

Get a job

Status, progress and cost of a job. `wait` (0–30 s) holds the request until the job reaches a final status (completed, failed or cancelled) or the time runs out. Keys with `jobs:read` see their own jobs; `jobs:read_all` sees every job of the account.

- `id` (path, required): Object id (`lkp_…`, `job_…` or `we_…`).
- `wait` (query): Seconds (0–30) to wait for pending checks before answering.

Responses: 200 The job.; default Error. Branch on `error.code`; `suggestion` (optional) says how to fix the request.

### DELETE /jobs/{id}

Cancel a running job or delete a finished job's results

Running job → cancelled: rows not yet sent to a data source are released and never charged; rows already sent finish and are billed as usual. Finished job → its per-row results are deleted now instead of at the end of the retention period (`purged_at` is set).

- `id` (path, required): Object id (`lkp_…`, `job_…` or `we_…`).

Responses: 200 The job after cancelling or deleting results.; default Error. Branch on `error.code`; `suggestion` (optional) says how to fix the request.

### GET /jobs/{id}/results

List a job's results

One item per row (number or e-mail address) in input order, with every check of the row. Filter by `registered` / `status` of one `service`; page with `limit` and `after` = the previous page's `next_cursor`.

- `id` (path, required): Object id (`lkp_…`, `job_…` or `we_…`).
- `registered` (query): Only rows whose answer for `service` is true / false / null.
- `status` (query): Only rows whose `service` check has this status.
- `service` (query): Service (code or alias) the registered/status filters apply to; default = the job's first check. With a filter, only rows of that service's input type are returned.
- `limit` (query): Rows (numbers and e-mails) per page. A row's checks are never split across pages.
- `after` (query): Cursor from the previous page (`next_cursor`).

Responses: 200 A page of result items.; default Error. Branch on `error.code`; `suggestion` (optional) says how to fix the request.

### GET /jobs/{id}/download

Download a job's results as CSV or NDJSON

Streams the whole result file, one line per row (numbers, then e-mails) in input order. CSV columns: row_no, input_masked, e164, country, number_status, service, status, registered, business, checked_at, cached, billed (for the row's primary check = first requested check of its kind), then kind, email, email_status, then per service `<service>.status`, `<service>.registered`, `<service>.<attribute>`, `<service>.billed` (empty for rows of the other kind). NDJSON lines have the same shape as result items.

- `id` (path, required): Object id (`lkp_…`, `job_…` or `we_…`).
- `format` (query, required): File format.

Responses: 200 The result file (Content-Disposition attachment).; default Error. Branch on `error.code`; `suggestion` (optional) says how to fix the request.

## Account

### GET /account

Get the account balance

Balance, reserved amount and today's usage (UTC) of the key's account. Needs `account:read`.

Responses: 200 The account.; default Error. Branch on `error.code`; `suggestion` (optional) says how to fix the request.

### GET /limits

Get limits and remaining allowances

Request rate, request sizes, the daily identifiers cap and the key's daily spend cap. Daily counters reset at 00:00 UTC.

Responses: 200 Current limits.; default Error. Branch on `error.code`; `suggestion` (optional) says how to fix the request.

### GET /usage

Get usage by day or by check

Checks, conclusive answers, cached answers, billed units and cost for an inclusive UTC date range (at most 366 days), for the key's mode (live or test). Rows past the retention period are no longer counted.

- `from` (query, required): First day (UTC).
- `to` (query, required): Last day (UTC), inclusive.
- `group_by` (query): One row per day or per check.

Responses: 200 Usage rows.; default Error. Branch on `error.code`; `suggestion` (optional) says how to fix the request.

## Webhooks

### GET /webhook_endpoints

List webhook endpoints

Every endpoint of the account that is not deleted and has the same mode as the key (test keys list test endpoints, live keys live endpoints). Needs `webhooks:manage`.

Responses: 200 Endpoints.; default Error. Branch on `error.code`; `suggestion` (optional) says how to fix the request.

### POST /webhook_endpoints

Create a webhook endpoint

Registers a public HTTPS URL (at most 20 per account). The response contains the signing `secret` — shown only once. The endpoint stays `pending_verification` until it answers our signed `webhook.verification` event with a 2xx status. Not available with the public sandbox key.

Responses: 201 Endpoint created; `secret` is shown only this once.; default Error. Branch on `error.code`; `suggestion` (optional) says how to fix the request.

### PATCH /webhook_endpoints/{id}

Update a webhook endpoint

Changes the URL and/or the subscribed events. A new URL must be verified again (`pending_verification`).

- `id` (path, required): Object id (`lkp_…`, `job_…` or `we_…`).

Responses: 200 The updated endpoint.; default Error. Branch on `error.code`; `suggestion` (optional) says how to fix the request.

### DELETE /webhook_endpoints/{id}

Delete a webhook endpoint

Nothing is delivered to a deleted endpoint.

- `id` (path, required): Object id (`lkp_…`, `job_…` or `we_…`).

Responses: 204 Deleted.; default Error. Branch on `error.code`; `suggestion` (optional) says how to fix the request.

### POST /webhook_endpoints/{id}/test

Send a test event

Queues a signed `webhook.test` event to the endpoint (delivered even before verification). Use it to check your signature code.

- `id` (path, required): Object id (`lkp_…`, `job_…` or `we_…`).

Responses: 202 Test event queued.; default Error. Branch on `error.code`; `suggestion` (optional) says how to fix the request.

## Service catalog

### GET /services

List checks and prices

Without a key: the public catalog. With a key: every check the key's account may use, at the account's prices. Lists active phone and e-mail checks; switched-off checks (e.g. number.hlr, coming soon) are not listed.

Responses: 200 Service catalog with prices by mode.; default Error. Branch on `error.code`; `suggestion` (optional) says how to fix the request.

## Keys

### POST /test-keys

Get a personal test key

Issues a personal test key (`mv_test_…`) for a work e-mail address, protected by a Cloudflare Turnstile token from the form at https://mobilevalidate.com/get-test-key. The key works in test mode with any number (never billed, never reaches a data source), including bulk jobs and test-mode webhooks. It is shown **once** and never returned again. Each address can receive at most 3 test keys in 30 days; after that the answer is 409 test_key_exists. At most 3 requests per network per day, whatever their result (IPv6 networks count per /64). The request also asks for live access, which we review by hand. No authentication.

Responses: 201 The key (shown only once).; 409 No more test keys can be issued for this e-mail address right now (at most 3 in 30 days). Keys already issued are never shown again.; default Error. Branch on `error.code`; `suggestion` (optional) says how to fix the request.

## System

### GET /health

Check that the API is up

No authentication. 200 when the API can reach its database, 503 otherwise.

Responses: 200 Healthy.; 503 The API cannot reach its database; retry after Retry-After.

## Webhook events

- `lookup.completed`: A pending lookup finished
- `job.completed`: A bulk job finished
- `job.failed`: A bulk job failed (subscribable; delivery rolling out)
- `job.progress`: Progress of a running job (opt-in; delivery rolling out)
- `balance.low`: The balance fell below the low-balance threshold (delivery rolling out)
- `limits.cap_reached`: A daily identifiers cap or spend cap was reached (delivery rolling out)
- `webhook.verification`: Ownership challenge for a new or changed endpoint
- `webhook.test`: Test event from POST /v1/webhook_endpoints/{id}/test
