Webhooks
Rymi pushes real-time events to your backend as HTTP POST requests whenever critical call lifecycle milestones occur.
Events
| Event | Trigger |
|---|---|
call.started | Call session has started (room opened) |
call.ended | Call has ended and the room is closed |
call.transcript.ready | Final transcript is available |
call.intelligence.ready | Post-call intelligence (summary, extraction, evaluation) has completed |
call.completed | Legacy alias for post-call intelligence completion |
call.recording.ready | Recording is finalized and available for download |
participant.joined | A participant has connected to the call |
participant.left | A participant has disconnected from the call |
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
Failed webhook deliveries (non-2xx responses) are retried with exponential backoff up to 5 attempts. If all retries fail, the event is marked as failed in the dashboard event log.

