# Authentication

> How MobileValidate API keys work: test, live and agent keys, scopes, IP allowlists for IPv4 and IPv6, and what a 401 means.

Canonical: https://mobilevalidate.com/docs/authentication · Last updated: 2026-09-25

Every request authenticates with an API key sent as a bearer token: `Authorization: Bearer <key>`. There are three kinds of key: test, live and agent. The key's prefix tells you which kind it is. Keys carry scopes and can be locked to IP addresses. Any key problem returns the same `401 unauthorized`.

## What kinds of key are there?

| Prefix | Kind | What it does |
|---|---|---|
| `mv_test_` | Test | Answers from fixed [test data](/docs/test-mode). Never reaches a real network, never billed. Real numbers get deterministic fake answers. |
| `mv_test_publicSandbox…` | Public sandbox | A test key published in these docs so every example runs as pasted. Only the documented test numbers and addresses (`403 sandbox_magic_only` otherwise), per-IP limits, jobs up to 10 rows, no webhooks. |
| `mv_live_` | Live | Real checks, billed per conclusive answer. Test numbers and the test e-mail domain are refused (`test_number_only`). |
| `mv_agent_` | Agent | For AI agents and the [MCP server](/docs/mcp). Only the scopes it was given, plus a daily spend cap. |

A key is its prefix, 30 random base62 characters and a 6-character checksum. The checksum lets secret scanners and our API spot a mistyped or truncated key. We store only a keyed hash of each key, so a key is shown once, when it is created. If you lose a key, ask for a new one and revoke the old one.

Keep keys on your servers. Don't put them in front-end code, mobile apps, URLs or logs. The SDKs, the CLI and the MCP server read `MOBILEVALIDATE_API_KEY` from the environment and never print it. It is the only variable name we use.

## What are scopes?

Scopes limit what a key can do. A request outside a key's scopes gets `403 insufficient_scope`.

| Scope | Allows |
|---|---|
| `lookup:write` | `POST /v1/lookup`, `GET /v1/lookups/{id}` |
| `jobs:write` | `POST /v1/jobs/estimate`, `POST /v1/jobs`, `DELETE /v1/jobs/{id}` |
| `jobs:read` | Read your own jobs, their results and downloads |
| `jobs:read_all` | Read every job in the account (by default a key sees only the jobs it created). Any `lookup:write` key can read the account's lookups. |
| `account:read` | `GET /v1/account`, `/v1/limits`, `/v1/usage` |
| `webhooks:manage` | Create, change, test and delete webhook endpoints |

Test and live keys without an explicit list get every scope except `webhooks:manage`, which has to be granted on purpose. The public sandbox key never has `webhooks:manage` or `jobs:read_all`. Agent keys have exactly the scopes they were issued with. `GET /v1/services` and `GET /v1/health` work without a key; with a key, `/v1/services` shows what your account can use, with your prices. An unknown path returns `404 not_found` before the key is checked.

## How do IP allowlists work?

A key can be restricted to a list of IP addresses or CIDR ranges. Requests from anywhere else are refused with the same `401 unauthorized` as a bad key. Some live keys must have an allowlist, depending on your account.

- Entries can be single addresses (`203.0.113.10`) or ranges (`203.0.113.0/24`, `2001:db8:1234::/48`).
- IPv4 and IPv6 are both supported. An IPv4-mapped IPv6 address (`::ffff:203.0.113.10`) is matched as its IPv4 address.
- **IPv6 note:** servers and networks with IPv6 often rotate their source address (privacy extensions, cloud NAT, container networking). A dual-stack client may also switch from IPv4 to IPv6 without warning. Allow your IPv6 prefix, usually the `/64` of the host or the `/56` or `/48` assigned to your network, rather than a single address. You can also force your client to use IPv4.

## What happens when authentication fails?

A missing, malformed, unknown, revoked or expired key, or a key used from an address outside its allowlist, returns:

```json
{"error": {"code": "unauthorized", "message": "…", "status": 401, "retryable": false, "param": null,
  "doc_url": "https://mobilevalidate.com/docs/errors#unauthorized", "request_id": "req_…"}}
```

The answer is the same in every case on purpose, so nobody can probe which keys exist. Retrying won't help. Check the prefix, check for a trailing space or line break, and check your server's outgoing IP address. Include the `request_id` when you contact support.

## Frequently asked questions

### Why does a wrong IP address give the same error as a wrong key?

A missing, unknown, revoked, expired or wrong-IP key always returns the same 401 unauthorized, so the response never tells an attacker which part was wrong. Your account team can see the exact cause in the audit log.

### My allowlisted server suddenly gets 401. What changed?

Often the server started connecting over IPv6, or its IPv6 address rotated. Add your IPv6 prefix (for example a /64) as well as your IPv4 address.

### Can I put the key in a query string?

No. Keys go only in the Authorization header. Never embed a key in browser or mobile app code; call the API from your backend.
