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

# Campaign approvals

> Gate campaign launches behind a supervisor review so nothing goes out until an owner or admin approves it — how to enable the gate, work the queue, and what denial does to a campaign.

# Campaign approvals

Campaign approvals add a review step between "an operator launches a campaign" and "messages leave the platform." One person drafts campaigns; a different person — an owner or an admin — signs off on what goes out. It is the dual-control pattern regulated teams run on email platforms, applied to every outbound channel (SMS, WhatsApp, email, voice).

Use it when a member or developer should not launch to a large audience unreviewed, or when your compliance policy requires a named reviewer on every send. If your launch permission depends on an org-wide policy, this page covers the surface; for the full campaign object it gates, see the [campaign lifecycle concept](/concepts/campaign-lifecycle).

## Who does what

The gate separates the requester from the approver:

* **Requester** — any team member with launch rights on the campaign (a member or developer). They build the campaign, resolve the audience, and press **Launch campaign** on the campaign detail page.
* **Approver** — an **owner** or an **admin**. They review queued launches under **Outbound → Approvals** and decide. Owners and admins launching themselves are never gated — the gate would deadlock otherwise.

Every member of the team can read the approval policy (`GET /campaigns/approvals/settings`) — an operator needs to know whether their next launch will be gated before they press the button.

## Enabling the gate

Turn the gate on under **Outbound → Approvals**, or over the API. Two org-level settings decide what gets gated:

| Setting                       | Default | Effect                                                                                                                                              |
| ----------------------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| `require_approval_default`    | off     | When `true`, every launch by a non-supervisor member is gated.                                                                                      |
| `min_recipients_for_approval` | blank   | Optional audience threshold — only launches at or above this resolved audience size are gated. Leave blank to gate every launch regardless of size. |

Both default to off, so until an owner or admin enables the gate, launches go straight through — the gate is strictly opt-in. Owners and admins can always launch directly; the threshold only narrows which non-supervisor launches get parked.

Configure over the API:

```bash theme={null}
curl -X PUT "https://api.orbit.devotel.io/api/v1/campaigns/approvals/settings" \
  -H "X-API-Key: $ORBIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "require_approval_default": true, "min_recipients_for_approval": 500 }'
```

## What launch looks like to the requester

When the gate is on, a requester's launch does not send. It parks:

```bash theme={null}
curl -X POST "https://api.orbit.devotel.io/api/v1/campaigns/cmp_demo123/send" \
  -H "X-API-Key: $ORBIT_API_KEY"
```

```json theme={null}
{ "gated": true, "approval_id": "capap_…", "status": "pending_approval" }
```

The launch returns `202` with an approval id — not an error. The campaign moves to `pending_approval`, and the request lands in the supervisors' queue with the resolved audience size and an optional note from the requester. Nothing reaches recipients until an approver acts.

## Working the queue

Open **Outbound → Approvals**. The page has two parts: the policy toggle at the top (Enable / size threshold) and the **Pending launches** queue below it.

Each queued launch shows:

* **Who requested it** — the team member's display name resolved from the team list, so the supervisor sees a person, not an id.
* **Audience size** — the resolved recipient count at request time (the gate snapshots it, so a later list change does not re-derive the count the supervisor approved).
* **When it was submitted** — relative then absolute timestamp.
* **Requester's note** — optional context the requester attached; empty shows "No note from the requester."

An approver has two decisions:

* **Approve & send** — the campaign re-enters the normal launch path and dispatches exactly as if the approver had launched it themselves. The approval is marked only after dispatch succeeds.
* **Reject** — optional reason; the campaign returns to `draft`. Nothing was sent. The requester edits the body, list, or schedule and resubmits.

Reject has no partial approve-with-edits mode — the approver either sends the campaign as-is or hands it back. If dispatch fails on approve (insufficient wallet, a policy gate on the campaign itself), the approval stays in the queue and the error surfaces to the approver — the queue does not silently lose the launch.

## Campaign states across the lifecycle

| Route                               | Meaning                                                            |
| ----------------------------------- | ------------------------------------------------------------------ |
| `draft` / `scheduled`               | Editable; the launch button is live (gated if the policy says so). |
| `pending_approval`                  | A requester launched; waiting on an approver. Not sending.         |
| `sending` → `running` → `completed` | After approve — the normal dispatch path.                          |
| back to `draft`                     | After reject — the requester edits and resubmits.                  |

The [campaign lifecycle](/concepts/campaign-lifecycle) page covers the full status machine these four steps slot into.

## How deny affects scheduling

A denied campaign does not fail; it returns to `draft`. Anything the launch depended on (a schedule, a list fix, a body edit) is still editable — the approver just enforced a human checkpoint before any of it ran. A rejected launch never sends, and the audit reason (if provided) is stored with the decision.

This is by design: the gate changes **who can launch**, not **how messages leave**. An approved campaign dispatches over the same pipeline as a direct launch — the same suppression, consent, quiet-hours, and provider chain.

## API surface

| Method | Path                            | Role                     |
| ------ | ------------------------------- | ------------------------ |
| `GET`  | `/campaigns/approvals/pending`  | owner / admin            |
| `POST` | `/campaigns/:id/approve`        | owner / admin            |
| `POST` | `/campaigns/:id/reject`         | owner / admin            |
| `GET`  | `/campaigns/approvals/settings` | any authenticated member |
| `PUT`  | `/campaigns/approvals/settings` | owner / admin            |

**Bulk approval** — approvers decide one campaign at a time; the queue is the batch surface, so the reviewer handles each payload deliberately rather than rubber-stamping a page of checkboxed launches.

## Worked example: gated launch to a rejected campaign

A pass through gate → reject → re-edit → approve:

**1. Enable the gate** (owner, once per org) — gate every launch above 500 recipients:

```bash theme={null}
curl -X PUT "https://api.orbit.devotel.io/api/v1/campaigns/approvals/settings" \
  -H "X-API-Key: $ORBIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "require_approval_default": true, "min_recipients_for_approval": 500 }'
```

**2. A member launches a 7,800-recipient SMS campaign.** The launch parks:

```json theme={null}
{ "gated": true, "approval_id": "capap_…", "status": "pending_approval" }
```

**3. An admin opens Outbound → Approvals,** reads the requester's note ("Holiday promo — audience is the opted-in holiday list"), and rejects with reason "Audience list includes recipients from the suppressed segment — trim and resubmit."

```bash theme={null}
curl -X POST "https://api.orbit.devotel.io/api/v1/campaigns/cmp_demo123/reject" \
  -H "X-API-Key: $ORBIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "reason": "Audience list includes recipients from the suppressed segment." }'
```

The campaign returns to `draft`; the requester trims the list, re-resolves the audience, and resubmits the launch. This time the approver approves, the campaign dispatches on the normal path, and the launch reads `approved` in the queue's history.

## Related pages

* [Campaign lifecycle](/concepts/campaign-lifecycle) — the status machine `pending_approval` parks a campaign in.
* [Send a campaign end-to-end](/guides/campaign-end-to-end) — the full launch workflow the gate attaches to.
* [Outbound goals, approvals, and direct send](/guides/outbound-goals-approvals-direct-send) — the three governance controls behind Outbound, including this gate alongside goals and direct send.
