Webhooks
Rymi pushes real-time events to your backend as HTTP POST requests whenever critical call, agent, billing, and system milestones occur.
Events
| Event | Trigger |
|---|---|
call.completed | Call ended with transcript and metadata (fires after post-call processing) |
call.intelligence.ready | Post-call analysis (summary, extraction, evaluation) is ready |
agent.published | Agent was published to a live version |
agent.failed | Agent runtime hit a fatal error |
agent.number_assigned | Phone number was attached to an agent |
billing.topup_confirmed | Wallet top-up was captured |
billing.payment_failed | Payment attempt failed |
auth.welcome | First successful sign-up |
system.api_key_created | Secret API key was issued |
system.api_key_revoked | API key was revoked |
Some additional event names (call.started, call.failed, batch.completed, billing.low_balance, billing.balance_depleted, custom.alert) are reserved but not emitted yet — see the Webhooks API reference for the authoritative catalog and payload examples.
Payload Format
{
"type": "call.intelligence.ready",
"event": "call.intelligence.ready",
"timestamp": "2026-04-01T12:30:00Z",
"data": {
"call_id": "call_abc123",
"agent_id": "550e8400-...",
"status": "completed",
"duration": 145,
"cost": 0.21,
"summary": "The caller confirmed the appointment.",
"sentiment": "positive"
}
}Signature Verification
Every outbound webhook includes X-Rymi-Signature, X-Rymi-Timestamp, and X-Rymi-Event headers to verify the payload originated from Rymi and wasn't tampered with. Header names are case-insensitive, so most Node frameworks expose them as lowercase keys such as x-rymi-signature.
The signature is an HMAC-SHA256 hash of {timestamp}.{raw_body} using your webhook secret.
Verification Steps
- Extract
x-rymi-signatureandx-rymi-timestampfrom the request headers - Build the signed payload:
{x-rymi-timestamp}.{raw_request_body} - Compute
HMAC-SHA256(webhook_secret, signed_payload) - Compare the computed hash against
x-rymi-signatureusing constant-time comparison - Optionally reject requests where the timestamp is older than 5 minutes (replay protection)
Node.js Example
Using the @rymi/node SDK:
import express from 'express';
import { Rymi } from '@rymi/node';
const app = express();
const rymi = new Rymi();
app.post('/webhook', express.text({ type: 'application/json' }), (req, res) => {
const signature = req.headers['x-rymi-signature'] as string;
const timestamp = req.headers['x-rymi-timestamp'] as string;
const payload = req.body;
try {
rymi.webhooks.verifySignature(
payload,
signature,
timestamp,
process.env.RYMI_WEBHOOK_SECRET!
);
const event = JSON.parse(payload);
switch (event.type) {
case 'call.intelligence.ready':
console.log('Intelligence ready:', event.data.call_id, event.data.summary);
break;
}
res.status(200).send('OK');
} catch (error) {
console.error('Invalid webhook signature!', error);
res.status(401).send('Unauthorized');
}
});Manual Verification (without SDK)
import crypto from 'crypto';
function verifyWebhook(rawBody: string, signature: string, timestamp: string, secret: string): boolean {
const signedPayload = `${timestamp}.${rawBody}`;
const expected = crypto
.createHmac('sha256', secret)
.update(signedPayload)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(expected, 'hex'),
Buffer.from(signature, 'hex')
);
}Python Example
import hmac
import hashlib
def verify_webhook(raw_body: str, signature: str, timestamp: str, secret: str) -> bool:
signed_payload = f"{timestamp}.{raw_body}"
expected = hmac.new(
secret.encode(), signed_payload.encode(), hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature)Registering Webhooks
Configure your webhook URL via the Webhooks API or the dashboard settings.
Raw Body Required
You must verify the signature against the raw request body (the exact bytes sent over the wire), not a parsed-then-re-serialized JSON object. Frameworks like Express should use express.text() or express.raw() for the webhook route.
Retry Policy
Rymi waits up to 3 seconds for a 2xx response — acknowledge immediately and process the event asynchronously. Failed deliveries (non-2xx or timeout) are retried with exponential backoff up to 5 total attempts. If all attempts fail, the event is marked as failed in the dashboard event log, and the webhook's alert_email (if set) is notified at most once per hour.

