> ## Documentation Index
> Fetch the complete documentation index at: https://docs.semicola.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Semi confirmations

> How Semi asks for explicit approval before a durable write, and how a client renders, approves or cancels it.

Semi can propose writes while it helps you buy or run a storefront: creating or launching a
campaign, changing a storefront's setup, filing an ask for the Semicola team. For protected writes Semi doesn't run the write straight
away. It returns a structured **pending confirmation**, and the write runs only after a person
approves it.

A client renders each pending confirmation as an explicit **Approve / Cancel** control. The control
exists only when the response carries a structured pending confirmation; text in Semi's answer can
never create one.

## When a plain "yes" is enough

In the Semicola app you can approve by pressing **Approve**, or by replying in chat. A short, plain
reply such as "yes", "ok", "sounds good" or "go ahead" approves the open confirmations only when
every one of them has `plainAffirmativeAccepted: true`. Otherwise Semi asks you to use the button.

* Some tools always need the button or the launch phrase, `save_campaign` among them, so their
  confirmations have `plainAffirmativeAccepted: false`.
* **Spend and launch** never accept a plain "yes". Press **Approve**, or type the exact phrase
  `confirm launch`.
* **Tainted** confirmations (`policy: "tainted"`) need the button. A turn becomes tainted once Semi
  has read content it didn't write, such as an uploaded file or text from a seller. Text from those
  sources can contain instructions, and your click is what stops such an instruction from acting
  in your name.
* Anything that isn't a clear, short approval, including a conditional or mixed reply, approves
  nothing.

## Confirmation lifecycle

1. Send a chat turn with `POST /api/v2/assistant/chat` or `POST /api/v2/assistant/chat/stream`.
2. If the response has entries in `pendingConfirmations`, show each proposed action with its own
   approve/cancel control. A single turn can propose several writes; each entry has its own
   `confirmationUid` and is decided independently.
3. Post each decision to `POST /api/v2/assistant/confirmations/{confirmationUid}/decision`.
4. On `confirm`, the write runs and Semi finishes the turn it paused. The response carries the
   resumed chat response.
5. On `cancel`, nothing runs and the response only records the cancelled status.

<Warning>
  A `confirmationUid` is short-lived and single-use. It expires 15 minutes after it's issued and is
  bound to the tool and the exact parameters shown. Don't store it as a lasting approval.
</Warning>

## Pending confirmation fields

The chat response (and the `confirmation` frame of the stream) carries `pendingConfirmations`, an
array in tool-call order. It's empty when nothing needed approval.

```json theme={null}
{
  "data": {
    "conversationUid": "conv_7Hq2…",
    "answer": "I can file this for the Semicola team. Approve it to send.",
    "toolsUsed": [],
    "pendingConfirmations": [
      {
        "status": "pending_confirmation",
        "confirmationUid": "cnf_9Xk4…",
        "toolName": "save_ask",
        "toolKey": "save_ask:{…}",
        "summary": "File a support request: \"Checkout page error\"",
        "policy": "always",
        "resolvedParams": { "type": "support", "title": "Checkout page error" },
        "writeExecuted": false,
        "plainAffirmativeAccepted": true,
        "expiresAt": "2026-11-02T18:45:00.000Z"
      }
    ]
  }
}
```

| Field                      | Type                      | Notes                                                                                   |
| -------------------------- | ------------------------- | --------------------------------------------------------------------------------------- |
| `confirmationUid`          | string (`cnf_` prefix)    | Pass it to the decision endpoint. Each pending write gets its own id.                   |
| `toolName`                 | string                    | The tool that asked for the write.                                                      |
| `toolKey`                  | string                    | Stable key for the gated call (tool plus normalized parameters), used to dedupe panels. |
| `summary`                  | string                    | One-line, human-readable summary for the control.                                       |
| `policy`                   | `"always"` or `"tainted"` | Why the gate applied: the tool always asks, or the turn read outside content.           |
| `resolvedParams`           | object                    | The exact parameters the person is approving.                                           |
| `writeExecuted`            | `false`                   | The write did not run on this turn.                                                     |
| `plainAffirmativeAccepted` | boolean                   | When `true`, a plain "yes" can approve it (see above).                                  |
| `expiresAt`                | date-time                 | When the confirmation lapses.                                                           |
| `risk`                     | enum, optional            | `none`, `durable`, `spend` or `external`. Spend never accepts a plain "yes".            |

## Confirm or cancel

```bash theme={null}
curl -X POST "https://api.semicola.com/api/v2/assistant/confirmations/cnf_9Xk4…/decision" \
  -H "Authorization: Bearer $SEMICOLA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "decision": "confirm" }'
```

| Field      | Type                      | Required | Notes                            |
| ---------- | ------------------------- | -------- | -------------------------------- |
| `decision` | `"confirm"` or `"cancel"` | Yes      | Runs or drops the pending write. |

The response is `{ confirmationUid, status, response? }`. `status` is `confirmed`, `cancelled` or
`expired`. After a confirm that finishes the paused turn, `response` holds the same shape as a chat
response, and the write that ran appears in `toolsUsed`. Send `Accept: text/event-stream` to receive
the resumed turn as stream frames instead.

## Errors

* `400 VALIDATION_ERROR`: the body is malformed.
* `404 NOT_FOUND`: the confirmation is unknown, or belongs to a chat you can't see.
* `409 CONFLICT`: a response is still running in this chat. Wait for it to finish, then decide.
* An expired confirmation returns `status: "expired"` and nothing runs. Ask Semi again for a fresh
  one.

## Confirmations over MCP

Agents that call the v3 MCP endpoint get the same gate as a tool result: `status:
"pending_confirmation"` with a `confirmationUid`. After the person approves, call the same tool again
with the same arguments plus `confirm: true` and the `confirmationUid`. Changed arguments are
refused. See [Errors](/v3/errors) for the result shape.

## Related

<CardGroup cols={2}>
  <Card title="Errors" icon="triangle-exclamation" href="/v3/errors">
    `pending_confirmation`, `needs_input` and the error codes.
  </Card>

  <Card title="Limits" icon="gauge" href="/v3/limits">
    Confirmation expiry and other limits.
  </Card>
</CardGroup>
