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

# Reply approvals — the supervisor review gate for the inbox

> Require a second set of eyes before an agent's reply reaches the customer. Route a gated agent's composed reply to the supervisor pending queue, approve or reject it, and keep a per-agent override map on top of the org default.

# Reply approvals — the supervisor review gate for the inbox

Reply approvals turn an agent's outgoing reply into a supervised decision instead of an immediate send. An agent whose approval requirement is on composes a reply as usual; Orbit holds the proposed text in a queue instead of dispatching it, and a supervisor — or any owner/admin — approves it or rejects it with a reason. Approving dispatches the exact queued body through the same channel path a direct reply would take; rejecting sends nothing and tells the agent why, so they revise and resubmit.

The gate and the queue live under **Inbox → Pending replies** (supervisors) and **Inbox → Settings → Reply approvals** (who is gated). The API surface mirrors both endpoints and is documented here in full.

## Who the gate is for

Use the gate where a reply must be reviewed before it leaves the workspace:

* **Junior-agent onboarding.** A new or training agent answers with real customers from day one, and a supervisor clears every reply until the required competence to send directly is proven. Turn the gate off for that agent when the review stops adding value.
* **Regulated or high-stakes queues.** Teams answering financial, legal, or complaint-handling conversations often run a four-eyes rule — two people touch every customer-facing message — as a customer control. Orbit supplies the control; whether it applies is the tenant's call, default off.

The gate is scoped per agent. Agents left un-gated send directly and never see the queue; supervisors work one shared queue of pending replies from all gated agents.

## When approvals are required (resolution rules)

The requirement resolves in two levels, checked every time a reply is submitted:

1. **Per-agent override** — the requirement set for that specific user wins over everything below.
2. **Org-wide default** — `require_reply_approval_default` under the org's inbox settings; the fallback for every agent with no override. Default is off.

The composer never has to branch — submit every reply through the queue endpoint and let the server resolve. A reply from a non-gated agent dispatches immediately; a reply from a gated agent returns `gated: true` and lands in the queue.

The workspace owner is never gated. Routing the sole administrator's own replies through an approval queue would strand them behind their own gate, so the server always dispatches owner's replies directly — enabling an override for an owner is refused, and clearing a stale one is always allowed.

## Submit a reply through the gate

`POST /api/v1/inbox/replies/queue` — any authenticated full-seat agent may call it.

```bash theme={null}
curl -X POST "https://api.orbit.devotel.io/api/v1/inbox/replies/queue" \
  -H "X-API-Key: dv_live_sk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "conversation_id": "conv_support_123",
    "body": "Here is the return label for your order.",
    "channel": "whatsapp"
  }'
```

Two shapes come back, both `202 ACCEPTED` — the call is accepted either way:

* **Gated.** `{"gated": true, "approval_id": "repla_...", "status": "pending"}` — the reply is queued and waits for a supervisor.
* **Not gated.** `{"gated": false, "dispatched": {...}}` — the reply was dispatched immediately through the standard reply path, and carries the dispatched message payload.

Body fields mirror the direct-reply endpoint: `conversation_id` and `body` are required; `channel`, `from`, `media_url`, `metadata` are optional. Email replies additionally accept `subject`, `cc`, and `bcc`, and those ride with the queue row so an approved email reply ships with the operator-entered headers instead of a blank subject.

## Work the pending queue

List, approve, and reject are restricted to owners, admins, and supervisors — the approval charter.

**List pending rows:**

```bash theme={null}
curl "https://api.orbit.devotel.io/api/v1/inbox/replies/pending?limit=50" \
  -H "X-API-Key: dv_live_sk_your_key_here"
```

Returns pending rows oldest-first (the queue reads top-down), each with its `conversation_id`, `requested_by_user_id`, `body`, channel fields, and `status`. A workspace where the approvals table isn't provisioned yet gets an empty list, not an error.

**Approve:**

```bash theme={null}
curl -X POST "https://api.orbit.devotel.io/api/v1/inbox/replies/repla_a1b2c3/approve" \
  -H "X-API-Key: dv_live_sk_your_key_here"
```

The queued body dispatches through the exact same channel path a direct reply uses, and the row is marked `approved` with the dispatched message id stamped on it.

