> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://agentphone.to/docs/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://agentphone.to/docs/_mcp/server.

# Webhooks

> Configure webhook endpoints and receive real-time event notifications

Webhooks deliver real-time notifications to your server when events occur in your AgentPhone project. Each project has a single master webhook endpoint that receives all events.

> **Note**
>
> You can also configure per-agent webhooks that override the project default for a specific agent. See [Per-agent webhooks](#per-agent-webhooks) below.

## Events

All inbound messages (SMS, MMS, iMessage, RCS, WhatsApp, and voice) are delivered as a unified `agent.message` event. The `channel` field tells you the source. When a call ends, `agent.call_ended` delivers the full transcript and call analysis. Reactions trigger `agent.reaction`.

| Event                  | Channel                                     | Description                                                                                                                                               |
| ---------------------- | ------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `agent.message`        | `sms`, `mms`, `imessage`, `rcs`, `whatsapp` | An inbound message was received on one of your numbers                                                                                                    |
| `agent.message`        | `voice`                                     | A voice transcript is ready from an active call                                                                                                           |
| `agent.reaction`       | `imessage`, `whatsapp`                      | Someone reacted to a message (tapback on iMessage, emoji on WhatsApp)                                                                                     |
| `agent.contact_shared` | `imessage`                                  | A contact shared their name; the payload includes `address`, `displayName`, `firstName`, `lastName`, `numberId`, and `externalId`. No photo is included.  |
| `agent.call_ended`     | `voice`                                     | A voice call has ended — includes full transcript, duration, and analysis                                                                                 |
| `number.released`      | —                                           | A number you own was released automatically because its `expiresAt` passed. See [Expiring numbers](/docs/documentation/guides/phone-numbers#expiring-numbers). |

## Webhook payload

Each webhook delivery includes the following structure. SMS, iMessage, and voice share the same top-level format:

```json
{
  "event": "agent.message",
  "channel": "sms",
  "timestamp": "2025-01-15T12:00:00Z",
  "agentId": "agt_abc123",
  "data": {
    "messageId": "msg_abc123",
    "conversationId": "conv_def456",
    "numberId": "num_xyz789",
    "from": "+15559876543",
    "to": "+15551234567",
    "message": "Hi, I need help with my order",
    "mediaUrl": null,
    "direction": "inbound",
    "receivedAt": "2025-01-15T12:00:00Z"
  },
  "conversationState": {
    "customerName": "Jane Doe",
    "orderId": "ORD-12345"
  },
  "recentHistory": [
    { "content": "Hello", "direction": "inbound", "channel": "sms", "at": "2025-01-15T11:59:00Z" }
  ]
}
```

### `agent.message` fields by channel

`agent.message` keeps one envelope and changes only the `data` shape by channel:

| Channel                         | Primary content fields in `data`                                                         |
| ------------------------------- | ---------------------------------------------------------------------------------------- |
| `sms`, `mms`, `imessage`, `rcs` | `messageId`, `message`, `mediaUrl`, `mediaUrls`, `from`, `to`, `direction`, `receivedAt` |
| `whatsapp`                      | the same fields, plus `replyTo` when the message is a quote reply or a button/list tap   |
| `voice`                         | `transcript`, `confidence`, `status`, `from`, `to`, `direction`                          |

### WhatsApp

Inbound WhatsApp messages arrive as the same `agent.message` event with `channel: "whatsapp"`. Nothing extra to configure once a WhatsApp Business Account is connected.

**Button and list taps** arrive as ordinary inbound messages. `data.message` is the label the customer tapped, so code that already handles text handles taps for free:

```json
{
  "event": "agent.message",
  "channel": "whatsapp",
  "data": {
    "from": "+14155550123",
    "to": "+12125550199",
    "message": "Buy GTX",
    "direction": "inbound",
    "replyTo": {
      "messageId": "example_id",
      "message": "*Trail Runner GTX* — $139\nLightweight, waterproof, size 8-13 in stock.",
      "mediaUrls": ["https://api.agentphone.ai/v1/messages/example_id/media"]
    }
  }
}
```

The `replyTo` block is what makes taps genuinely useful. It points at the message that offered the choice, so when you have sent five product cards each with a **Buy** button, the label alone cannot tell you which product they meant but `replyTo.messageId` can. It is present on list taps and on customer quote replies too, and omitted entirely on an ordinary message. `mediaUrls` carries the quoted message's attachments when it had any (a second attachment would be `?index=1`), and is an empty array otherwise.

**Reactions** on WhatsApp arrive as `agent.reaction`, the same event iMessage tapbacks use, with the emoji in `reactionType`.

### Group chats

Group chats are an iMessage feature. When your number is part of a group thread, inbound group messages arrive as the same `agent.message` event (`channel: "imessage"`), with two additions to `data`:

* `data.group` — the group roster and metadata
* `data.senderIdentifier` — which member sent this specific message

For one-to-one conversations these two fields are **omitted entirely** (not `null`), so the presence of `data.group` is a reliable "is this a group?" check. SMS numbers cannot participate in group threads.

```json
{
  "event": "agent.message",
  "channel": "imessage",
  "timestamp": "2025-01-15T12:00:00Z",
  "agentId": "agt_abc123",
  "data": {
    "messageId": "msg_abc123",
    "conversationId": "conv_def456",
    "numberId": "num_xyz789",
    "from": "+15554444444",
    "to": "+15551234567",
    "message": "Are we still on for Friday?",
    "mediaUrl": null,
    "direction": "inbound",
    "receivedAt": "2025-01-15T12:00:00Z",
    "senderIdentifier": "+15554444444",
    "group": {
      "isGroup": true,
      "groupId": "grp_abc123",
      "groupName": "Trip Planning",
      "groupIconUrl": null,
      "participants": [
        { "identifier": "+15554444444", "name": "Alice" },
        { "identifier": "+15555555555", "name": null }
      ]
    }
  },
  "conversationState": null,
  "recentHistory": [
    { "content": "Sounds good", "direction": "inbound", "channel": "imessage", "at": "2025-01-15T11:59:00Z", "senderIdentifier": "+15555555555" }
  ]
}
```

| Field                     | Description                                                                                                                                                                                  |
| ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `data.group.isGroup`      | Always `true` when present. Stable for the life of the thread.                                                                                                                               |
| `data.group.groupId`      | Provider-assigned group identifier (`grp_...`). Stable, and the value you reply to.                                                                                                          |
| `data.group.groupName`    | Group display name when known, else `null`.                                                                                                                                                  |
| `data.group.groupIconUrl` | Group icon URL when known, else `null`.                                                                                                                                                      |
| `data.group.participants` | Roster as last seen: an array of `{ identifier, name }`. Refreshed on every inbound message as members join or leave.                                                                        |
| `data.senderIdentifier`   | The member who sent this message. Use this for per-message attribution rather than reconstructing it from the roster. Each `recentHistory` entry in a group also carries `senderIdentifier`. |

The webhook is self-contained: you do not need to fetch the conversation before routing or replying. To reply to the group, send to the `groupId` (not `data.from`, which would start a one-to-one with that member): call [`POST /v1/messages`](/docs/documentation/guides/messages) with `to_number` set to the `grp_...` value.

For voice events, the `data` field contains the transcript instead:

```json
{
  "event": "agent.message",
  "channel": "voice",
  "timestamp": "2025-01-15T14:00:05Z",
  "agentId": "agt_abc123",
  "data": {
    "callId": "call_abc123",
    "numberId": "num_xyz789",
    "from": "+15559876543",
    "to": "+15551234567",
    "status": "in-progress",
    "transcript": "I need help with my order",
    "confidence": 0.95,
    "direction": "inbound"
  },
  "conversationState": null,
  "recentHistory": [
    { "content": "Hello, how can I help?", "direction": "outbound", "channel": "voice", "at": "2025-01-15T14:00:00Z" }
  ]
}
```

When a call ends, `agent.call_ended` delivers the full transcript (not limited by `contextLimit`), call duration, and optional analysis:

```json
{
  "event": "agent.call_ended",
  "channel": "voice",
  "timestamp": "2025-01-15T14:05:30Z",
  "agentId": "agt_abc123",
  "data": {
    "callId": "call_ghi012",
    "numberId": "num_xyz789",
    "from": "+15559876543",
    "to": "+15551234567",
    "direction": "inbound",
    "status": "completed",
    "startedAt": "2025-01-15T14:00:00Z",
    "endedAt": "2025-01-15T14:05:30Z",
    "durationSeconds": 330,
    "disconnectionReason": "agent_hangup",
    "transcript": [
      { "role": "agent", "content": "Hello! How can I help you today?" },
      { "role": "user", "content": "I need help with my order." },
      { "role": "agent", "content": "Sure! Could you provide your order number?" }
    ],
    "summary": "Customer called about an order inquiry. Agent helped locate the order.",
    "userSentiment": "Positive",
    "callSuccessful": true
  }
}
```

> **Note**
>
> `agent.call_ended` is fire-and-forget — it does not expect a response body. Return `200 OK` to acknowledge receipt.

When someone reacts to an iMessage your agent sent, `agent.reaction` is delivered:

```json
{
  "event": "agent.reaction",
  "channel": "imessage",
  "timestamp": "2025-01-15T12:01:00Z",
  "agentId": "agt_abc123",
  "data": {
    "conversationId": "conv_def456",
    "numberId": "num_xyz789",
    "reactionType": "love",
    "fromNumber": "+15559876543",
    "direction": "inbound",
    "messageId": "msg_001",
    "messageBody": "Hey! How can I help?",
    "messageMediaUrl": null,
    "createdAt": "2025-01-15T12:01:00Z"
  }
}
```

The `reactionType` is one of: `love`, `like`, `dislike`, `laugh`, `emphasize`, `question`.

### Voice webhook responses

For voice events, your webhook must return a JSON object (`{...}`) that tells the agent what to say. Non-object responses (numbers, strings, arrays) are ignored and the caller hears silence.

**Streaming response (recommended):** Return `Content-Type: application/x-ndjson` with newline-delimited JSON chunks. TTS starts speaking on the first chunk while your server continues processing.

```
{"text": "Let me check that for you.", "interim": true}
{"text": "I found 3 results for your order."}
```

Mark interim chunks with `"interim": true` — the final chunk (without `interim`) closes the turn. Voice webhooks have a **30-second default timeout** (configurable from 5–120 seconds per webhook) — always stream an interim chunk before doing slow work like LLM tool calls.

**Simple response:** Return a single JSON object for instant replies.

```json
{ "text": "How can I help you?" }
```

| Field          | Type    | Description                                                                                                                                                                                                                                                                   |
| -------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `text`         | string  | Text to speak to the caller                                                                                                                                                                                                                                                   |
| `hangup`       | boolean | Set to `true` to end the call after speaking                                                                                                                                                                                                                                  |
| `action`       | string  | `"transfer"` to cold-transfer the call (requires `transferNumber` on the agent), `"hangup"` to end it                                                                                                                                                                         |
| `digits`       | string  | DTMF digits to press on the keypad (e.g. `"1"`, `"123"`, `"1*#"`). Used to navigate IVR menus and automated phone systems. Aliases: `press_digit`, `dtmf`                                                                                                                     |
| `send_message` | object  | Send the call counterparty an SMS during the call. Shape: `{"body": "..."}`. The recipient defaults to the other party on this call; only pass `to` if your account is allowed to text arbitrary numbers. Aliases: `send_sms`, or `{"action": "send_message", "body": "..."}` |
| `interim`      | boolean | NDJSON only — marks a chunk as interim so TTS speaks it while the turn stays open                                                                                                                                                                                             |

Example: confirming an appointment by text while the agent stays on the call.

```json
{
  "text": "Sending you a confirmation now.",
  "send_message": { "body": "Confirmed: Tue 3pm with Dr. Lee. Reply STOP to cancel." }
}
```

### `number.released`

Sent when a number is released by its `expiresAt`. Signed and logged like every other event, but it is attempted once with no automatic retries. See the event-specific retry policy below. If your endpoint was down at that moment, the delivery still appears in your [webhook deliveries](#webhook-deliveries) with its status.

```json
{
  "event": "number.released",
  "timestamp": "2026-11-15T00:00:12Z",
  "agentId": "agt_abc123",
  "data": {
    "numberId": "num_xyz789",
    "phoneNumber": "+15551234567",
    "externalId": "lease-4421",
    "reason": "expired",
    "expiresAt": "2026-11-15T00:00:00Z"
  }
}
```

## Security

Each webhook delivery includes these headers:

| Header                | Description                                                   |
| --------------------- | ------------------------------------------------------------- |
| `X-Webhook-Signature` | HMAC-SHA256 signature (`sha256=<hex_digest>`)                 |
| `X-Webhook-Timestamp` | Unix timestamp of the delivery (for replay-attack protection) |
| `X-Webhook-ID`        | Unique delivery ID (use for idempotency)                      |
| `X-Webhook-Event`     | Event type (e.g. `agent.message`) for fast filtering          |

The signature is computed over the **timestamp + body**: the signed string is `{timestamp}.{raw_body}`, hashed with HMAC-SHA256 using your webhook secret. Always verify the timestamp is within 5 minutes to prevent replay attacks.

### Verification example (Node.js)

Save as `verify-webhook.mjs`. Pass the **original request bytes**, before JSON parsing. Parsing and then re-serializing JSON changes the signed bytes.

**`verify-webhook.mjs`**

```javascript title="verify-webhook.mjs"
import { createHmac, timingSafeEqual } from "node:crypto";

export function verifyWebhook(rawBody, signature, timestamp, secret) {
  if (!Buffer.isBuffer(rawBody) || typeof secret !== "string" || !secret) return false;
  if (typeof timestamp !== "string" || !/^\d{1,12}$/.test(timestamp)) return false;
  if (typeof signature !== "string" || !/^sha256=[0-9a-f]{64}$/.test(signature)) return false;
  const seconds = Number(timestamp);
  if (!Number.isSafeInteger(seconds) || Math.abs(Date.now() / 1000 - seconds) > 300) return false;
  const expected = createHmac("sha256", secret)
    .update(timestamp + ".")
    .update(rawBody)
    .digest();
  const received = Buffer.from(signature.slice(7), "hex");
  return received.length === expected.length && timingSafeEqual(received, expected);
}
```

### Verification example (Python)

Save as `verify_webhook.py`:

**`verify_webhook.py`**

```python title="verify_webhook.py"
import hashlib
import hmac
import re
import time


def verify_webhook(raw_body: bytes, signature: str, timestamp: str, secret: str) -> bool:
    if not isinstance(raw_body, bytes) or not isinstance(secret, str) or not secret:
        return False
    if not isinstance(timestamp, str) or not re.fullmatch(r"[0-9]{1,12}", timestamp):
        return False
    if not isinstance(signature, str) or not re.fullmatch(r"sha256=[0-9a-f]{64}", signature):
        return False
    if abs(time.time() - int(timestamp)) > 300:
        return False
    signed = timestamp.encode("ascii") + b"." + raw_body
    expected = hmac.new(secret.encode("utf-8"), signed, hashlib.sha256).hexdigest()
    return hmac.compare_digest("sha256=" + expected, signature)
```

Both examples reject missing or malformed headers, stale/future timestamps outside a five-minute window, and invalid signatures. Framework handlers are in [Code examples](/docs/documentation/reference/code-examples).

## Retry behavior

Delivery depends on the event:

| Event                                                                           | Response and retry policy                                                                                                                                              |
| ------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `agent.message`, non-voice                                                      | Return a 2xx status within the five-second delivery timeout. Failed delivery is retried up to seven times (eight attempts total). Response text does not send a reply. |
| `agent.message`, voice                                                          | Return the JSON/NDJSON response for the live turn. The configurable voice timeout applies. There is no delayed retry queue for a live turn.                            |
| `agent.call_ended`, `agent.reaction`, `agent.contact_shared`, `number.released` | Notification only. Return 2xx. These are attempted once, with no automatic retry queue.                                                                                |

For non-voice `agent.message`, the delay **after the previous failed attempt** is:

| Attempt | Delay      |
| ------- | ---------- |
| 1       | Immediate  |
| 2       | 15 seconds |
| 3       | 1 minute   |
| 4       | 5 minutes  |
| 5       | 30 minutes |
| 6       | 2 hours    |
| 7       | 6 hours    |
| 8       | 12 hours   |

Delays have ±10% jitter and are picked up by a periodic worker, so they are not exact delivery times. Timeouts, network failures, and non-2xx HTTP responses can be retried. A permanently blocked destination URL is not retried. After the last failed attempt, the delivery is marked failed; inspect [delivery history](#webhook-deliveries).

### Handling duplicate deliveries

The same message delivery keeps its `X-Webhook-ID` across retries. Verify the signature first, then atomically record the ID and enqueue work in durable storage before acknowledging. If the ID is already recorded, acknowledge without repeating the side effect. An in-memory set is insufficient across restarts or multiple instances.

For text events, use `POST /v1/messages` to reply. Keep the inbound `data.numberId` so the response uses the same line, and use `data.group.groupId` for a group reply. For voice, return speech in the current HTTP response; acknowledging with an empty body produces no speech.

## Conversation state

Store custom metadata on conversations to persist context across messages. This state is included in every webhook payload as `conversationState`.

```bash
curl -X PATCH "https://api.agentphone.ai/v1/conversations/conv_abc123" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "metadata": {
      "customerName": "John Smith",
      "orderId": "ORD-12345",
      "topic": "shipping"
    }
  }'
```

This metadata appears in subsequent webhook payloads as `conversationState`, enabling your AI backend to maintain context across messages without managing state yourself.

## Create or update webhook

Configure the webhook endpoint for your project. Each project can have one active master webhook. If a webhook already exists, it will be updated.

```
POST /v1/webhooks
```

### Request body

| Field          | Type            | Required | Description                                                                                                |
| -------------- | --------------- | -------- | ---------------------------------------------------------------------------------------------------------- |
| `url`          | string          | Yes      | The HTTPS URL to receive webhook deliveries                                                                |
| `contextLimit` | integer or null | No       | Number of recent messages to include in webhook payloads (0-50, default: 10). Set to 0 to disable history. |
| `timeout`      | integer or null | No       | Max seconds to wait for a webhook response (5-120, default: 30). Applies to voice webhook requests.        |

> **Warning**
>
> Save the `secret` returned every time you create or update a webhook. It can change; endpoints sharing the same URL within an account can reuse a secret. Do not assume the previous value remains valid.

### Example

```bash
curl -X POST "https://api.agentphone.ai/v1/webhooks" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://your-server.com/webhook", "contextLimit": 10}'
```

```json
{
  "id": "wh_mno678",
  "url": "https://your-server.com/webhook",
  "secret": "whsec_abc123...",
  "status": "active",
  "contextLimit": 10,
  "timeout": 30,
  "createdAt": "2025-01-15T11:00:00Z"
}
```

## Get webhook

Get the current webhook configuration for your project. Returns `null` if no webhook is configured.

```
GET /v1/webhooks
```

## Delete webhook

Remove the master webhook configuration. Events will no longer be delivered.

```
DELETE /v1/webhooks
```

### Example

```bash
curl -X DELETE "https://api.agentphone.ai/v1/webhooks" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

## Webhook deliveries

View the delivery history for your master webhook to monitor delivery status and debug issues.

```
GET /v1/webhooks/deliveries
```

### Query parameters

| Parameter | Type    | Required | Default | Description                               |
| --------- | ------- | -------- | ------- | ----------------------------------------- |
| `limit`   | integer | No       | 50      | Number of results to return (max 100)     |
| `offset`  | integer | No       | 0       | Number of results to skip (min 0)         |
| `hours`   | integer | No       | null    | Optional lookback window in hours (1-168) |

### Example

```bash
curl -X GET "https://api.agentphone.ai/v1/webhooks/deliveries?limit=10&offset=0" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

```json
{
  "items": [
    {
      "id": "del_pqr901",
      "messageId": "msg_001",
      "eventType": "agent.message",
      "status": "success",
      "httpStatus": 200,
      "errorMessage": null,
      "attemptCount": 1,
      "lastAttemptAt": "2025-01-15T12:00:01Z",
      "nextRetryAt": null,
      "createdAt": "2025-01-15T12:00:01Z"
    }
  ],
  "total": 124,
  "offset": 0,
  "limit": 10
}
```

## Test webhook

Send a test webhook to verify your endpoint is working correctly. This sends a synthetic `agent.message` event through the real delivery path (same signature, headers and payload shape as production) to your configured URL. Test events carry a top-level `"test": true` so your handler can tell them apart from live traffic.

```
POST /v1/webhooks/test
```

### Request body (optional)

By default the event uses sample values (`from` `+15551234567`, `to` `+15559876543`, a fixed message). Override any of them so a handler that routes on the numbers can be exercised end to end, for example before a business has bought a number or from an automated test suite.

| Field     | Type   | Description                                             |
| --------- | ------ | ------------------------------------------------------- |
| `from`    | string | Sender number in E.164 format                           |
| `to`      | string | Recipient number in E.164 format                        |
| `message` | string | Message text, up to 1600 characters                     |
| `channel` | string | `sms` (default), `imessage`, `mms`, `whatsapp` or `rcs` |

The event is only ever delivered to the calling account's own webhook. The numbers are reported in the payload as given; they do not need to belong to the account.

### Example

```bash
curl -X POST "https://api.agentphone.ai/v1/webhooks/test" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "from": "+14155550123",
    "to": "+12125550199",
    "message": "Do you have a table for two tonight?",
    "channel": "sms"
  }'
```

```json
{
  "success": true,
  "httpStatus": 200,
  "errorMessage": null
}
```

Your endpoint receives the usual `agent.message` payload with `data.from`, `data.to` and `data.message` set to the values above, plus `"test": true` at the top level. The per-agent variant, `POST /v1/agents/{agentId}/webhook/test`, accepts the same body.

## Per-agent webhooks

In addition to the project-level default webhook, you can configure an individual webhook endpoint for each agent. When a per-agent webhook is set, it **overrides** the project default for that agent's events — the event is delivered to exactly one endpoint, never both. This is useful when you run multiple agents and want to route their events to different backend services or pipelines.

### How routing works

When an event occurs (e.g., an inbound message to an agent's number):

1. If the agent has its **own webhook** configured, the event is delivered **only** to the agent's webhook.
2. If the agent does **not** have its own webhook, the event is delivered to the **project default webhook**.

Events are never duplicated across both endpoints. Per-agent webhooks use the same payload format, signature verification, and retry behavior described above.

### Create or update an agent webhook

Register or update a webhook URL for a specific agent.

```
POST /v1/agents/{agent_id}/webhook
```

| Field          | Type            | Required | Description                                                                                                |
| -------------- | --------------- | -------- | ---------------------------------------------------------------------------------------------------------- |
| `url`          | string          | Yes      | The HTTPS URL to receive webhook deliveries                                                                |
| `contextLimit` | integer or null | No       | Number of recent messages to include in webhook payloads (0-50, default: 10). Set to 0 to disable history. |
| `timeout`      | integer or null | No       | Max seconds to wait for a webhook response (5-120, default: 30). Applies to voice webhook requests.        |

> **Warning**
>
> Save the `secret` returned every time you create or update a webhook. It can change; endpoints sharing the same URL within an account can reuse a secret. Do not assume the previous value remains valid.

```bash
curl -X POST "https://api.agentphone.ai/v1/agents/agt_abc123/webhook" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://your-server.com/agent-webhook", "contextLimit": 5}'
```

```json
{
  "id": "wh_xyz789",
  "url": "https://your-server.com/agent-webhook",
  "secret": "whsec_def456...",
  "status": "active",
  "contextLimit": 5,
  "timeout": 30,
  "createdAt": "2025-01-15T11:00:00Z"
}
```

### Get an agent webhook

Get the webhook configuration for a specific agent. Returns `null` if no webhook is configured.

```
GET /v1/agents/{agent_id}/webhook
```

```bash
curl -X GET "https://api.agentphone.ai/v1/agents/agt_abc123/webhook" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

### Delete an agent webhook

Remove the webhook for a specific agent. Events revert to the project default webhook after deletion.

```
DELETE /v1/agents/{agent_id}/webhook
```

```bash
curl -X DELETE "https://api.agentphone.ai/v1/agents/agt_abc123/webhook" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

### Agent webhook deliveries

View recent delivery attempts for a specific agent's webhook.

```
GET /v1/agents/{agent_id}/webhook/deliveries
```

| Parameter | Type    | Required | Default | Description                           |
| --------- | ------- | -------- | ------- | ------------------------------------- |
| `limit`   | integer | No       | 50      | Number of results to return (max 100) |

```bash
curl -X GET "https://api.agentphone.ai/v1/agents/agt_abc123/webhook/deliveries?limit=10" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

```json
[
  {
    "id": "del_abc123",
    "messageId": "msg_001",
    "eventType": "agent.message",
    "status": "success",
    "httpStatus": 200,
    "errorMessage": null,
    "attemptCount": 1,
    "lastAttemptAt": "2025-01-15T12:00:01Z",
    "nextRetryAt": null,
    "createdAt": "2025-01-15T12:00:01Z"
  }
]
```

### Test an agent webhook

Send a test webhook to verify the agent's endpoint is working correctly.

```
POST /v1/agents/{agent_id}/webhook/test
```

```bash
curl -X POST "https://api.agentphone.ai/v1/agents/agt_abc123/webhook/test" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

```json
{
  "success": true,
  "httpStatus": 200,
  "errorMessage": null
}
```