> ## Documentation Index
> Fetch the complete documentation index at: https://docs.orbit.devotel.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Troubleshooting: call flip and call pickup

> Resolve the FLIP_* handoff rejects, the PICKUP_* claim-race rejects, and CONF_NOT_FOUND_OR_NOT_ACTIVE — map each conference-calls code to the handoff state that fired it and the retry that is safe.

# 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:

1. `POST /api/v1/voice/call-flip` issues 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.
2. `POST /api/v1/voice/call-flip/pull` with 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.

Pickup claims a ringing call instead of a token, but the model is the
same: exactly one pickup wins each ringing call.

* Directed pickup (`POST /api/v1/voice/pickup/directed`) answers the
  call ringing on a named extension — the `**201` desk-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/pickup` lists 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.

| Error code                     | HTTP | Fired on                                                                                  | Go to                                                               |
| ------------------------------ | ---- | ----------------------------------------------------------------------------------------- | ------------------------------------------------------------------- |
| `FLIP_ALREADY_PULLED`          | 410  | Pull: the token was already claimed by a device                                           | [Already pulled or expired](#already-pulled-or-expired)             |
| `FLIP_EXPIRED`                 | 410  | Pull: the token lived past its two-minute window                                          | [Already pulled or expired](#already-pulled-or-expired)             |
| `FLIP_TARGET_MISMATCH`         | 403  | Pull: the flip was pinned to a different device                                           | [Target pinned to another device](#target-pinned-to-another-device) |
| `FLIP_NOT_FOUND`               | 404  | Pull or cancel: no live handoff matches the value you sent                                | [Value does not resolve](#value-does-not-resolve)                   |
| `FLIP_ALREADY_CONSUMED`        | 410  | Cancel: the handoff was already pulled or cancelled                                       | [Cancel a second time](#cancel-a-second-time)                       |
| `PICKUP_NO_RINGING_CALL`       | 404  | Pickup: nothing is ringing on the extension or in your group                              | [Nothing ringing](#nothing-ringing)                                 |
| `PICKUP_ALREADY_TAKEN`         | 409  | Pickup: another device claimed the oldest call first                                      | [Lost claim race](#lost-claim-race)                                 |
| `CONF_NOT_FOUND_OR_NOT_ACTIVE` | 404  | Conference lookup, e.g. adding an AI agent: the room id is unknown or in a terminal state | [Conference lookup](#conference-lookup)                             |

## Already pulled or expired — FLIP\_ALREADY\_PULLED / FLIP\_EXPIRED<a id="already-pulled-or-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:

1. Re-issue `POST /api/v1/voice/call-flip` for 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.
2. 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.

If the same token keeps coming back as expired, your client is holding
it across the window instead of pulling. Track the expiry timestamp the
flip response returns and re-issue before pulling from a slow UI path.

## Target pinned to another device — FLIP\_TARGET\_MISMATCH<a id="target-pinned-to-another-device" />

`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<a id="value-does-not-resolve" />

`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.

Re-list or re-issue a fresh flip and pull the value the fresh response
carries. Note for desk-phone users: codes *1–9* are park-slot numbers,
not flip codes — pulling a flip from a registered desk phone is `*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<a id="cancel-a-second-time" />

`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<a id="nothing-ringing" />

`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<a id="lost-claim-race" />

`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:

1. The pickup reject is a lost race, not a signal to retry blindly.
2. `GET /api/v1/voice/pickup` — the claiming request is fresh.
3. Issue a new pickup against an entry the fresh list shows.

For polling clients the same rule holds at the list level: poll the
ringing-calls list, and only fire a pickup on an entry the latest
poll returned. A raw-socket client should follow the same poll-then-
claim pattern against the REST list endpoint rather than caching a
candidate across polls.

## 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-flip` never 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 `details`
  view. 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<a id="conference-lookup" />

`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:

1. `GET /api/v1/voice/conferences` and confirm the id you are sending
   survives the list.
2. 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.
3. 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:

1. **The full error body** — code, message, and the `details` view
   (prior-claim metadata when present).
2. **The token, flip id, or room id involved** and the endpoint it
   came from.
3. **Your organization ID** (Settings → Organization, or
   `organizationId` from `GET /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](/voice/conferences) — create and manage the
  conference rooms these lookups guard.
* [Concepts: team chat model](/concepts/team-chat-model) — the
  organization-scoped membership model the pickup group defaults draw
  from.
* [Reference: error codes](/reference/error-codes) — the full catalog
  of FLIP\_*, PICKUP\_*, and CONF\_\* codes.
* [Troubleshooting: voice call park](/troubleshooting/voice-call-park)
  — the sibling shared-slot claim flow (`*1`–`*9`) that park codes
  mirror.
* [Troubleshooting: conference failures](/troubleshooting/conference-failures)
  — when the bridge itself (not the lookup) is the failing stage.
