Approvals

Human-in-the-loop as an API primitive — the agent proposes, a person taps Approve / Edit / Reject on their phone, the agent acts only after action.approved.

An agent that can do consequential things needs a person in the loop, and "reply yes in chat" is not a safe primitive: anyone who can type in the channel can say yes, and a prompt-injected agent can fake the question. nmbr's approvals are bound to one person and one conversation, render as a native card, and fail closed.

The flow

  1. Propose. POST /agent/v1/actions with to (the person's nmbr), a developer-defined kind, a one-line title, optional description, and any payload your agent needs back.
  2. Card. The person sees an approval card in the 1:1 chat with your agent — push, badge, preview, like any message. They can Approve, edit the payload and approve, or Reject.
  3. Decision event. action.approved (with payload.action.editedPayload set if they edited — use it instead of payload), action.rejected, or action.expired, over long-poll or your webhook.
  4. Act — on your side. nmbr never executes anything. Only after action.approved does your agent do the thing.

Binding rules

In five lines (bash)

curl -s -X POST https://nmbr.ai/api/agent/v1/actions -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"to":"123-456-789","kind":"deploy","title":"Deploy v2 to prod?","payload":{"ref":"abc123"}}'
curl -s "https://nmbr.ai/api/agent/v1/updates?afterSeq=$SEQ&wait=25" -H "Authorization: Bearer $TOKEN"
# → { "events": [ { "type": "action.approved", "payload": { "action": { "id": "…", "editedPayload": null, … } } } ] }

Loop the second call until an action.* event names your action id. GET /agent/v1/actions/{id} returns the current state at any time (useful after a missed webhook).

With the SDK

const decision = await agent.proposeAndWait({ to: "123-456-789", kind: "send_email", title: "Send the Q3 summary to Dana?", payload: { threadId: "t1" } });
switch (decision.state) {
  case "approved": await sendEmail(decision.editedPayload ?? decision.payload); break;
  default: /* rejected or expired: do nothing */
}

If you already run agent.updates() elsewhere, use isActionEvent(event) in that loop instead of proposeAndWait (one consumer per event stream).

Writing good proposals