Skip to content

Webhooks

Rymi sends an HTTP POST to your backend whenever a call, agent, billing, or system event happens. Verify the signature, return a 2xx fast, then process the payload on your side.

Events

EventTrigger
call.startedCall connected and the agent session began
call.failedCall could not start or ended in a failure state (e.g. insufficient funds)
call.completedCall ended with transcript and metadata (fires after post-call processing)
call.intelligence.readyPost-call analysis (summary, extraction, evaluation) is ready
agent.publishedAgent was published to a live version
agent.failedAgent runtime hit a fatal error
agent.number_assignedPhone number was attached to an agent
billing.topup_confirmedWallet top-up was captured
billing.payment_failedPayment attempt failed
auth.welcomeFirst successful sign-up
system.api_key_createdSecret API key was issued
system.api_key_revokedAPI key was revoked
billing.low_balanceWallet balance crossed the low-balance threshold
billing.balance_depletedWallet balance reached zero

billing.low_balance and billing.balance_depleted are emitted live through the notification pipeline. A few other event names (batch.completed, custom.alert) are reserved but not emitted yet. See the Webhooks API reference for the authoritative catalog and payload examples.

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

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.