Skip to main content

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. Five 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), NOT_OWNING_AGENT (the owner gate answers 404 when a non-assigned, non-elevated agent tries to record someone else’s call), 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 and call disposition tags.

Recognize the signal

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:
The fix is a sequence, not a retry: read the queue’s catalog, post a code against it, then flip the status again.
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.

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.
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:
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?

3. NOT_OWNING_AGENT — the ownership 404

The record endpoint carries a second gate in front of the catalog lookup: ownership. A disposition write is only accepted from the agent the call was assigned to. When any other agent posts to POST /api/v1/voice/queues/:queueId/calls/:callId/disposition — same queue, same live call, even a valid catalog slug — the gate stops the write with 404 NOT_OWNING_AGENT:
details.queueId and details.callId echo the call the gate evaluated, so a shared softphone profile or a copy-pasted cURL line on the wrong session shows up immediately as the culprit. Only the assigned agent, or an owner/admin/superAdmin caller, may write the disposition; the elevated roles keep the write for post-incident corrections and audit back-fills.

Why 404 and not 403

A 403 would hand every agent a probe: post a disposition at any (queueId, callId) pair and read the error class to learn whether that call exists on the queue. Answering 404 makes a call the caller does not own indistinguishable from a call that does not exist, so the refusal carries no information about queue or call membership. The same reasoning is why the ownership check runs before the catalog lookup: a wrong slug on someone else’s call still reads NOT_OWNING_AGENT, never DISPOSITION_CODE_NOT_FOUND, so the refusal leaks no slug validity either.

Slugs vs. ownership — telling the two 404 families apart

The fix is one of two moves, not a retry: record the disposition from the owning agent’s session (their softphone or API key is already the right identity), or record it with an owner/admin/superAdmin session. The ownership rule sits on the queue’s assignment model — the entry, membership, and assignment mechanics are on the ACD queue model page.

4. The taxonomy — catalog-edit refusals (supervisor-side)

The sections above are the codes an agent hits when recording a disposition. A separate family fires when a supervisor edits the catalog itself — creating, re-parenting, renaming, or retiring wrap-up rows in Voice → Dispositions or through the queue-scoped admin endpoints. Six validation codes make up that family, all from POST/PATCH/DELETE .../queues/{queueId}/dispositions: All six are deterministic refusals: the envelope names the offending field, so read details and fix the request — a blind retry returns the same code forever. Authz matters up front: only owners/admins can edit the catalog (the POST/PATCH/DELETE endpoints sit behind the admin guard), so an agent token stops at 403 before any of these codes can fire.

The nesting-depth rule, and re-parenting before delete

The wrap-up catalog is a two-level tree: roots, and one level of children hanging off them. The picker renders that single nesting level, and the validator enforces it on every create and re-parent:
  • promoting a child to root — clear its parent: PATCH { "parentId": null }. Root-side null needs no validation.
  • attaching a row under a parent — the parent must be an active root on the same queue; pointing at another child raises DISPOSITION_NESTING_TOO_DEEP, and pointing at the row itself raises DISPOSITION_SELF_PARENT.
  • demoting a row that has children — refused with DISPOSITION_HAS_CHILDREN. Re-parent (or retire) its children first, then retry the demote.
Retiring a parent is safe to do directly. DELETE soft-deletes the row (flips is_active = false; the catalog row is never hard-deleted, so historical disposition records keep resolving their label), and its active children are promoted to root automatically — their parentId is cleared so the picker never renders a child dangling off an invisible retired parent. You never need a pre-delete sweep to protect the tree; re-parent children yourself only when you want them under a different parent instead of promoted.

DISPOSITION_ALREADY_EXISTS — the idempotent rename

Slugs (code) are unique across the queue’s active rows and immutable once created. Re-posting a slug that already exists returns 409 with the surviving row’s id in details.existingId — the fix is one of two moves, no retry:
  • You wanted a rename — PATCH the existing row by the id the envelope handed back, and change its label (the human display) or retire it (isActive: false) and re-create with the new slug. A PATCH that flips isActive: false frees the slug for reuse, because the uniqueness scope is the active set.
  • It was a double-submit — treat the 409 as idempotency: the row already exists, so read it from details.existingId and move on. A retry loop only re-burns 409s.

DISPOSITION_UPDATE_EMPTY — the no-op PATCH

A PATCH whose body carries no mutable field — an empty JSON object, or one holding only the immutable code — returns 400, because there is no SET to build. The message lists the six accepted fields (label / parentId / sortOrder / requiredOnTerminate / noteTemplate / isActive); send at least one:
Client-side, dodge the code at the source: build PATCH bodies from the diff between the form state and the last-read row — and skip the request entirely when the diff is empty — rather than posting whatever the form serialized.

5. 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 page; the dispatch-relevant slice: 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.

6. 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:

7. 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.
  • Do not read the ownership 404 as a missing call. NOT_OWNING_AGENT deliberately answers with the same status the catalog codes use; the refusal says the authenticated agent does not own the call, not that the call is gone. Read error.code, then re-post from the owning agent’s session.

8. 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 — the per-queue catalog rows, requireDisposition, analytics, and the owning-agent rule.
  • Call disposition tags — the tenant-wide tag catalog, stamps, and audit posture.
  • Agent presence lifecycle — the five-state presence matrix NOT_IN_WRAPUP points at, and who moves each edge.
  • Voice queues guide — queue configuration, memberships, and wrap-up window sizing.
  • ACD queue model — queue entries, assignment, and the owning-agent rule the disposition gate enforces.