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.
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.
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.
call.completedCall completed
Fires after every call ends, with outcome + transcript + duration.
call.transferredCall transferred
Fires when a caller is warm-transferred to your staff line.
voicemail.createdVoicemail received
Fires when a caller leaves a voicemail (transcribed).
order.createdOrder created
Restaurants: fires when an order is confirmed by the AI agent.
booking.createdBooking created
Services: fires when an appointment is booked by the AI agent.
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.
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'),
)
}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)If your endpoint returns a non-2xx status (or times out after 10 seconds), we retry on this schedule:
| Attempt | Delay from previous |
|---|---|
| 1 | immediate |
| 2 | 1 minute |
| 3 | 5 minutes |
| 4 | 30 minutes |
| 5 | 2 hours |
| 6 | 12 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.
id) on retry. De-dupe by id in your handler.call.completed within ~1 minute so you can verify your endpoint without waiting for a real call.