Troubleshooting: call flip and call pickup
Call flip moves a live call from one of your registered devices to another — initiated from the dashboard’s active-call view (or*7 on a
desk phone), finished on the second device. Call pickup answers a call
ringing on someone else’s extension. Both run over the conference-calls
surface and both use one-shot claim guards, so their error codes report
the state of the handoff at the instant your request arrived, not a
broken endpoint.
This page maps each FLIP_*, PICKUP_*, and
CONF_NOT_FOUND_OR_NOT_ACTIVE code to the state that produced it and
the response you own.
How flip and pickup arbitrate claims
Flip is a two-step handoff:POST /api/v1/voice/call-flipissues a short-lived pull token bound to the live call. The response includes the token, a four-digit short code for desk-phone pulls, and a two-minute expiry window — long enough to walk from the desk to a corridor.POST /api/v1/voice/call-flip/pullwith the token (or the short code over DTMF) claims it. Exactly one pull wins — the claim is atomic — and the loser of a race gets a classified reject instead of a double answer.
- Directed pickup (
POST /api/v1/voice/pickup/directed) answers the call ringing on a named extension — the**201desk-phone equivalent. - Group pickup (
POST /api/v1/voice/pickup/group) answers the oldest call ringing in your pickup group —*8. With no explicit member list, the whole organization is the default group. GET /api/v1/voice/pickuplists the ringing calls you can currently claim, oldest first.
Code matrix — which handoff state fired
Find the error code first, then jump to its section below.Already pulled or expired — FLIP_ALREADY_PULLED / FLIP_EXPIRED
410 FLIP_ALREADY_PULLED fires when the pull token was already
claimed, and 410 FLIP_EXPIRED fires when it outlived its roughly
two-minute window. Both response bodies carry a details.prior view —
the initiating user and, for an already-pulled token, the device that
won — so the client can render “Bob’s mobile took this call”
instead of a bare reject.
Fix it by issuing a fresh flip and pulling inside the window:
- Re-issue
POST /api/v1/voice/call-flipfor the same call. The flip is idempotent per (call, user): while an un-pulled, un-expired token exists you get that token back rather than a new one, so the retry is safe on flaky networks. - Pull the new token on the intended device. The original caller keeps talking on hold music until a pull succeeds — a 410 never damages the live call.
Target pinned to another device — FLIP_TARGET_MISMATCH
403 FLIP_TARGET_MISMATCH fires when the flip named a specific target
device and the pulling request’s device does not match. The response
carries details.expectedDevice naming the pinned device.
Fix it by pulling from that device’s username, or cancel the pinned
flip (POST /api/v1/voice/call-flip/{id}/cancel) and re-issue without
the target pin so any registered device in the organization can pull.
Value does not resolve — FLIP_NOT_FOUND
404 FLIP_NOT_FOUND fires on pull or cancel when the submitted token,
short code, or flip identifier matched no live handoff. Causes, in the
order you see them:
- A client still holding a token returned by an earlier list call — tokens never re-appear on list responses, only on the original flip response, so a stale identifier goes nowhere.
- A token for a call that free-timed-out or was cancelled while the pull was in flight.
- On cancel, a flip id from a list rendered before another release aged off the live view.
*7
followed by the four-digit short code, and entering a park-slot digit
fails this lookup the same way.
Cancel a second time — FLIP_ALREADY_CONSUMED
410 FLIP_ALREADY_CONSUMED fires on cancel when the handoff already
carries an outcome — pulled, or previously cancelled. The
details.prior view names the outcome. Treat it as success: the flip
is closed either way.
Nothing ringing — PICKUP_NO_RINGING_CALL
404 PICKUP_NO_RINGING_CALL fires when the scan finds nothing to
claim: a directed pickup where the extension named has no ringing call,
or a group pickup where no member in your pickup group (the whole
organization by default) has one.
Fix it by listing first — GET /api/v1/voice/pickup returns the
ringing calls you may claim, oldest first — and directing the pickup at
one of those entries. If the lookup succeeds but the caller hung up
before you claim, the next reject class covers it.
Lost claim race — PICKUP_ALREADY_TAKEN
409 PICKUP_ALREADY_TAKEN fires when your pickup resolved a ringing
call but another device claimed it between the lookup and the claim.
Exactly one pickup wins each ringing call; the loser sees this 409
instead of an accidental double answer.
The correct client behaviour is to take the failure and re-list — never
to re-fire the same pickup body in a loop:
- The pickup reject is a lost race, not a signal to retry blindly.
GET /api/v1/voice/pickup— the claiming request is fresh.- Issue a new pickup against an entry the fresh list shows.
Retry rules — what safe means here
- Flip re-issue is idempotent. Re-firing the flip button while a
live token exists returns the same token, so retries of
POST /api/v1/voice/call-flipnever multiply handoffs. - Re-using an identifier is not retrying. A pull on the same old token after an expiry keeps reclassifying as 410 forever. Mint a new token, or read a fresh list, before re-sending.
- Claim-race 409s resolve to re-list. The pickup 409 and the
occupied-flip 403 both answer “who / what won,” in the
detailsview. Read it and re-list; do not blindly loop the identical body. - No retry budget needs a backoff. None of the rejects in this class are rate-limit outcomes — pulling a value that still resolves is free, so burn no retries on a 410/409 your own list would already have predicted.
Conference lookup — CONF_NOT_FOUND_OR_NOT_ACTIVE
404 CONF_NOT_FOUND_OR_NOT_ACTIVE fires when a conference-room lookup
— most visibly adding an AI agent to a room — receives a room id
that either never exists in the tenant or has already reached a
terminal state (ended, failed, or cancelled). Brand-new rooms that are
still ringing their first participants count as active for this guard;
the reject genuinely means “unknown id or finished room.”
Fix it with the same re-list discipline:
GET /api/v1/voice/conferencesand confirm the id you are sending survives the list.- Add the agent after the first participant answers if you need an established bridge — a pending room accepts the leg, but operator expectations often read otherwise.
- Do not hold ids across calls: a list-entry rendered an hour ago is the most common dead-id source.
What to capture before escalating
Work the fixes above first. If the reject still fires on a value the live list shows, open a ticket with:- The full error body — code, message, and the
detailsview (prior-claim metadata when present). - The token, flip id, or room id involved and the endpoint it came from.
- Your organization ID (Settings → Organization, or
organizationIdfromGET /api/v1/me).
What not to do
- Do not retry a 410 in a loop — the value will keep classifying forever; mint a new token or re-list.
- Do not treat claim-race 409s as a crash — every one of them is a pre-bridge guard; the caller continues to hear hold or ring tone.
- Do not reuse identifiers across the poll boundary — tokens and list entries are one-shot values; always claim the freshest one.
See also
- Voice: conferences — create and manage the conference rooms these lookups guard.
- Concepts: team chat model — the organization-scoped membership model the pickup group defaults draw from.
- Reference: error codes — the full catalog of FLIP_, PICKUP_, and CONF_* codes.
- Troubleshooting: voice call park
— the sibling shared-slot claim flow (
*1–*9) that park codes mirror. - Troubleshooting: conference failures — when the bridge itself (not the lookup) is the failing stage.