> ## 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: disposition and wrap-up errors

> Recover from the voice disposition family — DISPOSITION_REQUIRED blocking the busy → available flip, DISPOSITION_CODE_NOT_FOUND and DISPOSITION_TAG_NOT_FOUND catalog rejects, and NOT_IN_WRAPUP presence mismatches.

# Troubleshooting: disposition and wrap-up errors

Voice queues and dashboards ship two disposition catalogs — per-queue wrap-up
codes and tenant-wide disposition tags — plus a presence gate that can hold an
agent out of the dispatch pool until a code is recorded. Four recovery-relevant
errors come out of that surface: `DISPOSITION_REQUIRED` (the queue's
`requireDisposition` gate keeps the `busy → available` flip blocked),
`DISPOSITION_CODE_NOT_FOUND` (the submitted code is not in the queue's
catalog), `DISPOSITION_TAG_NOT_FOUND` (the submitted tag is not in the
tenant's catalog), and `NOT_IN_WRAPUP` (a wrap-up call arrived while the
agent is not in the `wrapup` presence state). This page tells each one apart
and gets the next valid call through. For building the catalogs themselves,
see [wrap-up codes](/voice/wrap-up-codes) and
[call disposition tags](/voice/call-disposition-tags).

## Recognize the signal

| Code | HTTP | What it decides |
| - | - | - |
| `DISPOSITION_REQUIRED` | 422 | The queue has `requireDisposition` on; the agent's `busy → available` flip is blocked until a disposition is recorded on their most recent finished call. |
| `DISPOSITION_CODE_NOT_FOUND` | 404 | The slug or id you posted does not resolve to an active row in the **queue's** wrap-up catalog. |
| `DISPOSITION_TAG_NOT_FOUND` | 404 | The slug or id you posted does not resolve to an active row in the **tenant-wide** disposition-tag catalog. |
| `NOT_IN_WRAPUP` | 409 | A wrap-up endpoint (submit, extend, end) was called while the membership is not in the `wrapup` presence state. |

Every one of these is a deterministic refusal — the envelope tells you what
was wrong; a blind retry is never the fix. `meta.request_id` stays the
handle support reads when the refusal recurs.

## 1. DISPOSITION\_REQUIRED — the dispatch-blocking envelope

A queue with `requireDisposition` on refuses the membership's
`busy → available` flip while its most recent finished call carries no
recorded code. The agent sits in `wrapup`, off the dispatch pool, and the
status POST comes back `422` with the exact queue and call it is waiting
on:

```json theme={null}
{
  "error": {
    "code": "DISPOSITION_REQUIRED",
    "message": "A disposition code is required before flipping back to 'available' …",
    "details": {
      "queueId": "support",
      "callId": "call_01HZK…",
      "dispositionEndpoint": "/voice/queues/support/calls/call_01HZK…/disposition"
    }
  }
}
```

The fix is a sequence, not a retry: read the queue's catalog, post a code
against it, then flip the status again.

```bash theme={null}
# 1. Read the catalog the picker renders from
curl -X GET "https://api.orbit.devotel.io/api/v1/voice/queues/support/dispositions" \
  -H "X-API-Key: dv_live_sk_YOUR_KEY"

# 2. Record the disposition (dispositionCode or dispositionId)
curl -X POST "https://api.orbit.devotel.io/api/v1/voice/queues/support/calls/call_01HZK…/disposition" \
  -H "X-API-Key: dv_live_sk_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "dispositionCode": "sale_completed", "note": "Approved the refund" }'

# 3. Retry the status flip — only one edge is gated (`busy → available`)
curl -X POST "https://api.orbit.devotel.io/api/v1/voice/agents/agent_01HX…/status" \
  -H "X-API-Key: dv_live_sk_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "status": "available" }'
```

Only that one edge is gated — the agent can still step away (`away`,
`offline`, `paused`) without recording, and the gate re-prompts the next
time they try to go available. The `details.dispositionEndpoint` on the
envelope is the exact path to post to; follow it rather than reconstructing
the URL. The per-queue switch itself is documented on
[wrap-up codes](/voice/wrap-up-codes).

## 2. The catalog 4xx pair — code vs tag, queue vs tenant

Both rejects mean the same shape of failure at two different scopes: what
you posted is not an active catalog row.

### DISPOSITION\_CODE\_NOT\_FOUND — queue-scoped

Wrap-up codes live **per queue**: the slug or `dispositionId` you send must
resolve to an active row in that queue's catalog. A slug that reads fine on
another queue 404s here — the resolver looks at the queue named in the
path, not the whole tenant.

```bash theme={null}
curl -X POST "https://api.orbit.devotel.io/api/v1/voice/queues/support/calls/call_01HZK…/disposition" \
  -H "X-API-Key: dv_live_sk_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "dispositionCode": "sal_completed" }'
```

```json theme={null}
{
  "error": {
    "code": "DISPOSITION_CODE_NOT_FOUND",
    "status": 404
  }
}
```

Read the queue catalog (`GET .../queues/{queueId}/dispositions`), pick the
slugs it actually serves, and re-post. When every agent on the queue 404s
on the same slug, extend the catalog — the `POST .../queues/{queueId}/dispositions`
row-creation endpoint is owner/admin-only, and after it lands every picker
resolves the new code at once.

### DISPOSITION\_TAG\_NOT\_FOUND — tenant-scoped

Disposition tags live **tenant-wide**: one curated catalog stamps labels on
calls across queues and campaigns. The stamp endpoint rejects any
`tagSlugs` or `tagIds` entry that does not resolve and names the offenders
so you can fix exactly those:

