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 withrequireDisposition 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:
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 ordispositionId 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.
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 anytagSlugs or tagIds entry that does not resolve and names the offenders
so you can fix exactly those:
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 toPOST /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 fromPOST/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-sidenullneeds 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 raisesDISPOSITION_SELF_PARENT. - demoting a row that has children — refused with
DISPOSITION_HAS_CHILDREN. Re-parent (or retire) its children first, then retry the demote.
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. APATCHthat flipsisActive: falsefrees 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.existingIdand 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 immutablecode — 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:
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_REQUIREDis 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
notefield alone. - Do not rebuild the slug locally. Stale
detailsin a picker cache, or a retired tag, both look like “the slug should work.” Re-read the catalog withincludeInactive=truebefore concluding the row exists. - Do not read the ownership 404 as a missing call.
NOT_OWNING_AGENTdeliberately 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. Readerror.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_idfrom the failing response envelope, - the queue id and agent id,
- the attempted code or tag slug (and
dispositionId/tagIdwhen posted by id), and - the catalog read-back showing the row active at the time of the refusal.
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_WRAPUPpoints 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.