Documentation
First verified number in 60 seconds.
Quickstart
1. Get an API key from the dashboard — sign in with an email, keys are shown once. (Self-serve sign-in emails open at launch; pre-launch accounts are founder-minted via the waitlist.)
2. Make the call:
curl -X POST https://api.boundstone.io/v1/verify/phone \
-H "authorization: Bearer bs_live_YOUR_KEY" \
-H "content-type: application/json" \
-d '{"phone": "+1 415 555 0184"}'
3. Read the response — including what we didn't check:
{
"input": "+1 415 555 0184",
"valid": true,
"e164": "+14155550184",
"country": "US",
"line_type": "fixed_line_or_mobile",
"national_format": "(415) 555-0184",
"checks": {
"performed": ["format", "region", "line_type_metadata"],
"not_performed": ["carrier_lookup", "ported_status", "hlr_liveness"]
},
"credits_charged": 1
}
checks.not_performed is load-bearing. Most vendors return a confident-looking verdict whether or not they actually checked. We list both sides so your code — and your audit trail — knows exactly what a result is based on.
Authentication
Bearer key in the authorization header: Bearer bs_live_…. Keys are created and revoked in the dashboard (max 5 active). We store only a hash — lose a key, mint a new one.
POST /v1/verify/phone
| Field | Type | Notes |
|---|---|---|
| phone | string, required | Any human format — E.164 comes back normalized |
| country | string, optional | Two-letter default region for national-format inputs, e.g. "US", "AU" |
Returns validity, E.164, country, line type (200+ countries, libphonenumber metadata tier). Carrier, ported status and HLR liveness report as not_performed until those data sources ship — they'll appear in checks.performed the day they're real. 1 credit.
POST /v1/verify/email
| Field | Type | Notes |
|---|---|---|
| string, required |
Returns syntax validity, domain, live MX records, A-record fallback, disposable-domain flag, role-account flag (admin@, billing@…), free-provider flag. SMTP mailbox and catch-all probing are not_performed — we won't guess deliverability we didn't test. 1 credit.
Bulk CSV
curl -X POST "https://api.boundstone.io/v1/bulk/phone?country=US" \ -H "authorization: Bearer bs_live_YOUR_KEY" \ -H "content-type: text/csv" \ --data-binary @numbers.csv
First CSV column is read; a header row is auto-detected. Free plan: 250 rows per job. Paid: 10,000. Credits (1/row) are reserved when the job is accepted and refunded for any rows that fail to process. Small jobs usually finish before your first poll.
| Endpoint | Does |
|---|---|
| POST /v1/bulk/phone · /v1/bulk/email | Submit — returns job_id, 202 |
| GET /v1/bulk/:id | Status: queued → running → done |
| GET /v1/bulk/:id/results.csv | Results as CSV once done |
Account & status
| Endpoint | Does |
|---|---|
| GET /v1/account | Email, plan, credit balance (authed) |
| GET /v1/health | Liveness ping, no auth |
| GET /v1/status | Public uptime feed — same data as the status page, CORS-open |
Rate limits
Per key, sliding 10-second window: free 15 · Starter 60 · Growth/Scale 120. Hitting the limit returns 429 with a plain message; nothing queues silently.
Errors
| Status | Code | Meaning |
|---|---|---|
| 400 | bad_request | Missing or malformed input — never charged |
| 401 | unauthorized / key_revoked | Bad or revoked key |
| 402 | insufficient_credits | Out of credits — explicit, never silent overage |
| 404 | not_found | That endpoint doesn't exist — we won't pretend it does |
| 429 | rate_limited | Sliding-window limit hit |
| 501 | ip_intelligence_pending | IP endpoint, until licensed data ships |
Every error is JSON: {"error": "code", "message": "what happened, in words"}.
The honesty contract
Three rules the API keeps, machine-checkably: (1) every verification response lists checks.performed and checks.not_performed; (2) capabilities we haven't built return explicit errors instead of fabricated verdicts; (3) accuracy claims live in the numbered public benchmarks, not in adjectives. If a response ever violates these, that's a bug — tell us and we'll fix it in public.