Webhooks
Register webhook endpoints to receive lifecycle and post-call intelligence events from Rymi.
Register Webhook
Request Body
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
url | string | Yes | — | HTTPS endpoint to receive events. Must pass URL safety rules |
events | string[] | Yes | — | Events to subscribe to (see Available Events) |
secret | string | Yes | — | HMAC signing secret used to verify deliveries. Min 16 characters |
alert_email | string | No | — | Optional email address to notify when delivery fails after retries. Throttled to one alert per hour per endpoint |
redaction_level | string | No | partial | How much call data to include in deliveries: none, partial, or full |
Available Events
The dashboard's webhook manager mirrors this catalog under Settings → Webhooks. New event types arrive without breaking existing subscribers, so make your handlers ignore unknown payload events. (Every payload carries event; only API- and notification-origin events add a duplicate top-level type — gateway call events do not. See Payload Fields.)
Call events
| Event | Description |
|---|---|
call.started | A call connected and the live session began. Fires once per call (and once per participant leg on telephony, which adds a participant_id) |
call.failed | A call could not start or ended in a failure state (for example, insufficient funds) |
call.completed | A call ended with transcript and metadata. Fires together with call.intelligence.ready once post-call processing finishes |
call.intelligence.ready | Post-call analysis (summary, extraction, evaluation) is ready |
Agent events
| Event | Description |
|---|---|
agent.published | An agent was published to a live version |
agent.failed | An agent runtime hit a fatal error |
agent.number_assigned | A phone number was attached to an agent |
Campaign events
See the Campaigns guide for concepts and the Campaigns API for the endpoints that drive these events.
| Event | Description |
|---|---|
campaign.created | A campaign was created (draft status) |
campaign.launched | A campaign was launched: blockers passed, agent_snapshot_id stamped, members flipped to ready |
campaign.paused | A running campaign was paused |
campaign.completed | A campaign finished: zero members remain in a non-terminal state |
campaign.batch.queued | A scheduler sweep claimed a batch of due members |
campaign.batch.completed | A batch finished processing (all its attempts resolved) |
campaign.attempt.completed | One campaign attempt (one call) resolved to a terminal outcome |
campaign.followup.completed | A follow-up job reached a terminal status (sent, failed, blocked, done) |
campaign.report.ready | A campaign report rollup was (re)computed and is ready to read |
campaign.suggestion.created | The improve engine produced a new reviewable suggestion |
Campaign webhook payloads carry ids and rollup fields only — never transcripts or recordings. Fetch full call detail via the Calls API using the call_id on the related attempt.
campaign.launched and campaign.paused are live today. The remaining campaign events in the table above are accepted at registration time but not yet emitted — see Reserved event names. A related, separate event, campaign.followup.due, is delivered to your endpoint as the payload of a webhook-kind follow-up job itself (configured per-campaign in automation_policy, not a subscription event) — see Follow-ups.
Billing events
| Event | Description |
|---|---|
billing.topup_confirmed | A wallet top-up was captured |
billing.payment_failed | A payment attempt failed |
billing.low_balance | Wallet balance dropped below the low-balance threshold (delivered via the notification → webhook bridge) |
billing.balance_depleted | Wallet balance was exhausted, or was insufficient to admit a call (delivered via the notification → webhook bridge) |
Billing events need the webhook notification channel
billing.low_balance and billing.balance_depleted reach your endpoint through the notification pipeline, so they are gated on the webhook notification channel — which is off by default. Enable it in notification preferences or these two never deliver, even with a matching webhook subscription.
Auth & system events
| Event | Description |
|---|---|
auth.welcome | First successful sign-up |
system.api_key_created | A new secret API key was issued |
system.api_key_revoked | An API key was revoked |
Reserved event names
These names are accepted at registration time but are not emitted yet. Don't build flows that depend on them; watch the changelog for when they go live.
batch.completed · custom.alert · campaign.created · campaign.completed · campaign.batch.queued · campaign.batch.completed · campaign.attempt.completed · campaign.followup.completed · campaign.report.ready · campaign.suggestion.created
Example Request
curl -X POST https://api.rymi.live/v1/webhooks \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/webhooks/rymi",
"events": ["call.intelligence.ready", "call.failed", "batch.completed"],
"secret": "whsec_your_webhook_secret_here"
}'const result = await rymi.webhooks.create({
url: "https://example.com/webhooks/rymi",
events: ["call.intelligence.ready", "call.failed", "batch.completed"],
secret: "whsec_your_webhook_secret_here",
});result = rymi.webhooks.create(
url="https://example.com/webhooks/rymi",
events=["call.intelligence.ready", "call.failed", "batch.completed"],
secret="whsec_your_webhook_secret_here",
)Response 201
{
"status": "registered"
}Errors
| Status | Meaning |
|---|---|
400 | Missing or invalid url, events, or secret |
401 | Missing or invalid API key |
Payload Format
When an event fires, Rymi sends a POST request to your registered URL with the following JSON body:
call.started Payload
Emitted by the gateway, so the envelope is { event, timestamp, data } with no top-level type field.
{
"event": "call.started",
"timestamp": "2026-03-01T10:00:00Z",
"data": {
"call_id": "call_abc123",
"session_id": "room_abc123",
"agent_id": "agent_abc123",
"channel": "web",
"to_number": "webrtc",
"started_at": "2026-03-01T10:00:00Z"
}
}Telephony calls set "channel": "phone", carry the dialed to_number, and add a participant_id for the leg.
call.failed Payload
Emitted by the gateway, so the envelope is { event, timestamp, data } with no top-level type field.
{
"event": "call.failed",
"timestamp": "2026-03-01T10:00:00Z",
"data": {
"call_id": "call_abc123",
"session_id": "room_abc123",
"agent_id": "agent_abc123",
"status": "failed",
"reason": "insufficient_funds"
}
}call.completed Payload
call.completed normally fires once, from the API after post-call processing — the rich payload shown below (with a top-level type), delivered alongside call.intelligence.ready.
If the post-call queue is unavailable, the gateway instead emits a lean fallback call.completed when the call ends: envelope { event, timestamp, data } with no top-level type, carrying the transcript but no summary, sentiment, or costs (cost is null, intelligence_status is "pending"). A later reprocess then emits the rich API payload, so both can arrive for the same call.
Deduplicate on call_id and prefer the payload whose intelligence_status is "completed".
{
"type": "call.completed",
"event": "call.completed",
"timestamp": "2026-03-01T10:05:00Z",
"data": {
"call_id": "call_abc123",
"session_id": "room_abc123",
"agent_id": "agent_abc123",
"status": "completed",
"reason": "user_hangup",
"duration": 145,
"cost": 0.29,
"telecom_cost": 0.07,
"provider_cost": 0.05,
"provider_cost_breakdown": { "llm": 0.03, "tts": 0.01, "stt": 0.01 },
"intelligence_status": "completed",
"transcript": {
"text": "Caller: Hello\nAgent: Hi there",
"segments": []
},
"summary": "Caller asked for support and the agent resolved the issue.",
"sentiment": "neutral",
"structured_data": { "customer_intent": "support" },
"evaluation": { "passed": true, "score": 1, "reasoning": "The issue was resolved." },
"recording": null,
"errors": []
}
}cost is the customer-billed total (telecom_cost + provider_cost). provider_cost_breakdown itemizes the underlying model spend, and recording is the recording manifest when recording is enabled (otherwise null).
call.intelligence.ready Payload
{
"type": "call.intelligence.ready",
"event": "call.intelligence.ready",
"timestamp": "2026-03-01T10:05:00Z",
"data": {
"call_id": "call_abc123",
"status": "completed",
"duration": 145,
"cost": 0.21,
"intelligence_status": "completed",
"transcript": {
"text": "Caller: Hello\nAgent: Hi there",
"segments": [
{
"speaker": "user",
"text": "Hello",
"sequence": 0,
"started_at_ms": 0,
"ended_at_ms": 900
}
]
},
"summary": "Caller asked for support and the agent resolved the issue.",
"sentiment": "neutral",
"structured_data": {
"customer_intent": "support"
},
"evaluation": {
"passed": true,
"score": 1,
"reasoning": "The issue was resolved."
},
"errors": []
}
}Notification-event Payload
Agent, billing, auth, and system events are delivered with a lighter notification-shaped data object:
{
"type": "agent.number_assigned",
"event": "agent.number_assigned",
"timestamp": "2026-03-01T10:01:00Z",
"data": {
"notification_id": "ntf_123",
"tenant_id": "ten_456",
"event_category": "agent.number_assigned",
"title": "Number attached",
"message": "+15551234567 now routes to Priya - Sales Specialist",
"metadata": { "number": "+15551234567" }
}
}Payload Fields
| Field | Type | Description |
|---|---|---|
type | string | Event name (same value as event). Present only on API- and notification-origin events (e.g. the rich call.completed, call.intelligence.ready, agent/billing/auth/system events). Gateway call events (call.started, call.failed, the lean call.completed) omit it — always key off event |
event | string | Event name. Present on every payload |
timestamp | string | ISO 8601 timestamp of when the event was emitted |
data | object | Event-specific payload |
Delivery Behavior
- Timeout: Rymi waits up to 3 seconds for your endpoint to respond with a
2xxstatus code. Acknowledge fast and process asynchronously. Don't do heavy work before responding. - Retries: Each event is attempted up to 5 times with exponential backoff (roughly 1s, 2s, 4s, 8s between attempts).
- Failure alerts: When all retries are exhausted, Rymi emails the
alert_emailregistered with the webhook (if set). Throttled to one alert per hour per endpoint. - Ordering: Events are delivered in approximate order but are not guaranteed to arrive sequentially. Use
timestampandcall_idfor deduplication. - Idempotency: Make your webhook handler idempotent. The same event may arrive more than once.
Update Webhook
Update an existing webhook's URL, subscribed events, or signing secret. Only provided fields are changed.
Path Parameters
| Parameter | Type | Description |
|---|---|---|
id | string | Webhook ID |
Request Body
All fields are optional. Provide only the fields you want to change.
| Field | Type | Required | Description |
|---|---|---|---|
url | string | No | New HTTPS endpoint URL |
events | string[] | No | Updated event subscriptions |
secret | string | No | New HMAC signing secret (min 16 characters) |
alert_email | string | null | No | Replace the failure-alert email. Pass null or "" to clear it |
redaction_level | string | null | No | Change how much call data deliveries include: none, partial, full, or null to reset to the partial default |
Example: Change URL
curl -X PATCH https://api.rymi.live/v1/webhooks/wh_123 \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://new-endpoint.example.com/webhooks/rymi"
}'const result = await rymi.webhooks.update("wh_123", {
url: "https://new-endpoint.example.com/webhooks/rymi",
});result = rymi.webhooks.update(
"wh_123",
url="https://new-endpoint.example.com/webhooks/rymi",
)Example: Update Subscribed Events
curl -X PATCH https://api.rymi.live/v1/webhooks/wh_123 \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"events": ["call.intelligence.ready", "call.failed", "call.started"]
}'const result = await rymi.webhooks.update("wh_123", {
events: ["call.intelligence.ready", "call.failed", "call.started"],
});result = rymi.webhooks.update(
"wh_123",
events=["call.intelligence.ready", "call.failed", "call.started"],
)Response 200
{
"status": "updated",
"id": "wh_123"
}Errors
| Status | Meaning |
|---|---|
400 | Invalid url (not HTTPS, private network, etc.) or empty body |
401 | Missing or invalid API key |
404 | Webhook not found |
List Webhooks
Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
limit | integer | 50 | Max records to return |
offset | integer | 0 | Records to skip |
Example Request
curl "https://api.rymi.live/v1/webhooks?limit=10" \
-H "Authorization: Bearer YOUR_API_KEY"const { webhooks } = await rymi.webhooks.list({ limit: 10 });result = rymi.webhooks.list(limit=10)Response 200
{
"webhooks": [
{
"id": "wh_123",
"url": "https://example.com/webhooks/rymi",
"events": ["call.intelligence.ready"],
"created_at": "2026-03-01T10:00:00Z"
}
],
"total": 1,
"offset": 0,
"limit": 10
}Errors
| Status | Meaning |
|---|---|
401 | Missing or invalid API key |
Delete Webhook
Path Parameters
| Parameter | Type | Description |
|---|---|---|
id | string | Webhook ID |
Example Request
curl -X DELETE https://api.rymi.live/v1/webhooks/wh_123 \
-H "Authorization: Bearer YOUR_API_KEY"const result = await rymi.webhooks.delete("wh_123");result = rymi.webhooks.delete("wh_123")Response 200
{
"status": "deleted",
"id": "wh_123"
}Errors
| Status | Meaning |
|---|---|
401 | Missing or invalid API key |
404 | Webhook not found |
Signature Verification
Every webhook delivery includes three headers for verification:
| Header | Description |
|---|---|
X-Rymi-Event | Event name (e.g., call.intelligence.ready) |
X-Rymi-Timestamp | Unix timestamp (milliseconds) used in the signature payload |
X-Rymi-Signature | HMAC-SHA256 hex digest of {timestamp}.{raw_body} |
Verification Steps
- Read the raw request body exactly as received (do not parse JSON first).
- Concatenate
X-Rymi-Timestamp, a literal., and the raw body string. - Compute an HMAC-SHA256 digest using your webhook
secretas the key. - Compare the result to
X-Rymi-Signatureusing a timing-safe comparison.
Example (Node.js)
const crypto = require('crypto');
function verifyWebhook(rawBody, signature, timestamp, secret) {
const payload = `${timestamp}.${rawBody}`;
const expected = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
throw new Error('Invalid webhook signature');
}
}Or use the built-in verifier in @rymi/node:
rymi.webhooks.verifySignature(rawBody, signature, timestamp, secret);TIP
HTTP header names are case-insensitive. Frameworks commonly expose these as lowercase keys: x-rymi-signature, x-rymi-timestamp, x-rymi-event.
URL Safety Rules
Rymi rejects webhook URLs that look unsafe before saving them. Validation runs on POST /v1/webhooks and on every PATCH /v1/webhooks/:id that updates url. A 400 is returned with a human-readable error describing what to fix.
- Production webhook URLs must use
https://. Plainhttp://is rejected outside the test environment. - Hostnames
localhost,metadata.google.internal, and the EC2 metadata IP169.254.169.254are blocked. .localand.internalhostnames are rejected, since they aren't reachable from the public internet. Use a tunnel (ngrok, Cloudflare Tunnel) for local testing.- Hostnames that resolve to a private IPv4 (
10.0.0.0/8,127.0.0.0/8,169.254.0.0/16,172.16.0.0/12,192.168.0.0/16) or IPv6 unique-local / link-local range are rejected. - URLs that include
username:password@credentials are rejected. Use a header on your end instead. - Hostnames that fail DNS resolution are rejected with the resolution error.

