Webhook Subscriptions
Subscribe your URLs to TryVox events — call lifecycle, message delivery, custom notifications.
A webhook subscription registers a URL to receive HTTPS callbacks for one or more event_types. TryVox publishes events automatically and dispatches each event to every subscription whose events array matches.
call.ended is the event to subscribe to for call results. Unlike the per-call hangup_url, it does not require the number to be attached to a Voice Application — it is delivered for every call on the account that produces a CDR, including numbers delivered straight to your own SIP endpoint and numbers routed to an AI agent. Its payload is the same shape as the hangup webhook, so one handler serves both.
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.ended", "message.sent"],
"max_retries": 5,
"timeout_ms": 10000
}'Response
201 Created:
{
"id": "8f7a4b2e-91c2-4f3d-9a1e-2c4b6d8f0a1c",
"url": "https://your-app.com/hooks/tryvox",
"events": ["call.ended", "message.sent"],
"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.ended", "message.sent"],
"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.ended", "message.sent"], "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.ended
X-TryVox-Delivery: 1b2c3d4e-5f60-7180-9a2b-3c4d5e6f7a8b
X-TryVox-Timestamp: 1744194600
X-TryVox-Signature: t=1744194600,v1=<hex hmac-sha256>Body:
{
"event": "call.ended",
"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.ended | A call finished. Delivered for every call that produces a CDR, whatever the outcome — check Status in the payload to tell a completed call from a busy, unanswered, cancelled or failed one. |
call.started | Call request accepted. |
call.answered | Call connected. |
message.received | Inbound message received. |
message.sent | Message accepted by the carrier. |
balance.low | Account balance fell below the alert threshold. |
account.updated | Account details changed. |
Use ["*"] to subscribe to all current and future events.
call.ended is the only call event currently dispatched. The others above are accepted on a subscription and reserved, but are not yet emitted — subscribe to them now and they will start arriving when they ship. Any event type not in this table is rejected with INVALID_EVENT_TYPE.
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.