Troubleshooting: caller-ID verification rejects
The caller-ID verification journey is two calls — register sends a one-time code to the handset, confirm submits it — and every reject it can return is one of four codes. The full flow, status lifecycle, and resend and revoke endpoints are on Manage outbound caller IDs; this runbook decodes what each reject means and where the fix lives. Reject-safety first:ALREADY_VERIFIED is deterministic steering, not a
failure of your retry loop — treat it as “already done”. The other three are
check-side outcomes; of them, only CHALLENGE_INVALID on a mismatched code
is safe to retry, and only until the attempts counter or expires_at runs
out.
The four codes
INVALID_CALLER_ID — the number fails acceptance
The number you passed failed acceptance — either E.164 shape (digits with an
optional leading +, 8–20 characters) at register, or the ownership and
verification check the outbound gate applies at dial time.
- Shape — normalize to E.164 before calling
POST /api/v1/voice/caller-ids/verify; the leading+may be omitted and is added back, but anything else malformed returns422before a challenge is dispatched. - Ownership at dial time — a dial refused because the presented number is
neither an org-hosted number nor a verified caller ID returns
UNVERIFIED_CALLER_IDinstead; the fix is to register and confirm here first, then present. See the caller-ID FAQ entries for both the dial-time gate and the SIT-trunkVOICE_CALLER_ID_REJECTEDvariant.
ALREADY_VERIFIED — a no-op, not an error
Two paths return this, both as 409:
- Register — a
verifiedrow already exists for the number. Re-verifying would burn an OTP dispatch and a wallet charge for nothing; the short- circuit is the platform saving you both. If you registered the number again under a different friendly name, revoke the existing row first, then re-register. - Confirm — the row bound to the
verification_idis alreadyverified. Your earlier confirm succeeded; the response was lost somewhere, but the state is settled. Read the current status fromGET /api/v1/voice/caller-idsinstead of re-submitting the code.
409 ALREADY_VERIFIED as an idempotent “already done” outcome. It
belongs on the deterministic-steering side of your error handling — surface
the row, don’t retry the call.
VERIFICATION_REJECTED — the check closed without approval
The confirm call reached the OTP check and the check’s terminal status was
not approved — most commonly expired (the challenge outlived its TTL) or
failed (the attempts budget ran out). The submitted code itself may have
been correct; the challenge was already closed when it arrived.
Recovery is at the challenge layer, deterministically:
- Resend while the row is still pending — the row is left in
pendingwhen the check rejects, soPOST /api/v1/voice/caller-ids/:id/resendmints a fresh code on the same row without re-running register. - Re-register after expiry — once the row flips to
expired, only a fresh register (or resend before expiry) re-arms it.
status=expired, status=failed) — read that before choosing resend vs.
re-register. Driving your UI’s countdown from the expires_at register
returned is the clean fix for stops on the third try.
CHALLENGE_INVALID — the per-attempt failure
The shared Verify challenge surfaces mint a challenge, then consume it one
attempt at a time — an OTP code, a nonce, or a spoken possession phrase —
and the per-attempt mismatch rejects with 400 CHALLENGE_INVALID. On
caller-ID verify specifically that means the submitted code
verification_id ↔ code pair did not match on that attempt.
Each attempt decrements the challenge’s attempts budget; confirm reads the
row only after the check, so budget exhaustion surfaces through
VERIFICATION_REJECTED above. Retry is safe only while
attempts_remaining (and expires_at) still hold — honour them in your UI
so a user sees the countdown instead of a surprise hard-stop on the last
try.
Deciding retry-safe vs deterministic
The last row is the register-side dispatch outcome — an OTP that never
leaves the platform because a pre-send gate tripped. Work the
gates tripped by rate limits and outbound state
page when register fails before a challenge ever dispatches.
Ticket-grade sample
A fix never needs support until the four codes stop matching their fix. When they do, include:- Tenant id — from
GET /api/v1/me, or Settings → Organization in the dashboard. - The reject code and its step — e.g.
VERIFICATION_REJECTEDon confirm, not “verify failed”. - The numbers at confirm — the
verification_id(from the register response), the caller-ID row id (cid_…), and the terminal status the error message carried. - The number as passed — the E.164 string as it hit register.
- Attempts and expiry —
attempts_remainingif you surfaced it, and the register-returnedexpires_atfor a “stopped on the third try” report.
See also
- Manage outbound caller IDs — the full register → confirm flow, the status lifecycle, resend and revoke.
- Troubleshoot Verify OTP failures — every
VERIFY_*send-side gate and the check-status semantics. - Error codes reference — the platform-wide error enum.
- Caller-ID FAQ entries — the
dial-time
UNVERIFIED_CALLER_IDgate and the trunk-sideVOICE_CALLER_ID_REJECTEDvariant.