Skip to main content

OTP approvals

OTP approvals is the second-supervisor gate for the two highest-leverage pieces of Orbit Verify:
  • an OTP message template going live, and
  • a resend burst on an existing verification (a flurry of resend requests against one verification).
When the gate is on, anyone who submits a matching change becomes the requester. Nothing leaves the queue until a different owner or admin approves it — the requester may not approve their own request. This is the same four-eyes pattern used for campaign launches, so the queue reads identically across gated surfaces.

The policy

The policy has three fields: Owners and admins edit the policy from the Policy card on the OTP approvals page (dashboard), or over the API.

Read the policy

GET /api/v1/verify/approvals/settings is readable by any authenticated member, so a requester can tell whether their next submission will queue before they send it.
Gateway off (require_approval_default: false) and an unset threshold (null) are the defaults — the gate is off until an owner or admin flips it on.

Update the policy

PUT /api/v1/verify/approvals/settings takes the policy row with your changes. Owners and admins only.
Send require_approval_default on every update — it is required. resend_burst_threshold accepts a positive integer or null (gate every burst); resend_burst_window_seconds accepts a positive integer up to 86,400 and defaults to 60 when omitted.

Work the queue from the dashboard

Open Outbound → OTP approvals. The page shows the pending queue and the Policy card on one surface:
  • Each row carries the kind (template or resend burst), the requester, and the queued timestamp, oldest first.
  • Approve and reject controls sit on the pending row. Reject opens a dialog for an optional reason that goes back to the requester.
  • Template rows show the exact rendered OTP body the approver is judging; resend-burst rows show the burst count and window the request accumulated.
Role restriction: only owners and admins read the queue, decide items, or edit the policy. Other roles see an access-restricted notice and the API returns 403.

Work the queue over the API

Three endpoints cover the queue: list it, approve an item, reject an item.

List pending items

GET /api/v1/verify/approvals/pending returns the queue oldest first. Optional query parameters: limit (1–100, default 50) and kind (otp_template or resend_burst).
When the gate is off, the list is empty and nothing accrues.

Approve an item

POST /api/v1/verify/approvals/:id/approve releases the queued work. No body required.
Unknown or already-decided ids return 404. If the approver is the requester, the API returns 409 (see The self-approve rejection).

Reject an item

POST /api/v1/verify/approvals/:id/reject holds the item. The body is optional; reason goes back to the requester.

Role failure mode

A member without the owner or admin role gets 403 on the queue, the decision endpoints, and PUT /settings (GET /settings stays readable so requesters can predict the gate):
The dashboard shows the same restriction as an access-restricted notice on the OTP approvals page.

Lifecycle of a pending item

Every item starts as pending and leaves the queue in exactly one of two ways:
  • Approved → released. A gated template goes live: the rendered body is applied to its verification profile. A gated resend burst re-enters the ordinary Verify dispatch chain — the same path a direct resend takes — and still respects the verification’s expiry, channel fallback, and rate limits. Approval decides nothing more than the policy gate; it never touches the delivery path itself.
  • Rejected → held. The item stops. With an optional reason the requester sees why. A rejected resend burst is simply not delivered; the requester may re-submit it (the re-submission queues a fresh row).
A queued resend burst can expire while it waits: if the underlying verification reaches its expiry before an approver releases it, the replay is declined by the normal resend rules and the requester must start a new verification.

Read a pending item

A queued resend burst looks like this (the same shape the pending endpoint returns per item):
For otp_template rows, rendered_body carries the exact template copy the approver judges, template_id names the verification profile it lands in, and channel names the profile slot it rewrites. For resend_burst rows the approver decides against the burst_count / window_seconds pair.

The self-approve rejection

If the approver is the same user as the requester, the API returns 409 Conflict:
Nothing is recorded as approved: the attempt fails before any state moves. The audit log records only decisions that went through (approvals and rejections), so a rejected self-approve attempt leaves the decision trail to the eventual decider. To clear your own queued item as the requester, reject it (you hold supervisor rights either way) with a reason such as “superseded” — then re-submit the template change or resend if the request should still go ahead on a second person’s stamp.

Audit log and webhooks

Every queue decision and every policy change lands in the Audit log with the actor, the queue item or organization, and the optional reject reason:
  • verify.approval.approved — a supervisor released an item, recorded after the release succeeds.
  • verify.approval.rejected — a supervisor held an item, with the reason in the detail payload.
  • verify.approval.settings.updated — someone edited the policy; the new values are in the detail payload.
An approval event carries the actor, the queue item it decided, and its kind and target:
No approval-specific webhook event exists. An approved resend burst emits the ordinary verification.sent webhook as it dispatches, and an approved template shows up in the ordinary verification events of its next transmission — subscribe to the standard Verify events for downstream notification. See Webhook Events.

Troubleshooting

Still stuck? See the Verify overview and the Troubleshooting index.