Skip to main content
Voice Authentication is an outbound call-to-approve primitive. Your backend asks Wave to verify someone; Wave calls that number, plays a short bilingual prompt describing what’s being authorized, captures the keypress, and returns a trustworthy, machine-readable verdict over a signed webhook (and a status API). It’s the voice equivalent of a push-to-approve / OTP step — a security factor, not a menu-tree IVR. Because the outcome gates access, it is fail-closed by design: only an explicit approval grants, and a machine/voicemail answer can never approve.
This is an outbound, API-triggered flow (Wave calls the user). It is distinct from IVR Menus (a caller dials you). Voice Authentication is gated — ask your Wave contact to enable it for your account.

Authentication & tiers

Every /v1/voice-auth endpoint needs a production API key (sk_live_) with the voice_auth:write scope (or voice_auth:read for status). Sandbox keys can’t place real authentication calls.

Create a challenge

You are responsible for binding phone_number to the user you’re authenticating. Wave verifies that whoever answers that number approves — it does not verify the number belongs to the claimed identity. Look the number up from your own records for the user in session; never take it from untrusted input.
201 returns the challenge id and (for number_match) the digits to display:

The verdict — fail closed

A challenge resolves to exactly one terminal state:
Only approved and declined are affirmative user decisions. Treat every other state as a non-grant and fail closed. Always confirm the verdict via the status API (below) before honoring an approval — defense in depth.

Answering-machine detection (AMD)

Before any prompt, Wave classifies who answered. A machine or voicemail answer resolves to machine_detected and the approval prompt is never played — so a voicemail can never capture a keypress or leave a stray approval. This is enforced, not best-effort. The classification is returned as amd_result (human or machine) on both the status API and the webhook. It errs on the safe side: an ambiguous answer is treated as a machine and blocked. As a result, a real person who gives a long spoken greeting can occasionally be misread as a machine and safely declined.
Tell your users to answer and wait for the prompt — they don’t need to say anything. A quick “hello” is fine; a drawn-out greeting is what risks a misread. Waiting for the prompt is the reliable way to be recognized as a human.

High-assurance number matching

For high-value actions (payments, admin changes), set assurance: "number_match". The 201 response includes match_digits (e.g. "47") — display them to your user. The call then asks the callee to key those exact digits to approve. There is no “press 1” shortcut in this mode, and a wrong or partial entry resolves to timeout (a non-grant) — so it defeats “just press 1” MFA-fatigue and accidental approvals, and the short code can’t be guessed (a wrong entry ends the attempt).

Caller brand

By default the call says “This is Wave calling…”. Your organization can make it say your own name (a caller brand) instead, in the call language: “This is Acme calling to verify a login on Acme App…”. A caller brand is a spoken name only — the caller ID stays Wave’s verified number. Get a brand approved (portal). In the developer portal, open Caller brands and request a brand with an English name and an Arabic name. It needs two approvals before a call can use it: an owner or admin of your organization (a request by an owner or admin skips this step), then Wave. Names are 2–40 characters (letters, digits, spaces and . & ' -; no web addresses; no run of 4 or more digits); the Arabic name must use Arabic letters. A brand’s names never change — to use a new name, request a new brand. Use it on a challenge. Pass brand_id on create. The call speaks:
  1. the brand_id you send, if any; else
  2. your project’s default brand (set on the Caller brands page); else
  3. “Wave” — so an integration that never sends brand_id keeps working.
A brand_id must be an approved brand of your organization; anything else (unknown, pending, rejected, retired, another organization’s) returns 422 VOICE_AUTH_BRAND_NOT_APPROVED and no call is placed. The challenge keeps the name it started with, so a brand retired mid-call never changes what the user hears. The 201, the status API and the webhook carry brand_id (null = “Wave”). List your brands from your backend.
Needs a production key with voice_auth:read. Returns every brand of your organization with its status (pending_tenant, pending_wave, approved, rejected, retired), the reject reason, and is_default for the key’s project. Paginate with cursor = the previous next_cursor.
If Wave’s text-to-speech is unavailable, Wave can fall back to pre-recorded prompts. In that mode the standard prompt does not name a caller (neither “Wave” nor your brand); the number-match prompt still says your brand. brand_id is still stored and returned.

The result webhook

On a terminal state Wave sends a signed voice_auth.completed event to your project’s webhook endpoint:
Verify every delivery:
  • X-Wave-Signature — sha256=…, an HMAC-SHA256 of the raw body with your endpoint’s signing secret. Reject a mismatch.
  • X-Wave-Timestamp — unix seconds; reject deliveries older than your window (e.g. 5 minutes).
  • X-Wave-Event-Id — stable across retries; dedupe on it.
The webhook is a notification — reconfirm via the status API before granting.

Poll status

Returns the current state, attempts, amd_result, brand_id, timestamps, and (once terminal) the decision. Scoped to your organization only.

Testing each verdict

There is no sandbox simulation for Voice-Auth — testing needs a production key and a real phone you control. Each terminal state is reproducible by how you answer the call: Confirm each on the status API (state, amd_result, attempts). Space out repeated tests to the same number, or you’ll hit VOICE_AUTH_RATE_LIMITED.

Idempotency, rate limits & abuse controls

  • Idempotency — send an Idempotency-Key; a retry with the same key returns the same challenge and never places a second call.
  • Per-number & per-originator limits — Wave caps challenges to a single number and per API key, and blocks known-abusive numbers/prefixes, to prevent voice-bombing and toll fraud. Over a limit → 429.

Errors