Programmable communications, one base URL.
ByondAPI is the developer platform for programmable communications — Verify (OTP), Lookup, and scoped API key management behind a single key and a single base URL. Predictable JSON in, predictable JSON out. Ship in minutes with a single curl.
Base URL #
Every request is made over HTTPS to your account's base URL. Relative paths in this reference are appended to it.
All examples below substitute the live base automatically — the token __BASE__ in each sample renders as https://api.byondapi.com/api/v1. Press ⌘K (or /) any time to jump to an endpoint.
Authentication #
ByondAPI uses two distinct credential types depending on which plane you're calling.
Data plane — X-API-Key
Verify and Lookup are data-plane endpoints. Authenticate them with a hashed API key sent in the X-API-Key header. A key is two parts joined by a dot — a public prefix and a secret:
X-API-Key: pk_live_8fa2.s3cr3t_9c1d4f7b2a6e0h8j
The secret half is shown only once, at creation time — ByondAPI stores it as a hash and can never display it again. Treat the full prefix.secret like a password.
Key management — Authorization: Bearer
Creating, listing and revoking keys is a management-plane action performed by a signed-in console session. Those calls carry a short-lived session JWT in the Authorization header:
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Never ship a session Bearer JWT to a browser or mobile client, and never call data-plane endpoints with it. Use X-API-Key for Verify/Lookup, and Bearer only from your console-authenticated backend.
From zero to verified #
Mint a key in the console, then start your first verification. Email and WhatsApp deliver today.
Mint an API key
Open the console → API keys → New key. Copy the full prefix.secret the moment it appears — it is shown once. Export it so the next steps can read it:
export BYONDAPI_KEY="pk_live_8fa2.s3cr3t_9c1d4f7b2a6e0h8j"
Start a verification
POST to /verify/start with a channel and recipient. ByondAPI generates the code, delivers it, and returns a verification_id:
# start an email OTP — one POST, any channel
curl -X POST __BASE__/verify/start \
-H "X-API-Key: $BYONDAPI_KEY" \
-H "Content-Type: application/json" \
-d '{
"channel": "email",
"recipient": "[email protected]",
"brand": "Acme",
"code_length": 6
}'
const res = await fetch("__BASE__/verify/start", {
method: "POST",
headers: {
"X-API-Key": process.env.BYONDAPI_KEY,
"Content-Type": "application/json"
},
body: JSON.stringify({
channel: "email",
recipient: "[email protected]",
brand: "Acme",
code_length: 6
})
});
const { verification_id } = await res.json();
Check the code
Collect the code from your user and POST it to /verify/check. A status of approved means you're done:
# confirm the 6-digit code
curl -X POST __BASE__/verify/check \
-H "X-API-Key: $BYONDAPI_KEY" \
-H "Content-Type: application/json" \
-d '{
"verification_id": "vrf_3kZ9c1d4f7b21bQ",
"code": "084213"
}'
const res = await fetch("__BASE__/verify/check", {
method: "POST",
headers: {
"X-API-Key": process.env.BYONDAPI_KEY,
"Content-Type": "application/json"
},
body: JSON.stringify({
verification_id: "vrf_3kZ9c1d4f7b21bQ",
code: "084213"
})
});
const { status } = await res.json(); // "approved"
Start a verification #
Generate and deliver a one-time code over the chosen channel. ByondAPI owns the code, the TTL and the retry/lockout logic — you never store a secret or run a timer.
Request body
email and whatsapp are live; sms and voice are accepted but currently respond channel_unavailable.
+6591234567) for sms/voice/whatsapp, or an email address for the email channel.
6.
300 (5 minutes).
Request
curl -X POST __BASE__/verify/start \
-H "X-API-Key: $BYONDAPI_KEY" \
-H "Content-Type: application/json" \
-d '{
"channel": "email",
"recipient": "[email protected]",
"brand": "Acme",
"code_length": 6,
"ttl_seconds": 300
}'
const res = await fetch("__BASE__/verify/start", {
method: "POST",
headers: {
"X-API-Key": process.env.BYONDAPI_KEY,
"Content-Type": "application/json"
},
body: JSON.stringify({
channel: "email",
recipient: "[email protected]",
brand: "Acme",
code_length: 6,
ttl_seconds: 300
})
});
const data = await res.json();
console.log(data.verification_id, data.status);
{
"verification_id": "vrf_3kZ9c1d4f7b21bQ",
"channel": "email",
"status": "pending",
"expires_at": "2026-06-15T09:42:00Z"
}
Response fields
- verification_id — opaque handle you pass to
/verify/check. - channel — the channel the code was sent on (echoes the request).
- status —
pendingwhen delivery is in flight;channel_unavailablewhen the channel isn't serving yet. - expires_at — RFC 3339 UTC timestamp after which the code is rejected.
Status values
Code generated and dispatched; awaiting the user's input.
The requested channel isn't serving yet — see the note below.
Channel availability. email and whatsapp deliver today. sms and voice are accepted by the API but return channel_unavailable until those channels finish wiring up — your integration can stay the same and simply light up when they go live.
Per-recipient throttle. Repeated starts to the same recipient in a short window return HTTP 429 Too Many Requests. Back off and respect the Retry-After header rather than retrying immediately.
Check a code #
Submit the code your user entered against the verification you started. ByondAPI compares, counts attempts, and locks after too many failures.
Request body
/verify/start.
Request
curl -X POST __BASE__/verify/check \
-H "X-API-Key: $BYONDAPI_KEY" \
-H "Content-Type: application/json" \
-d '{
"verification_id": "vrf_3kZ9c1d4f7b21bQ",
"code": "084213"
}'
const res = await fetch("__BASE__/verify/check", {
method: "POST",
headers: {
"X-API-Key": process.env.BYONDAPI_KEY,
"Content-Type": "application/json"
},
body: JSON.stringify({
verification_id: "vrf_3kZ9c1d4f7b21bQ",
code: "084213"
})
});
const { status } = await res.json();
if (status === "approved") {
// user is verified
}
{
"verification_id": "vrf_3kZ9c1d4f7b21bQ",
"status": "approved"
}
Status values
Code matched. The recipient is verified.
Code did not match. The user may retry until locked.
The TTL elapsed — start a new verification.
Too many failed attempts. The verification is dead; start fresh.
A wrong code returns 200 with "status": "incorrect" — it is not an HTTP error. Branch on the status field, not the HTTP code, for verification outcomes.
Number lookup #
Validate and normalize any phone number to E.164, with country, national format and line type. Clean your list before you spend on delivery.
Query parameters
+6591234567); national formats resolve when paired with country.
SG) used to parse numbers given in national format.
Request
# validate & normalize a number
curl "__BASE__/lookup?number=+6591234567&country=SG" \
-H "X-API-Key: $BYONDAPI_KEY"
const params = new URLSearchParams({
number: "+6591234567",
country: "SG"
});
const res = await fetch("__BASE__/lookup?" + params, {
headers: { "X-API-Key": process.env.BYONDAPI_KEY }
});
const data = await res.json();
console.log(data.valid, data.line_type);
{
"input": "+6591234567",
"number_e164": "+6591234567",
"valid": true,
"possible": true,
"country_code": "SG",
"country": "Singapore",
"national_format": "9123 4567",
"line_type": "mobile",
"source": "offline"
}
Response fields
- input — the raw value you sent, echoed back.
- number_e164 — canonical E.164 form, or
nullif unparseable. - valid — boolean; the number is a real, dialable number for its region.
- possible — boolean; the digit count is plausible even if not provably valid.
- country_code — ISO 3166-1 alpha-2 region (e.g.
SG). - country — human-readable country name.
- national_format — locally formatted display string.
- line_type —
mobile,fixed_line,voip,toll_freeorunknown. - source — always
"offline"today (computed from an offline number-plan dataset).
Offline today. Lookup is computed from an offline number-plan dataset — fast and cheap, with no per-query carrier cost. Live carrier / HLR enrichment (real-time portability and roaming) is a planned future enhancement; source will identify the data origin when it ships.
Create an API key #
Mint a scoped, hashed key for your backend. This is a management-plane call — authenticate with a session Bearer token, not an API key.
Request body
prod-backend).
null) for an unrestricted key. See scopes below.
Request
# mint a scoped key (session-authed)
curl -X POST __BASE__/api-keys \
-H "Authorization: Bearer $JWT" \
-H "Content-Type: application/json" \
-d '{
"name": "prod-backend",
"scopes": ["verify", "lookup"]
}'
const res = await fetch("__BASE__/api-keys", {
method: "POST",
headers: {
"Authorization": "Bearer " + jwt,
"Content-Type": "application/json"
},
body: JSON.stringify({
name: "prod-backend",
scopes: ["verify", "lookup"]
})
});
const data = await res.json();
// data.key is shown ONCE — store it now
console.log(data.key);
{
"id": "key_7b2a6e0h8j",
"name": "prod-backend",
"key_prefix": "pk_live_8fa2",
"scopes": ["verify", "lookup"],
"created_at": "2026-06-15T09:40:00Z",
"key": "pk_live_8fa2.s3cr3t_9c1d4f7b2a6e0h8j"
}
Shown once. The full key field appears in this response and never again — every later call returns only the key_prefix. Copy it into your secret store immediately; if you lose it, revoke and mint a new one.
Scopes
Scopes constrain what a key can call. The data plane enforces them with a require_scope check on each request:
- NULL scopes — a key created without scopes is unrestricted and may call every data-plane endpoint.
"verify"— permits/verify/startand/verify/check."lookup"— permits/lookup."*"— wildcard that grants every scope explicitly.
Prefer least privilege: give each service only the scopes it needs. Calling an endpoint outside a key's scopes returns 403 Forbidden.
List API keys #
Enumerate the keys on your account. Secrets are never returned — only the safe key_prefix and metadata.
Request
curl __BASE__/api-keys \
-H "Authorization: Bearer $JWT"
const res = await fetch("__BASE__/api-keys", {
headers: { "Authorization": "Bearer " + jwt }
});
const { keys } = await res.json();
{
"keys": [
{
"id": "key_7b2a6e0h8j",
"name": "prod-backend",
"key_prefix": "pk_live_8fa2",
"scopes": ["verify", "lookup"],
"created_at": "2026-06-15T09:40:00Z",
"last_used_at": "2026-06-15T11:02:14Z"
},
{
"id": "key_1f3d9a2c4e",
"name": "marketing-lookup",
"key_prefix": "pk_live_2c4e",
"scopes": ["lookup"],
"created_at": "2026-06-10T08:15:00Z",
"last_used_at": null
}
]
}
Revoke an API key #
Permanently revoke a key by its id. Revocation takes effect immediately — in-flight requests using that key start failing with 401.
Path parameter
id (e.g. key_7b2a6e0h8j) — not the prefix. Get it from GET /api-keys.
Request
curl -X DELETE __BASE__/api-keys/key_7b2a6e0h8j \
-H "Authorization: Bearer $JWT"
const res = await fetch("__BASE__/api-keys/key_7b2a6e0h8j", {
method: "DELETE",
headers: { "Authorization": "Bearer " + jwt }
});
const { revoked } = await res.json();
{
"id": "key_7b2a6e0h8j",
"revoked": true
}
Revocation is irreversible. To rotate, mint the replacement key first, deploy it, then revoke the old one to avoid downtime.
Errors #
ByondAPI uses conventional HTTP status codes. 2xx means success; 4xx means the request was rejected and includes a machine-readable JSON body:
{
"error": {
"code": "invalid_api_key",
"message": "The provided API key is invalid or revoked."
}
}
The X-API-Key (or Authorization: Bearer) header is absent, malformed, or revoked. Returned error.code: invalid_api_key.
The key authenticated but lacks the required scope for this endpoint (e.g. a lookup-only key calling /verify/start). Returned error.code: insufficient_scope.
The body parsed but a field is missing or out of range (e.g. code_length > 10, or an invalid channel). The body lists the offending fields.
You hit the per-recipient or per-key rate limit. Honor the Retry-After header before retrying. Returned error.code: rate_limited.
Example bodies
{
"error": {
"code": "invalid_api_key",
"message": "The provided API key is invalid or revoked."
}
}
{
"error": {
"code": "insufficient_scope",
"message": "This key is not authorized for the 'verify' scope.",
"required_scope": "verify"
}
}
{
"error": {
"code": "validation_error",
"message": "One or more fields are invalid.",
"fields": {
"channel": "must be one of: sms, voice, whatsapp, email",
"code_length": "must be between 4 and 10"
}
}
}
{
"error": {
"code": "rate_limited",
"message": "Too many verifications for this recipient. Try again later.",
"retry_after": 60
}
}
