# Validate a number from the browser with a publishable key

`POST /api/v1/phone/public`

- Authentication: Publishable key in the body, checked against the browser Origin header
- Billing: Bills the key owner like a single lookup; capped by the key's daily credit cap when one is set.
- Group: Phone

The browser counterpart of `POST /api/v1/phone`, for checking a phone field in a signup form on the client without exposing a secret key; it is what the form helper's `data-spaw-phone` inputs call. It is authenticated by a publishable `pk_` key in the body plus the browser's `Origin` header, which must match one of the domains the key is locked to. A missing `Origin` is rejected on purpose: servers use a secret key. Pass `country` (an ISO 3166-1 alpha-2 code, usually from a country select on the same form) so numbers typed without a calling code parse.

The lookup bills the key's owner under the normal rules (a fresh valid answer costs one credit; invalid numbers, unassigned blocks and 7-day repeats are free) and answers `{ "success", "data" }` with **no meta block**, so page visitors never see the owner's balance. The live carrier check is never offered here: an `hlr` field in the body is ignored. Because the key sits in page source, give it a daily credit cap in the dashboard: once the cap is spent the endpoint answers `429 KEY_SPEND_CAP_REACHED` until the next day. Free answers never count toward the cap. A key that carries a Cloudflare Turnstile pair requires a confirmed `turnstile_token` and answers `403 TURNSTILE_FAILED` without one, before any credit is spent.

Throttled at 20 requests per minute per IP.

## Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `Origin` | header | string | yes | Sent by browsers automatically. Its host must be on the key's allowed-domain list. |

## Request body

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `key` | string | yes | A publishable key, which starts with `pk_`. At most 64 characters. |
| `phone` | string | yes | The number, in any common notation. Include the country calling code or pass `country`. At most 32 characters. |
| `country` | string | null | no | An ISO 3166-1 alpha-2 region code used to parse numbers written without a calling code, and to recognise that region's emergency numbers and short codes. |
| `turnstile_token` | string | null | no | Required when the key carries a Turnstile pair. A single-use token from the Turnstile widget on the page. At most 2,048 characters. |

## Example request

```bash
curl -X POST https://spaw.co/api/v1/phone/public \
  -H "Origin: https://www.example.com" \
  -H "Content-Type: application/json" \
  -d '{
  "key": "pk_live_…",
  "phone": "07911 012345",
  "country": "GB"
}'
```

## Responses

### 200 — The same fields as `POST /api/v1/phone`, without a meta block.

```json
{
    "success": true,
    "data": {
        "valid": true,
        "reason": null,
        "e164": "+447911012345",
        "international": "+44 7911 012345",
        "country": "GB",
        "line_type": "mobile",
        "line_type_source": "numbering_plan",
        "carrier": "Marathon Telecom Limited",
        "carrier_source": "block_allocation",
        "block_status": "allocated",
        "is_fictional": false,
        "is_virtual": false,
        "is_disposable": false,
        "risk_score": 0,
        "risk_level": "low",
        "hlr_checked": false
    }
}
```

### 401 — The publishable key does not exist or was revoked.

```json
{
    "success": false,
    "error": {
        "code": "INVALID_PUBLISHABLE_KEY",
        "message": "That publishable key does not exist or was revoked.",
        "request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
    }
}
```

### 402 — The balance is empty. The lookup did not run.

```json
{
    "success": false,
    "error": {
        "code": "INSUFFICIENT_CREDITS",
        "message": "Your credit balance is empty.",
        "request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
    }
}
```

### 403 — The page's origin is not on the key's allowed-domain list (`ORIGIN_NOT_ALLOWED`), or the key requires a Turnstile token that was missing or not confirmed (`TURNSTILE_FAILED`).

```json
{
    "success": false,
    "error": {
        "code": "ORIGIN_NOT_ALLOWED",
        "message": "This publishable key cannot be used from this origin.",
        "request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
    }
}
```

### 422 — The request body could not be validated; `error.errors` lists the fields.

```json
{
    "success": false,
    "error": {
        "code": "VALIDATION_FAILED",
        "message": "The email field is required.",
        "errors": {
            "email": [
                "The email field is required."
            ]
        },
        "request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
    }
}
```

### 429 — The key has spent its owner's daily credit cap (`KEY_SPEND_CAP_REACHED`), or the IP exceeded 20 requests per minute (`RATE_LIMITED`).

```json
{
    "success": false,
    "error": {
        "code": "KEY_SPEND_CAP_REACHED",
        "message": "This publishable key has reached its daily credit cap. The counter resets each day.",
        "request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
    }
}
```

## Error codes

- `INVALID_PUBLISHABLE_KEY` — https://spaw.co/docs/errors/INVALID_PUBLISHABLE_KEY
- `ORIGIN_NOT_ALLOWED` — https://spaw.co/docs/errors/ORIGIN_NOT_ALLOWED
- `TURNSTILE_FAILED` — https://spaw.co/docs/errors/TURNSTILE_FAILED
- `KEY_SPEND_CAP_REACHED` — https://spaw.co/docs/errors/KEY_SPEND_CAP_REACHED
- `VALIDATION_FAILED` — https://spaw.co/docs/errors/VALIDATION_FAILED
- `INSUFFICIENT_CREDITS` — https://spaw.co/docs/errors/INSUFFICIENT_CREDITS
- `RATE_LIMITED` — https://spaw.co/docs/errors/RATE_LIMITED

---

Canonical page: https://spaw.co/docs/api/validate-phone-public · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
