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

# Webhooks

> Receive real-time call and SMS conversation events.

Webhooks let your integration receive real-time call and SMS conversation events at an HTTPS endpoint that you control.

Webhook registrations configure the default destination for direct `/v1/calls` and `/v1/sms-conversations` events. A `webhookUrl` on an individual create request overrides only that conversation's destination; event subscriptions and signing still come from your webhook registration.

Register a webhook with the event types you want to receive:

```bash theme={null}
curl https://api.pamhq.com/v1/webhooks \
  -X POST \
  -H "Authorization: Bearer $PAM_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/pam-webhooks",
    "events": ["call.started", "call.ended", "call.failed", "sms.message_received", "sms.message_delivery_updated"]
  }'
```

The response includes a signing secret once. Store it securely and use it to verify future webhook payloads.

## Event Types

| Event                          | Description                                            |
| ------------------------------ | ------------------------------------------------------ |
| `call.started`                 | Voice call connected or started ringing.               |
| `call.ended`                   | Voice call disconnected before analysis is complete.   |
| `call.analyzed`                | Post-call analysis is available.                       |
| `call.failed`                  | Could not place voice call.                            |
| `sms.message_sent`             | Outbound SMS sent to a contact and persisted.          |
| `sms.message_received`         | Inbound SMS received from a contact and persisted.     |
| `sms.message_delivery_updated` | Canonical delivery status changed for an outbound SMS. |
| `sms.tool_call_invocation`     | SMS agent invoked a tool during the conversation loop. |
| `sms.tool_call_result`         | Tool result was persisted for an SMS conversation.     |

## Payload Shape

Webhook payloads share a common envelope:

```json theme={null}
{
  "event": "sms.message_received",
  "timestamp": "2026-04-10T14:00:00.000Z",
  "conversationId": "b7ee8d1b-5e3e-4a1a-8f0a-9e2a4f8f1aa2",
  "externalReferenceId": "touchpoint_123",
  "agentId": "2c4a1e72-7a5d-4b6e-9c2a-01a78b7b3f3f",
  "clientOrgId": "acme",
  "channel": "sms",
  "data": {
    "messageId": "f1c2b1a0-3e4d-4a5b-9c7d-0e1f2a3b4c5d",
    "sequenceNumber": 1,
    "from": "+15555550199",
    "to": "+15555550123",
    "content": "Yes please",
    "direction": "inbound",
    "sentAt": "2026-04-10T14:00:00.000Z"
  }
}
```

PAM routes webhooks by `clientOrgId` and event type. A conversation event is delivered only to webhook subscriptions for the conversation's client organization.

If the create request included `externalReferenceId`, PAM includes it as top-level `externalReferenceId` next to `conversationId` on every webhook for that conversation. For pre-conversation `call.failed` events, PAM includes it when the original create-call request provided one. The value is an optional caller-owned reference. PAM does not interpret it, does not require it to be globally unique, and does not use it as an idempotency key.

If the create request included `externalReferenceId`, PAM includes it as top-level `externalReferenceId` next to `conversationId` on every webhook for that conversation. For pre-conversation `call.failed` events, PAM includes it when the original create-call request provided one. The value is an optional caller-owned reference. PAM does not interpret it, does not require it to be globally unique, and does not use it as an idempotency key.

## Lifecycle and Delivery

PAM emits webhook events after it persists the related conversation, call, or message state. For example, `sms.message_received` means the inbound message is already stored by PAM, and `call.ended` means the call end state has been recorded.

Webhook delivery is asynchronous and at least once. Your endpoint may receive duplicate events, and retries can arrive after newer events. Verify the signature, enqueue your own work, and return a `2xx` response quickly.

Use the event payload to deduplicate and reconcile state in your system.

SMS delivery updates are canonical PAM states. They are not raw carrier events. PAM normalizes duplicate and out-of-order provider signals before emitting `sms.message_delivery_updated`.

## Voice Event Examples

`call.started` fires when the call has started:

```json theme={null}
{
  "event": "call.started",
  "timestamp": "2026-04-10T14:00:02.123Z",
  "conversationId": "b7ee8d1b-5e3e-4a1a-8f0a-9e2a4f8f1aa2",
  "externalReferenceId": "touchpoint_123",
  "agentId": "2c4a1e72-7a5d-4b6e-9c2a-01a78b7b3f3f",
  "clientOrgId": "acme",
  "channel": "voice",
  "data": {
    "fromNumber": "+15555550123",
    "toNumber": "+15555550199",
    "startedAt": "2026-04-10T14:00:02.010Z",
    "dispatchRequestId": "7d6f4b32-9f2b-4c9d-8fd8-734dbec6c812",
    "idempotencyKey": "call_01HV7K4ZD3Q9X8P1M8Y2K9A0BC"
  }
}
```

`call.ended` includes the final transcript and call timing:

```json theme={null}
{
  "event": "call.ended",
  "timestamp": "2026-04-10T14:03:41.456Z",
  "conversationId": "b7ee8d1b-5e3e-4a1a-8f0a-9e2a4f8f1aa2",
  "externalReferenceId": "touchpoint_123",
  "agentId": "2c4a1e72-7a5d-4b6e-9c2a-01a78b7b3f3f",
  "clientOrgId": "acme",
  "channel": "voice",
  "data": {
    "fromNumber": "+15555550123",
    "toNumber": "+15555550199",
    "startedAt": "2026-04-10T14:00:02.010Z",
    "endedAt": "2026-04-10T14:03:41.000Z",
    "durationMs": 218990,
    "endReason": "user_hangup",
    "recordingUrl": "https://recordings.example/abc.mp3",
    "transcript": [
      { "role": "assistant", "content": "Hi, this is PAM Motors calling about your recall." },
      { "role": "user", "content": "Thanks, I can bring the car in tomorrow." }
    ],
    "dispatchRequestId": "7d6f4b32-9f2b-4c9d-8fd8-734dbec6c812",
    "idempotencyKey": "call_01HV7K4ZD3Q9X8P1M8Y2K9A0BC"
  }
}
```