**Reject:**

```bash theme={null}
curl -X POST "https://api.orbit.devotel.io/api/v1/inbox/replies/repla_a1b2c3/reject" \
  -H "X-API-Key: dv_live_sk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{ "reason": "Offer the discount code instead of a refund." }'
```

Nothing is dispatched. The row is marked `rejected` with an optional reason (up to 1,000 characters) the agent can read as review feedback.

The dashboard renders the same list under **Inbox → Pending replies**: each card shows the composing agent, conversation, channel, and proposed body, with approve and reject actions inline. Rejecting opens a reason dialog.

## Manage who is gated

**Read the effective setting.** Any workspace member can read the resolution for themselves — the composer uses it to decide whether the send goes through the queue:

```bash theme={null}
curl "https://api.orbit.devotel.io/api/v1/inbox/replies/settings" \
  -H "X-API-Key: dv_live_sk_your_key_here"
```

```json theme={null}
{
  "org_default": false,
  "my_override": true,
  "effective_require_approval": true,
  "agent_overrides": { "usr_abc": true, "usr_def": false }
}
```

The `agent_overrides` map — the full per-agent override table — only ships to supervisor roles; other callers get back their own override and the org default.

**Set an agent's requirement.** Owners, admins, and supervisors may pin or unpin an agent:

```bash theme={null}
curl -X PUT "https://api.orbit.devotel.io/api/v1/inbox/replies/settings/usr_abc123" \
  -H "X-API-Key: dv_live_sk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{ "require_reply_approval": true }'
```

The dashboard does the same work under **Inbox → Settings → Reply approvals**, where a row per team member carries the gate toggle. Setting `true` on the workspace owner returns 422 — the owner exemption; clearing a stale override (`false`) always succeeds.

## Error contract

| Condition                                                                | Status | Code                            |
| ------------------------------------------------------------------------ | ------ | ------------------------------- |
| Non-supervisor role lists pending, approves, rejects, or writes settings | 403    | `INSUFFICIENT_PERMISSIONS`      |
| Approve / reject on an unknown approval id                               | 404    | `NOT_FOUND`                     |
| Approve / reject on a row already decided (`approved` / `rejected`)      | 422    | `INVALID_STATE`                 |
| Enabling the gate on the workspace owner                                 | 422    | `OWNER_CANNOT_REQUIRE_APPROVAL` |
| Malformed body (missing `conversation_id` / `body`, body over 32 KB)     | 422    | validation                      |

A 422 from a parse failure carries the field-level issue list, so a client surfaces "body is too long" rather than an opaque rejection. If dispatch fails when the supervisor approves — the provider is down, the conversation closed, a send-time validation trips — the approve request errors with the dispatch reason and the row stays `pending`, so the supervisor can retry after the cause is fixed. Nothing is half-approved.

On reject, the `reason` is optional; an empty reject works, but the agent reviewing their rejected queue row only sees feedback when one is supplied.

## Audit trail

Every gate decision writes an audit event against the org: the queued submission (`inbox.reply.queued`), the not-gated bypass (`inbox.reply.queue.bypassed`), the supervisor decision (`inbox.reply.approved` or `inbox.reply.rejected`), and every settings change (`inbox.reply.settings.updated`). Each event carries the acting user, the conversation or approval id it touched, and the extra detail (the reject reason, the dispatched message id, the new setting value).

The queue row itself keeps the record too: `requested_by_user_id` and `decided_by_user_id` name both sides of the four-eyes pair, `decided_at` timestamps the decision, `reject_reason` holds the feedback, and `dispatched_message_id` points to the message that actually went out. That pair — submitter and decider — is the controlled record an audit or QA review reads.

## See also

* [Inbox setup](/guides/inbox-setup) — channels, assignment, and the workspace the gate operates inside.
* [Per-digital-queue SLA escalation policies](/inbox/sla-escalation-policies) — the queue-level alerting counterpart on the inbox tree.
* [Agent assist whisper coaching](/guides/agent-assist-whisper-coaching) — the other supervisor review surface; coaching instructs live, reply approvals gate outbound text.
* [Conversations API](/api-reference/conversations) — the direct reply path a not-gated submission falls back to.
