Skip to content

Webhooks

Rymi pushes real-time events to your backend as HTTP POST requests whenever critical call lifecycle milestones occur.

Events

EventTrigger
call.startedCall session has started (room opened)
call.endedCall has ended and the room is closed
call.transcript.readyFinal transcript is available
call.intelligence.readyPost-call intelligence (summary, extraction, evaluation) has completed
call.completedLegacy alias for post-call intelligence completion
call.recording.readyRecording is finalized and available for download
participant.joinedA participant has connected to the call
participant.leftA participant has disconnected from the call

Payload Format

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

  1. Extract x-rymi-signature and x-rymi-timestamp from the request headers
  2. Build the signed payload: {x-rymi-timestamp}.{raw_request_body}
  3. Compute HMAC-SHA256(webhook_secret, signed_payload)
  4. Compare the computed hash against x-rymi-signature using constant-time comparison
  5. Optionally reject requests where the timestamp is older than 5 minutes (replay protection)

Node.js Example

Using the @rymi/node SDK:

typescript
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)

typescript
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

python
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.