Docs
Overview Console API operational
ByondAPI reference

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.

Status operational Auth hashed keys Channels email · whatsapp live

Base URL #

Every request is made over HTTPS to your account's base URL. Relative paths in this reference are appended to it.

Default https://api.byondapi.com/api/v1
Live origin https://byondcx.com/api/v1
Note: api.byondapi.com is still being wired up. byondcx.com/api/v1 is the origin serving traffic today — if a copied curl doesn't resolve yet, swap the host for the live origin.

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.

Auth

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.

Quickstart

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

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.

POST https://api.byondapi.com/api/v1/verify/start X-API-Key

Request body

FieldTypeapplication/json
channel required stringsms · voice · whatsapp · email Delivery channel for the code. email and whatsapp are live; sms and voice are accepted but currently respond channel_unavailable.
recipient required string Destination address. An E.164 phone number (e.g. +6591234567) for sms/voice/whatsapp, or an email address for the email channel.
brand optional string Sender/brand name shown to the recipient in the message body. Defaults to your account name.
code_length optional integer4 – 10 Number of digits in the generated code. Default 6.
ttl_seconds optional integer30 – 3600 How long the code stays valid before it expires. Default 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);
201 Created
{
  "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).
  • statuspending when delivery is in flight; channel_unavailable when the channel isn't serving yet.
  • expires_at — RFC 3339 UTC timestamp after which the code is rejected.

Status values

pending

Code generated and dispatched; awaiting the user's input.

channel_unavailable

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.

Verify

Check a code #

Submit the code your user entered against the verification you started. ByondAPI compares, counts attempts, and locks after too many failures.

POST https://api.byondapi.com/api/v1/verify/check X-API-Key

Request body

FieldTypeapplication/json
verification_id required string The id returned by /verify/start.
code required string The code the recipient entered. Sent as a string to preserve any leading zeros.

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
}
200 OK
{
  "verification_id": "vrf_3kZ9c1d4f7b21bQ",
  "status": "approved"
}

Status values

approved

Code matched. The recipient is verified.

incorrect

Code did not match. The user may retry until locked.

expired

The TTL elapsed — start a new verification.

locked

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.

Lookup

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.

GET https://api.byondapi.com/api/v1/lookup X-API-Key

Query parameters

ParameterTypequery string
number required string The number to validate. Best results in E.164 (e.g. +6591234567); national formats resolve when paired with country.
country optional stringISO 3166-1 alpha-2 Two-letter region hint (e.g. 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);
200 OK
{
  "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 null if 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_typemobile, fixed_line, voip, toll_free or unknown.
  • 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.

API keys

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.

POST https://api.byondapi.com/api/v1/api-keys Bearer

Request body

FieldTypeapplication/json
name required string Human label to identify the key in the console (e.g. prod-backend).
scopes optional string[]verify · lookup · * Permissions this key may exercise. Omit (or send 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);
201 Created
{
  "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/start and /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.

API keys

List API keys #

Enumerate the keys on your account. Secrets are never returned — only the safe key_prefix and metadata.

GET https://api.byondapi.com/api/v1/api-keys Bearer

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();
200 OK
{
  "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
    }
  ]
}
API keys

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.

DELETE https://api.byondapi.com/api/v1/api-keys/{id} Bearer

Path parameter

ParameterTypepath
id required string The key's 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();
200 OK
{
  "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.

Reference

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."
  }
}
401Unauthorized
Bad or missing key

The X-API-Key (or Authorization: Bearer) header is absent, malformed, or revoked. Returned error.code: invalid_api_key.

403Forbidden
Out of scope

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.

422Unprocessable
Validation failed

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.

429Throttled
Too many requests

You hit the per-recipient or per-key rate limit. Honor the Retry-After header before retrying. Returned error.code: rate_limited.

Example bodies

401 Unauthorized
{
  "error": {
    "code": "invalid_api_key",
    "message": "The provided API key is invalid or revoked."
  }
}
403 Forbidden
{
  "error": {
    "code": "insufficient_scope",
    "message": "This key is not authorized for the 'verify' scope.",
    "required_scope": "verify"
  }
}
422 Unprocessable Entity
{
  "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"
    }
  }
}
429 Too Many Requests
{
  "error": {
    "code": "rate_limited",
    "message": "Too many verifications for this recipient. Try again later.",
    "retry_after": 60
  }
}