API reference/phone

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.

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 description
OriginrequiredheaderstringSent by browsers automatically. Its host must be on the key's allowed-domain list.

Request body

field type description
keyrequiredstringA publishable key, which starts with `pk_`. At most 64 characters.
phonerequiredstringThe number, in any common notation. Include the country calling code or pass `country`. At most 32 characters.
countrystring | nullAn 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_tokenstring | nullRequired 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

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"
}'
const response = await fetch('https://spaw.co/api/v1/phone/public', {
  method: 'POST',
  headers: {
    'Origin': 'https://www.example.com',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "key": "pk_live_…",
    "phone": "07911 012345",
    "country": "GB"
  }),
});
const result = await response.json();
import requests

response = requests.post(
    'https://spaw.co/api/v1/phone/public',
    headers={'Origin': 'https://www.example.com'},
    json={
        'key': 'pk_live_…',
        'phone': '07911 012345',
        'country': 'GB'
    },
)
result = response.json()
$ch = curl_init('https://spaw.co/api/v1/phone/public');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CUSTOMREQUEST => 'POST',
    CURLOPT_HTTPHEADER => ['Origin: https://www.example.com', 'Content-Type: application/json'],
    CURLOPT_POSTFIELDS => json_encode([
        'key' => 'pk_live_…',
        'phone' => '07911 012345',
        'country' => 'GB'
    ]),
]);
$result = json_decode(curl_exec($ch), true);

Responses

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

{
    "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
    }
}

401The publishable key does not exist or was revoked.

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

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

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

403The 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`).

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

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

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

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

{
    "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

Failures answer { success: false, error: { code, message, request_id } }. Each code has its own page.

markdown version·openapi.json