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

# Delivery

```text {13} theme={null}
support-agent/
├── agent.json
├── instructions.md
└── .telnyx/                        written by deploy
    ├── index.ts
    ├── telnyx.toml
    ├── telnyx-env.d.ts
    └── package.json

@telnyx/acp/                      the package
├── managed/
│   ├── index.ts
│   ├── agent.ts                                       ◀
│   ├── model.ts
│   ├── memory.ts
│   ├── tools.ts
│   ├── mcp.ts
│   ├── voice.ts
│   ├── direct-turn.ts
│   └── filler.ts
├── channels/
│   ├── index.ts
│   ├── phone.ts
│   ├── email.ts
│   ├── web.ts
│   └── wire.ts
├── synth/
│   ├── index.ts
│   ├── code-entry.ts
│   └── vendor.ts
└── spec/
    ├── schema.ts
    ├── model.ts
    ├── phone.ts
    ├── project.ts
    └── io.ts
```

A reply to a message that came in through a channel goes back the same way. `noteRun` finds the `via` that `receiveVia` stored under the message's key before the run was admitted, records the reply in `pendingDeliveries` under the run id, and hands it to `sendPending`, the first of its tries:

```ts theme={null}
// managed/agent.ts, noteRun()
const via = state.replyTo?.[run.key] ?? state.replyTo?.[run.id];
// …the outcome is written to state, pendingDeliveries[run.id] with it
const pending = patch["pendingDeliveries"]?.[run.id] ?? pendingFor(state, run.id);
const error = errorFor(state, run.id);                 // a retry already on the clock, or final
const waiting = error && !error.final && error.nextAt !== undefined && error.nextAt > this.now();
if (pending && !delivered(state, run.id) && run.status === "completed" && !waiting) await this.sendPending(pending, (error?.attempts ?? 0) + 1);
```

`state` is what [`getState()`](/docs/agent-sdk/interface/getState) returned at the start of `noteRun`, and the writes go through [`setState()`](/docs/agent-sdk/interface/setState).

* `replyTo` holds the `via` that `receiveVia` stored, under the message's idempotency key before the run was queued, so a run that completes at once still finds it.
* `pendingDeliveries[run.id]` is written with the outcome before `deliver` runs, so a redelivered task still finds it; one entry per run, so a second run's reply never displaces a first run's that is still waiting on a retry.
* `runStarts[run.id]` is the message-log position written before the run's first dispatch; a redelivered task reads the same boundary, so its own reply is never mistaken for one already there.
* `deliveredRuns` lists the runs sent, and a `null` under `pendingDeliveries` deletes the entry; `sendPending` writes both once `deliver` returns. A redelivered task continues the attempt count instead of starting a second chain.

`deliver` sends the reply:

```ts theme={null}
// managed/agent.ts
async deliver(text, via) {
  if (text.trim() === "") return;                      // nothing to say is not a message
  const telnyx = this.env.TELNYX;
  if (!telnyx) throw new Error(`replying on ${via.kind === "email" ? "an email inbox" : "a messaging profile"} needs the TELNYX binding ([telnyx] in telnyx.toml)`);
  if (via.kind === "messaging") {
    await telnyx.messages.send({ from: via.to, to: via.from, text, messaging_profile_id: via.profileId });
  } else {
    // The inbox reply action: recipients, In-Reply-To and References come from the message it answers.
    await telnyx.post(`/email_inboxes/${encodeURIComponent(via.inboxId)}/messages/${encodeURIComponent(via.messageId)}/actions/reply`, { body: { text } });
  }
}
```

`this.env.TELNYX` is the Telnyx binding of the instance's [`env`](/docs/agent-sdk/interface/env), declared by `[telnyx]` in `telnyx.toml`. It holds no API key, and the runtime adds the credential before each request leaves.

For a text the reply is sent from the number the text was sent to, back to the sender, on the same messaging profile. For an email the reply goes through the inbox's reply action on the message that came in, and the platform threads and addresses it.

A `deliver` that throws never fails the run's task; the reply has a retry window of its own:

```ts theme={null}
// managed/agent.ts
export const DELIVERY_TASK = "__telnyx_acp_deliver";
export const DELIVERY_RETRY_SECONDS = [30, 60, 120, 300, 600, 1200, 1800];   // about an hour in all

private async sendPending(pending, attempt) {
  try {
    await this.deliver(pending.text, pending.via);
  } catch (err) {
    const status = httpStatus(err);                       // the API's status, or undefined for no answer
    const retry = retryableDelivery(status) && attempt <= DELIVERY_RETRY_SECONDS.length;
    const delay = retry ? DELIVERY_RETRY_SECONDS[attempt - 1] : undefined;
    console.log(JSON.stringify({ delivery: retry ? "failed" : "gave up", run: pending.runId, via: pending.via, attempt, status, error: err.message, retryInSeconds: delay }));
    if (!retry) { await this.setState({ pendingDeliveries: { [runId]: null }, deliveryErrors: { [runId]: { …, final: true } } }); return; }
    await this.setState({ deliveryErrors: { [runId]: { …, nextAt: this.now() + delay * 1000 } } });
    await this.schedule(delay, DELIVERY_TASK, { runId: pending.runId, attempt: attempt + 1 });
    return;
  }
  await this.setState({ deliveredRuns: [...deliveredRuns, runId], pendingDeliveries: { [runId]: null }, deliveryErrors: { [runId]: null } });
}
```

* `retryableDelivery` is true for no answer, a 429 or a 5xx; any other 4xx is final at once, since the API would refuse it again.
* Each try is one log line, `delivery: "failed"` with `retryInSeconds` or `delivery: "gave up"`, with the channel, the target, the status and the API's reason.
* `deliveryErrors[run.id]` in state says what stands between the person and that reply: `status`, `message`, `attempts`, `nextAt` while a try is due, `final` once given up. It is cleared when the reply goes out. (An agent deployed before this shape held one `pendingDelivery`/`deliveryError` slot; those are read as one entry and cleared by the next write.)
* The `DELIVERY_TASK` fires through [`schedule()`](/docs/agent-sdk/interface/schedule) and arrives at `onTask`, which [The Run](/docs/agent-sdk/inside/turn-taking/run) shows; `retryDelivery` finds the `pendingDelivery` for that run and calls `sendPending` with the next attempt. A reply that went out in the meantime, or a run that already delivered, is not delivered twice.
* The window spent is final too: after the seventh wait the pending entry is dropped and `final` is set on its error.
* A run whose approval expired is reconciled by `ready()` after `recover()`: it gets `failedRunId` and `lastError`, and its `replyTo` entry is forgotten, so the state stream shows the outcome.

A reply to `invoke` or `chat` is never delivered anywhere, since it had no channel. It is read from the state stream.

`deliver` is overridable, which is how a class changes where replies go.