`call.analyzed` arrives after post-call analysis is complete:

```json theme={null}
{
  "event": "call.analyzed",
  "timestamp": "2026-04-10T14:04:05.000Z",
  "conversationId": "b7ee8d1b-5e3e-4a1a-8f0a-9e2a4f8f1aa2",
  "externalReferenceId": "touchpoint_123",
  "agentId": "2c4a1e72-7a5d-4b6e-9c2a-01a78b7b3f3f",
  "clientOrgId": "acme",
  "channel": "voice",
  "data": {
    "analysis": {
      "call_summary": "Customer agreed to schedule recall service.",
      "call_successful": true
    },
    "recordingUrl": "https://recordings.example/abc.mp3",
    "dispatchRequestId": "7d6f4b32-9f2b-4c9d-8fd8-734dbec6c812",
    "idempotencyKey": "call_01HV7K4ZD3Q9X8P1M8Y2K9A0BC"
  }
}
```

`call.failed` fires when PAM accepted a voice call request but could not place the call.

PAM tries to place the call before sending this event. You can retry the call with a new request, handle it manually, or treat the attempt as failed in your system.

```json theme={null}
{
  "event": "call.failed",
  "timestamp": "2026-05-10T12:00:00.000Z",
  "externalReferenceId": "touchpoint_123",
  "agentId": "2c4a1e72-7a5d-4b6e-9c2a-01a78b7b3f3f",
  "clientOrgId": "acme",
  "channel": "voice",
  "data": {
    "dispatchRequestId": "7d6f4b32-9f2b-4c9d-8fd8-734dbec6c812",
    "idempotencyKey": "call_01HV7K4ZD3Q9X8P1M8Y2K9A0BC",
    "failureReason": "provider_unavailable",
    "retryable": false,
    "failedAt": "2026-05-10T12:00:00.000Z",
    "message": "Could not place voice call"
  }
}
```

Use `data.dispatchRequestId`, `data.idempotencyKey`, top-level `externalReferenceId`, or your own request metadata to reconcile the failed call attempt.

## SMS Event Examples

`sms.message_sent` fires for the opening SMS and each later assistant SMS:

```json theme={null}
{
  "event": "sms.message_sent",
  "timestamp": "2026-04-10T14:00:00.000Z",
  "conversationId": "b7ee8d1b-5e3e-4a1a-8f0a-9e2a4f8f1aa2",
  "externalReferenceId": "touchpoint_123",
  "agentId": "2c4a1e72-7a5d-4b6e-9c2a-01a78b7b3f3f",
  "clientOrgId": "acme",
  "channel": "sms",
  "data": {
    "messageId": "4b5c6d7e-8f9a-0b1c-2d3e-4f5a6b7c8d9e",
    "sequenceNumber": 1,
    "from": "+15555550123",
    "to": "+15555550199",
    "content": "Hi Ada, this is PAM Motors following up on your service recall.",
    "direction": "outbound",
    "sentAt": "2026-04-10T14:00:00.000Z"
  }
}
```

`sms.message_delivery_updated` is a canonical delivery projection. It is not a raw carrier event:

```json theme={null}
{
  "event": "sms.message_delivery_updated",
  "timestamp": "2026-04-10T14:00:05.000Z",
  "conversationId": "b7ee8d1b-5e3e-4a1a-8f0a-9e2a4f8f1aa2",
  "externalReferenceId": "touchpoint_123",
  "agentId": "2c4a1e72-7a5d-4b6e-9c2a-01a78b7b3f3f",
  "clientOrgId": "acme",
  "channel": "sms",
  "data": {
    "messageId": "4b5c6d7e-8f9a-0b1c-2d3e-4f5a6b7c8d9e",
    "sequenceNumber": 1,
    "from": "+15555550123",
    "to": "+15555550199",
    "direction": "outbound",
    "deliveryStatus": "delivered",
    "sentAt": "2026-04-10T14:00:00.000Z",
    "deliveredAt": "2026-04-10T14:00:05.000Z",
    "failedAt": null,
    "segments": 1,
    "encoding": "GSM_7",
    "updatedAt": "2026-04-10T14:00:05.000Z"
  }
}
```

Tool-call events expose the tool name, tool version, arguments, and result without exposing provider transport details.

## Signature Verification

Every delivery includes headers for the event type and payload signature:

```bash theme={null}
X-Pam-Signature: sha256=abc123def456...
X-Pam-Event: sms.message_received
```

The signature is an HMAC-SHA256 of the raw request body using the webhook endpoint's `secret` as the key.

```javascript theme={null}
const crypto = require("crypto");

function verifySignature(secret, body, signatureHeader) {
  const expected = crypto
    .createHmac("sha256", secret)
    .update(body)
    .digest("hex");
  const received = signatureHeader.replace("sha256=", "");

  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(received));
}
```

## Delivery Notes

* Webhook URLs must use HTTPS.
* The webhook secret is returned only when the endpoint is created.
* Delivery is at least once, so deduplicate events by delivery id and event body.
* Failed deliveries are retried with exponential backoff.
* Delivery times out after 10 seconds.
* Return a `2xx` response quickly after verifying and enqueueing the event.
