Skip to main content
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:
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

EventDescription
call.startedVoice call connected or started ringing.
call.endedVoice call disconnected before analysis is complete.
call.analyzedPost-call analysis is available.
call.failedCould not place voice call.
sms.message_sentOutbound SMS sent to a contact and persisted.
sms.message_receivedInbound SMS received from a contact and persisted.
sms.message_delivery_updatedCanonical delivery status changed for an outbound SMS.
sms.tool_call_invocationSMS agent invoked a tool during the conversation loop.
sms.tool_call_resultTool result was persisted for an SMS conversation.

Payload Shape

Webhook payloads share a common envelope:
{
  "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:
{
  "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:
{
  "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:
{
  "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.
{
  "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:
{
  "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:
{
  "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:
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.
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.