> ## 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.

# The Messaging Webhook

```bash theme={null}
telnyx-acp text support-agent "hi"
```

```text {5,13,20} 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
```

`POST /webhooks/messaging_profile/<id>`. `channelRoutes` verifies the signature, then `messagingWebhook` in `channels/index.ts` reads the event:

```ts theme={null}
// channels/index.ts
export async function messagingWebhook(event, env, route, options) {
  const data = event?.data;
  if (!data || data.event_type !== "message.received") return new Response("ok");
  const p = data.payload ?? {};
  const from = p["from"]?.phone_number, to = (p["to"] ?? [])[0]?.phone_number, text = p["text"], id = p["id"];
  // …type checks; media-only messages and other profiles' messages are acknowledged and ignored
  const via = { kind: "messaging", profileId: route.id, from, to };
  const result = await stubFor(env, options.binding, from).receiveVia(text, from, id, via);
  return Response.json({ ok: true, conversation: from, ...result });
}
```

The sender's number is the conversation, and `stubFor` addresses its instance exactly as the mount does:

```ts theme={null}
// channels/index.ts
function stubFor(env, binding, conversation) {
  const ns = env[binding];
  if (!ns || typeof ns.idFromName !== "function") throw new Error(`binding ${binding} is not an actor namespace`);
  return ns.idFromName(encodeAgentName(conversation));
}
```

`encodeAgentName(conversation)` is [`encodeAgentName()`](/docs/agent-sdk/internals/mount/encodeAgentName), the encoding the mount applies to the name in every address.

* `conversation` is the sender's number, so the text reaches the same instance as `/agents/support-agent/<number>`.
* `env[binding]` is the actor namespace that `telnyx.toml` binds, and `idFromName` returns the stub of the instance with that id. The stub calls any public method of the class in-process.

`receiveVia` in `managed/agent.ts` is the second ingress path, and deliberately not [`@rpc`](/docs/agent-sdk/interface/rpc), so only the function's own handlers, which verified the signature, can name where a reply goes:

```ts theme={null}
// managed/agent.ts
async receiveVia(text: string, from: string, key: string, via: ReplyVia): Promise<{ runId: string; duplicate: boolean }> {
  assertReplyVia(via);
  // Written before the run exists, under the message's key: the run's task may
  // run the moment it is queued, and it finds its channel by the key it carries.
  await this.setState({ replyTo: { [key]: via } });
  return this.admit(text, from, key);
}
```

`this.setState()` is [`setState()`](/docs/agent-sdk/interface/setState).

* The entry is keyed by the message's key, not the run's id, and written before `admit` queues the run, since the run's task may start the moment it is queued; [Delivery](/docs/agent-sdk/inside/turn-taking/delivery) reads it as `state.replyTo?.[run.key]`.
* `setState()` merges the patch, so the new `replyTo` entry joins those of other messages.
* A redelivered message carries the same key, so it overwrites its own entry with the same target and `admit` answers `duplicate: true`.
* `noteRun` deletes the entry with `null` once the run ends.

The message id is the key, so a redelivered webhook runs nothing. `via` is kept in state under the message key, before the run exists, and [Delivery](/docs/agent-sdk/inside/turn-taking/delivery) reads it when the run completes.
