Skip to content

Webhooks

Register webhook endpoints to receive lifecycle and post-call intelligence events from Rymi.

Register Webhook

POST/v1/webhooks

Request Body

FieldTypeRequiredDefaultDescription
urlstringYesHTTPS endpoint to receive events. Must pass URL safety rules
eventsstring[]YesEvents to subscribe to (see Available Events)
secretstringYesHMAC signing secret used to verify deliveries. Min 16 characters
alert_emailstringNoOptional email address to notify when delivery fails after retries. Throttled to one alert per hour per endpoint
redaction_levelstringNopartialHow 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

EventDescription
call.startedA call connected and the live session began. Fires once per call (and once per participant leg on telephony, which adds a participant_id)
call.failedA call could not start or ended in a failure state (for example, insufficient funds)
call.completedA call ended with transcript and metadata. Fires together with call.intelligence.ready once post-call processing finishes
call.intelligence.readyPost-call analysis (summary, extraction, evaluation) is ready

Agent events

EventDescription
agent.publishedAn agent was published to a live version
agent.failedAn agent runtime hit a fatal error
agent.number_assignedA 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.

EventDescription
campaign.createdA campaign was created (draft status)
campaign.launchedA campaign was launched: blockers passed, agent_snapshot_id stamped, members flipped to ready
campaign.pausedA running campaign was paused
campaign.completedA campaign finished: zero members remain in a non-terminal state
campaign.batch.queuedA scheduler sweep claimed a batch of due members
campaign.batch.completedA batch finished processing (all its attempts resolved)
campaign.attempt.completedOne campaign attempt (one call) resolved to a terminal outcome
campaign.followup.completedA follow-up job reached a terminal status (sent, failed, blocked, done)
campaign.report.readyA campaign report rollup was (re)computed and is ready to read
campaign.suggestion.createdThe 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

EventDescription
billing.topup_confirmedA wallet top-up was captured
billing.payment_failedA payment attempt failed
billing.low_balanceWallet balance dropped below the low-balance threshold (delivered via the notification → webhook bridge)
billing.balance_depletedWallet 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

EventDescription
auth.welcomeFirst successful sign-up
system.api_key_createdA new secret API key was issued
system.api_key_revokedAn 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

bash
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"
  }'
ts
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",
});
python
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

json
{
  "status": "registered"
}

Errors

StatusMeaning
400Missing or invalid url, events, or secret
401Missing 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.

json
{
  "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.

json
{
  "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".

json
{
  "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

json
{
  "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:

json
{
  "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

FieldTypeDescription
typestringEvent 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
eventstringEvent name. Present on every payload
timestampstringISO 8601 timestamp of when the event was emitted
dataobjectEvent-specific payload

Delivery Behavior

  • Timeout: Rymi waits up to 3 seconds for your endpoint to respond with a 2xx status 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_email registered 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 timestamp and call_id for deduplication.
  • Idempotency: Make your webhook handler idempotent. The same event may arrive more than once.

Update Webhook

PATCH/v1/webhooks/:id

Update an existing webhook's URL, subscribed events, or signing secret. Only provided fields are changed.

Path Parameters

ParameterTypeDescription
idstringWebhook ID

Request Body

All fields are optional. Provide only the fields you want to change.

FieldTypeRequiredDescription
urlstringNoNew HTTPS endpoint URL
eventsstring[]NoUpdated event subscriptions
secretstringNoNew HMAC signing secret (min 16 characters)
alert_emailstring | nullNoReplace the failure-alert email. Pass null or "" to clear it
redaction_levelstring | nullNoChange how much call data deliveries include: none, partial, full, or null to reset to the partial default

Example: Change URL

bash
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"
  }'
ts
const result = await rymi.webhooks.update("wh_123", {
  url: "https://new-endpoint.example.com/webhooks/rymi",
});
python
result = rymi.webhooks.update(
    "wh_123",
    url="https://new-endpoint.example.com/webhooks/rymi",
)

Example: Update Subscribed Events

bash
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"]
  }'
ts
const result = await rymi.webhooks.update("wh_123", {
  events: ["call.intelligence.ready", "call.failed", "call.started"],
});
python
result = rymi.webhooks.update(
    "wh_123",
    events=["call.intelligence.ready", "call.failed", "call.started"],
)

Response 200

json
{
  "status": "updated",
  "id": "wh_123"
}

Errors

StatusMeaning
400Invalid url (not HTTPS, private network, etc.) or empty body
401Missing or invalid API key
404Webhook not found

List Webhooks

GET/v1/webhooks

Query Parameters

ParameterTypeDefaultDescription
limitinteger50Max records to return
offsetinteger0Records to skip

Example Request

bash
curl "https://api.rymi.live/v1/webhooks?limit=10" \
  -H "Authorization: Bearer YOUR_API_KEY"
ts
const { webhooks } = await rymi.webhooks.list({ limit: 10 });
python
result = rymi.webhooks.list(limit=10)

Response 200

json
{
  "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

StatusMeaning
401Missing or invalid API key

Delete Webhook

DELETE/v1/webhooks/:id

Path Parameters

ParameterTypeDescription
idstringWebhook ID

Example Request

bash
curl -X DELETE https://api.rymi.live/v1/webhooks/wh_123 \
  -H "Authorization: Bearer YOUR_API_KEY"
ts
const result = await rymi.webhooks.delete("wh_123");
python
result = rymi.webhooks.delete("wh_123")

Response 200

json
{
  "status": "deleted",
  "id": "wh_123"
}

Errors

StatusMeaning
401Missing or invalid API key
404Webhook not found

Signature Verification

Every webhook delivery includes three headers for verification:

HeaderDescription
X-Rymi-EventEvent name (e.g., call.intelligence.ready)
X-Rymi-TimestampUnix timestamp (milliseconds) used in the signature payload
X-Rymi-SignatureHMAC-SHA256 hex digest of {timestamp}.{raw_body}

Verification Steps

  1. Read the raw request body exactly as received (do not parse JSON first).
  2. Concatenate X-Rymi-Timestamp, a literal ., and the raw body string.
  3. Compute an HMAC-SHA256 digest using your webhook secret as the key.
  4. Compare the result to X-Rymi-Signature using a timing-safe comparison.

Example (Node.js)

javascript
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:

typescript
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://. Plain http:// is rejected outside the test environment.
  • Hostnames localhost, metadata.google.internal, and the EC2 metadata IP 169.254.169.254 are blocked.
  • .local and .internal hostnames 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.