```json theme={null}
{
  "error": {
    "code": "DISPOSITION_TAG_NOT_FOUND",
    "message": "One or more tagSlugs do not resolve to an active catalog row",
    "status": 404,
    "details": {
      "missingSlugs": ["vip", "lead"]
    }
  }
}
```

Read `GET /api/v1/voice/disposition-tags` (add `?includeInactive=true` if
you suspect retirement), drop or correct the missing values, and re-stamp.
Retired tags soft-delete — historical stamps keep resolving the label and
the slug frees up for reuse, so a 404 on a slug that used to work usually
means retirement, not a typo. Expose the right catalog writer: owners and
admins curate tags in **Voice → Disposition Tags** or via
`POST /api/v1/voice/disposition-tags`; agents stamp but do not curate.

### Which catalog do I write where?

| | Wrap-up code (per queue) | Disposition tag (tenant-wide) |
| - | - | - |
| Missing error | `DISPOSITION_CODE_NOT_FOUND` | `DISPOSITION_TAG_NOT_FOUND` |
| Resolve against | `GET .../queues/{queueId}/dispositions` | `GET /api/v1/voice/disposition-tags` |
| Extend via | `POST .../queues/{queueId}/dispositions` | `POST /api/v1/voice/disposition-tags` (or Voice → Disposition Tags) |

## 3. NOT\_IN\_WRAPUP — the presence mismatch

`409 NOT_IN_WRAPUP` fires when a wrap-up endpoint — submitting the
disposition, extending the window, or ending wrap-up — arrives while the
agent's membership on that queue is not in the `wrapup` state. The
written-by-thread write-through can lag or a supervisor can flip the state
first; the envelope is the route telling you "there is no open wrap-up
window to act on."

Check the membership's current state before posting, then only call the
wrap-up endpoints while the state is `wrapup`. The full presence matrix
lives on the
[agent presence lifecycle](/concepts/agent-presence-lifecycle) page; the
dispatch-relevant slice:

| State | What it means to wrap-up endpoints |
| - | - |
| `wrapup` | Post-call window open — submit / extend / end wrap-up apply. |
| `busy` | Live call — hang up first; the dispatcher enters `wrapup` on call end. |
| `available` | Already back in the pool — the window closed via timer expiry or a prior submit; nothing to end. |
| `paused` / `offline` | Stepped away — same as `available` for this purpose; the window is not open. |

Soften the client flow to: read the current membership state, and when it
already reads `available` treat the earlier submit as landed (the record is
idempotent on `(queueId, callId)`), instead of hard-failing the agent UX.

## 4. cURL per code

Collect all four envelopes the same way — the failing call plus a read-back
of the catalog or state it refused against:

```bash theme={null}
# DISPOSITION_REQUIRED — details.dispositionEndpoint tells you where to post
curl -X POST "https://api.orbit.devotel.io/api/v1/voice/agents/agent_01HX…/status" \
  -H "X-API-Key: dv_live_sk_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "status": "available" }'

# DISPOSITION_CODE_NOT_FOUND — re-read the queue catalog after
curl -X GET "https://api.orbit.devotel.io/api/v1/voice/queues/support/dispositions?includeInactive=true" \
  -H "X-API-Key: dv_live_sk_YOUR_KEY"

# DISPOSITION_TAG_NOT_FOUND — missingSlugs / missingIds name the offenders
curl -X GET "https://api.orbit.devotel.io/api/v1/voice/disposition-tags?includeInactive=true" \
  -H "X-API-Key: dv_live_sk_YOUR_KEY"

# NOT_IN_WRAPUP — read current presence before calling wrap-up endpoints
curl -X GET "https://api.orbit.devotel.io/api/v1/voice/agents/agent_01HX…/status" \
  -H "X-API-Key: dv_live_sk_YOUR_KEY"
```

## 5. What not to retry

* **Do not loop a 404.** Catalog membership is deterministic — the same
  slug misses a hundred times in a row. Extend the catalog (or correct the
  value), then send exactly one re-post.
* **Do not force the status flip.** `DISPOSITION_REQUIRED` is the queue
  asking for a record, not a bug in the state machine — record the
  disposition first, then the flip succeeds on the very next attempt.
* **Do not invent free-text outcomes.** The catalog reject is the reason
  free text never accumulates in reporting; write the outcome into the
  queue's catalog, not the `note` field alone.
* **Do not rebuild the slug locally.** Stale `details` in a picker cache,
  or a retired tag, both look like "the slug should work." Re-read the
  catalog with `includeInactive=true` before concluding the row exists.

## 6. Bundle for support

When the refusal keeps coming after the catalog was extended:

* the **`meta.request_id`** from the failing response envelope,
* the **queue id** and **agent id**,
* the **attempted code or tag slug** (and `dispositionId` / `tagId` when
  posted by id), and
* the **catalog read-back** showing the row active at the time of the
  refusal.

Support reads the logged refusal straight off the request id and compares
it against the catalog state instead of re-deriving it from queue config.

## See also

* [Wrap-up codes](/voice/wrap-up-codes) — the per-queue catalog rows,
  `requireDisposition`, analytics, and the owning-agent rule.
* [Call disposition tags](/voice/call-disposition-tags) — the tenant-wide
  tag catalog, stamps, and audit posture.
* [Agent presence lifecycle](/concepts/agent-presence-lifecycle) — the
  five-state presence matrix `NOT_IN_WRAPUP` points at, and who moves each
  edge.
* [Voice queues guide](/guides/voice-queues) — queue configuration,
  memberships, and wrap-up window sizing.
