# Verify an address from the browser with a publishable key

`POST /api/v1/email/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: Email

The endpoint behind the form widget (`https://spaw.co/spaw-form.js`). 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 and `POST /api/v1/email` instead.

The lookup bills the key's owner under the normal rules and answers `{ "success", "data" }` with **no meta block**, so page visitors never see the owner's balance. 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 (undeliverable, cache hits, test addresses) never count toward the cap.

For forms open to the public, store a Cloudflare Turnstile site key and secret on the publishable key. The endpoint then requires a confirmed `turnstile_token` with every lookup and answers `403 TURNSTILE_FAILED` without one, before any credit is spent. Tokens are single-use. The form widget obtains a fresh token per lookup when the script tag carries `data-turnstile-site-key`.

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. |
| `email` | string | yes | The address to verify. At most 254 characters. |
| `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/email/public \
  -H "Origin: https://www.example.com" \
  -H "Content-Type: application/json" \
  -d '{
  "key": "pk_live_…",
  "email": "mia@acme.com"
}'
```

## Responses

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

```json
{
    "success": true,
    "data": {
        "email": "mia@acme.com",
        "deliverable": "deliverable",
        "reason": null,
        "risk_score": 0,
        "risk_level": "low",
        "did_you_mean": null
    }
}
```

### 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/verify-email-public · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
