Webhook Subscriptions
Subscribe your URLs to TryVox events — call lifecycle, message delivery, custom notifications.
Webhook Subscriptions
A webhook subscription registers a URL to receive HTTPS callbacks for one or more event_types. TryVox publishes events automatically (call lifecycle, message delivery, recording completion, custom notifications you fire via Send notification) and dispatches each event to every subscription whose events array matches.
This is the right surface for hooking up your own infrastructure to TryVox events. For per-call webhooks (answer_url, hangup_url), use the inline URL fields on the call request — those are dispatched once per call, not per subscription.
Endpoints
| Method | Path | Purpose |
|---|---|---|
POST | /v1/notifications/webhooks | Create a subscription |
GET | /v1/notifications/webhooks | List subscriptions |
GET | /v1/notifications/webhooks/{id} | Get a subscription |
PUT | /v1/notifications/webhooks/{id} | Update a subscription |
DELETE | /v1/notifications/webhooks/{id} | Delete a subscription |
Create
POST /v1/notifications/webhooksRequest body
| Field | Type | Required | Description |
|---|---|---|---|
url | string | yes | HTTPS URL TryVox POSTs to. HTTP is rejected. |
events | string[] | yes | Event types to subscribe to. Use ["*"] for all events. |
secret | string | no | Shared secret used to sign each delivery (X-TryVox-Signature header). If omitted, TryVox generates one and returns it once. |
active | boolean | no | Default: true. Inactive subscriptions are skipped during dispatch. |
max_retries | integer | no | Retries on non-2xx response. Default: 3. |
timeout_ms | integer | no | Per-attempt timeout. Default: 5000 (5 seconds). |
Example
curl -X POST https://api.tryvox.io/v1/notifications/webhooks \
-u $TRYVOX_AUTH_ID:$TRYVOX_AUTH_TOKEN \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-app.com/hooks/tryvox",
"events": ["call.completed", "message.delivered", "recording.completed"],
"max_retries": 5,
"timeout_ms": 10000
}'Response
201 Created:
{
"id": "8f7a4b2e-91c2-4f3d-9a1e-2c4b6d8f0a1c",
"url": "https://your-app.com/hooks/tryvox",
"events": ["call.completed", "message.delivered", "recording.completed"],
"active": true,
"max_retries": 5,
"timeout_ms": 10000,
"secret": "whsec_3b2c1a0d4e5f60718293a4b5c6d7e8f9",
"created_at": "2026-04-09T10:30:00Z"
}The secret is returned once at create time and never again. Store it. If you lose it, Update with a new secret.
List
GET /v1/notifications/webhooks200 OK:
{
"data": [
{
"id": "8f7a4b2e-91c2-4f3d-9a1e-2c4b6d8f0a1c",
"url": "https://your-app.com/hooks/tryvox",
"events": ["call.completed", "message.delivered"],
"active": true,
"max_retries": 5,
"timeout_ms": 10000,
"created_at": "2026-04-09T10:30:00Z",
"updated_at": "2026-04-09T10:30:00Z"
}
]
}secret is not returned on list / get — only on create / update.
Get
GET /v1/notifications/webhooks/{id}404 NOT_FOUND if the subscription doesn't exist on this tenant.
Update
PUT /v1/notifications/webhooks/{id}PATCH-style: only fields present in the body are applied. Include secret to rotate; the new value is returned in the response (the only time you'll see it).
curl -X PUT https://api.tryvox.io/v1/notifications/webhooks/$WEBHOOK_ID \
-u $TRYVOX_AUTH_ID:$TRYVOX_AUTH_TOKEN \
-H "Content-Type: application/json" \
-d '{"events": ["call.completed", "call.failed", "message.delivered"], "active": true}'200 OK with the updated subscription.
Delete
DELETE /v1/notifications/webhooks/{id}204 No Content. Existing in-flight deliveries complete; no new dispatches.
Delivery format
Each delivery is a POST to your URL with:
Content-Type: application/json
X-TryVox-Event: call.completed
X-TryVox-Delivery: 1b2c3d4e-5f60-7180-9a2b-3c4d5e6f7a8b
X-TryVox-Timestamp: 1744194600
X-TryVox-Signature: t=1744194600,v1=<hex hmac-sha256>Body:
{
"event": "call.completed",
"delivery_id": "1b2c3d4e-5f60-7180-9a2b-3c4d5e6f7a8b",
"occurred_at": "2026-04-09T10:30:00Z",
"data": { /* event-specific payload */ }
}Verifying the signature
Compute HMAC-SHA256(secret, "{X-TryVox-Timestamp}.{raw-body}") and compare hex to the v1= value in X-TryVox-Signature. Reject deliveries whose timestamp is more than 5 minutes off — that defeats replay attacks.
import hmac, hashlib, time
def verify(secret, header_sig, header_ts, raw_body):
if abs(time.time() - int(header_ts)) > 300:
return False
expected = hmac.new(
secret.encode(),
f"{header_ts}.{raw_body}".encode(),
hashlib.sha256
).hexdigest()
sent = dict(p.split("=", 1) for p in header_sig.split(",")).get("v1")
return hmac.compare_digest(expected, sent or "")Retries
A delivery is retried when:
- The response status is not
2xx. - The connection times out (
timeout_ms). - The TLS handshake fails.
Retries use exponential backoff: 1s → 4s → 16s → 64s → 256s, capped at max_retries. After all retries are exhausted the delivery is marked failed and surfaces in the delivery log — TryVox does not keep retrying indefinitely.
Make your handler idempotent — duplicate deliveries can occur on retry edges and on the rare double-publish.
Event types
| Event | When |
|---|---|
call.queued | Outbound call request accepted. |
call.ringing | Call is ringing the destination. |
call.answered | Call connected. |
call.completed | Call ended normally. |
call.failed | Call failed before answer. |
message.queued | Message accepted by TryVox. |
message.sent | Accepted by Meta / SMS carrier. |
message.delivered | Recipient handset received it. |
message.read | Recipient opened it (only with read receipts on). |
message.failed | Delivery failed permanently. |
recording.completed | Recording finalised and uploaded. |
transcription.completed | Speech-to-text finished. |
conversation.opened | New conversation created. |
<custom> | Anything you publish via Send notification. |
Use ["*"] to subscribe to all current and future events.
Errors
| Status | Code | Reason |
|---|---|---|
| 400 | VALIDATION_FAILED | Missing field, url not HTTPS, events empty |
| 401 | INVALID_CREDENTIALS | Auth ID / Auth Token bad |
| 404 | NOT_FOUND | Subscription not on this tenant |
Notes
- HTTPS only. HTTP URLs are rejected on create and update.
- One subscription per URL is recommended. Multiple subscriptions to the same URL with overlapping event filters duplicate deliveries.
active: falseis the right way to pause without losing the subscription. TryVox skips inactive subscriptions but keeps the configuration and delivery log.