Back to docs
Webhooks

Webhooks

Receive event POSTs at your own HTTPS endpoint when calls finish, voicemails arrive, and orders come in. Every delivery is signed with HMAC-SHA256, retried with exponential backoff, and logged in your dashboard.

1. Register your endpoint

Head to Settings → Webhooks and click New endpoint. Provide an HTTPS URL (or http://localhost during development) and optionally choose which events you want. Leave none selected to receive all.

On creation we'll show your signing secret once. Save it in your env vars or secret manager immediately. We never display it again (we only store it for HMAC computation).

2. Event payload format

Every POST has this Stripe-style envelope:

{
  "id": "evt_4f8a92e8cdd14f12bc41a7d23ee0f33b",
  "type": "call.completed",
  "created": 1748630593,
  "api_version": "v1",
  "data": {
    "object": {
      "object": "call",
      "id": "call_7f3a...",
      "agent_id": "ag_8a0c...",
      "phone": "+14155550100",
      "duration_sec": 87,
      "outcome": "completed",
      "transcript": "Caller: Hi I'd like to...",
      "recording_url": "https://...mp3",
      "transferred": false,
      "transfer_to": null,
      "transfer_succeeded": null,
      "detected_language": "en",
      "created_at": "2026-05-30T18:03:11Z"
    }
  },
  "org_id": "1f95..."
}

The data.object field carries the same shape as the corresponding REST API resource, so anything you can do with GET /api/v1/calls/... you can do directly with the webhook payload.

3. Event types

call.completed

Call completed

Fires after every call ends, with outcome + transcript + duration.

call.transferred

Call transferred

Fires when a caller is warm-transferred to your staff line.

voicemail.created

Voicemail received

Fires when a caller leaves a voicemail (transcribed).

order.created

Order created

Restaurants: fires when an order is confirmed by the AI agent.

booking.created

Booking created

Services: fires when an appointment is booked by the AI agent.

4. Verifying signatures

Every POST includes:

X-Proon-Signature: t=1748630593,v1=8c4d2f7c8...

Recompute the signature and compare. Reject requests where t is older than 5 minutes (replay-attack window) or whose v1doesn't match what you compute.

Node.js example

import crypto from 'crypto'

function verifyProonSignature(rawBody, header, secret) {
  const parts = Object.fromEntries(
    header.split(',').map(kv => kv.split('='))
  )
  const t = parts.t
  const expected = parts.v1
  if (!t || !expected) return false

  // Reject old timestamps (replay protection)
  if (Math.abs(Date.now() / 1000 - parseInt(t, 10)) > 300) return false

  const hmac = crypto
    .createHmac('sha256', secret)
    .update(`${t}.${rawBody}`)
    .digest('hex')

  return crypto.timingSafeEqual(
    Buffer.from(expected, 'hex'),
    Buffer.from(hmac, 'hex'),
  )
}

Python example

import hmac, hashlib, time

def verify_proon_signature(raw_body: bytes, header: str, secret: str) -> bool:
    parts = dict(item.split('=') for item in header.split(','))
    t = parts.get('t')
    expected = parts.get('v1')
    if not t or not expected:
        return False
    if abs(time.time() - int(t)) > 300:
        return False
    digest = hmac.new(
        secret.encode(),
        f"{t}.{raw_body.decode()}".encode(),
        hashlib.sha256,
    ).hexdigest()
    return hmac.compare_digest(digest, expected)

5. Retry policy

If your endpoint returns a non-2xx status (or times out after 10 seconds), we retry on this schedule:

AttemptDelay from previous
1immediate
21 minute
35 minutes
430 minutes
52 hours
612 hours (final attempt)

After 6 failed attempts the delivery is marked deadand won't retry automatically. You can see all delivery attempts (and their HTTP status / error) in Settings → Webhooks.

6. Best practices

  • Respond quickly. Return 200 within 10 seconds, then do heavy work async in your own queue.
  • Be idempotent. We may re-send the same event (same id) on retry. De-dupe by id in your handler.
  • Verify the signature on every request. Skip verification only in local development.
  • Use the test button. Settings → Webhooks → Test sends a synthetic call.completed within ~1 minute so you can verify your endpoint without waiting for a real call.
  • Watch consecutive failures. The dashboard flags endpoints that have failed 3+ times in a row. We don't auto-disable them, but you should investigate before the customer call backlog gets stale.