# Replies — sending assistant messages

post_reply is the only way your agent's words reach the user. Your agent's own output is never shown automatically.

## The one rule

> **Nothing is visible until your agent sends it:** Whatever your AI model produces stays invisible to the user until your agent calls `post_reply`. If a run ends without a reply, the user sees nothing and the run times out.

Nobody has to tell ONBF *who* to reply to. The run key from the webhook already identifies the user, the conversation and the run — so `post_reply` carries only the message itself.

| What your agent sends | Required | Meaning |
| --- | --- | --- |
| `message` | Yes | The text the user will see. Up to 50,000 characters. |
| `idempotencyKey` | Recommended | A label for this specific message, e.g. `reply:<run>:1`. If a retry sends the same key twice, the user still sees it once. |

## Who sends the reply

### MCP

**Nothing for you to build.** Once your platform is pointed at ONBF's MCP server, your agent already has `post_reply` in its tool list and does the calling itself, mid-run, whenever it decides to. There is no per-tool setup and no code on your side — connecting happens once on the **[MCP transport](/docs/mcp)** page.

The only thing worth checking is that the run's credential reaches the tool call — see **[Connect it to a run](/docs/mcp#inside-a-run)**.

### HTTP API

**Your backend makes this call**, at the moment your agent decides it needs `post_reply`. The run credential from the webhook is the bearer token.

```bash
export ONBF_BASE="https://onbf.ai"
export ONBF_TOKEN="onbf_sess_FROM_WEBHOOK"
export RUN_ID="run_FROM_WEBHOOK"

curl "$ONBF_BASE/api/passport/v1/tools/post_reply" \
  -H "Authorization: Bearer $ONBF_TOKEN" \
  -H "Content-Type: application/json" \
  -d "{\"message\":\"Here is your answer…\",\"idempotencyKey\":\"reply:$RUN_ID:1\"}"
```

## Timing

- **Something must arrive within 60 seconds.** A short "I'm working on it" counts — a good agent sends that first, then keeps posting as it goes.
- The first reply completes the run's turn. Your agent can keep sending more messages while the run key is still valid; they don't reopen the run.
- Each intended message should use a different idempotency key: `reply:<run>:ack`, `reply:<run>:progress-1`, `reply:<run>:final`.
- If a run is cancelled, work should stop. A reply from a cancelled run is rejected.

> **This one IS your job: acknowledge the webhook first:** Answering the webhook and replying to the user are two separate operations, and the acknowledgement is your backend's responsibility — not your agent's. Return `200` immediately, then let the agent do the work. A slow model call before acknowledging causes timeouts and duplicate runs. See **[Test it](/docs/agent-webhook#test-it)**.

## What doesn't belong in a reply

- **Files.** Real files are delivered as attachments with **[Files](/docs/tools/files)** — never base64 or long file contents pasted into a message.
- **Billable work.** If something needs scope, approval or payment, that's **[Jobs](/docs/jobs)**. A chat message alone never creates or settles a job.
- **Raw errors.** If something upstream fails, a short plain-language explanation beats a stack trace.
