Skip to content

verifyWebhook()

async verifyWebhook<T extends WebhookEvent = WebhookEvent>(
body: string | Buffer,
headers: Record<string, string | string[] | undefined>,
options?: { tolerance?: number },
): Promise<VerifyResult<WebhookVerifyData<T>>>
ParameterTypeDescription
bodystring | BufferRaw request body (must not be parsed)
headersRecord<string, string | string[] | undefined>Raw request headers
options.tolerancenumberOverride timestamp tolerance in seconds (default: 300, max: 600)
interface WebhookVerifyData<T> {
deliveryId: string; // From X-Rakomi-Delivery-Id header (falls back to webhook-id if absent)
timestamp: number; // Unix timestamp from webhook-timestamp header
payload: T; // Parsed and verified webhook event
}
const result = await ca.verifyWebhook(rawBody, headers);
if (result.ok) {
const { deliveryId, timestamp, payload } = result.data;
console.log(`Event: ${payload.type}, Delivery: ${deliveryId}`);
switch (payload.type) {
case 'user.created':
// Handle new user
break;
case 'user.deleted':
// Handle user deletion
break;
}
} else {
console.error('Webhook invalid:', result.error.code);
}

Rakomi sends these headers with every webhook delivery, following the Standard Webhooks specification:

HeaderDescription
webhook-idStable event ID (same across retries)
webhook-signatureHMAC-SHA256 signature (v1,<base64>)
webhook-timestampUnix timestamp (seconds) for replay prevention
X-Rakomi-Delivery-IdUnique per-attempt delivery ID
X-Rakomi-EventEvent type for header-based routing
X-Rakomi-AttemptAttempt number (1-based)

The signature is computed over: {webhook-id}.{webhook-timestamp}.{body}

During signing key rotation (24h overlap), the webhook-signature header contains two signatures separated by a space: v1,<new_sig> v1,<old_sig>. The SDK tries each and succeeds if any matches.

import express from 'express';
const app = express();
// Use express.raw() for webhook routes — NOT express.json()
app.post('/webhook', express.raw({ type: 'application/json' }), async (req, res) => {
const result = await ca.verifyWebhook(req.body, req.headers);
if (!result.ok) {
return res.status(400).json({ error: result.error.code });
}
// Handle event...
res.sendStatus(200);
});
export async function POST(request: Request) {
const body = await request.text(); // Get raw body as string
const headers = Object.fromEntries(request.headers);
const result = await ca.verifyWebhook(body, headers);
if (!result.ok) {
return Response.json({ error: result.error.code }, { status: 400 });
}
// Handle event...
return Response.json({ received: true });
}
app.post('/webhook', async (c) => {
const body = await c.req.text(); // Get raw body as string
const headers = Object.fromEntries(c.req.raw.headers);
const result = await ca.verifyWebhook(body, headers);
if (!result.ok) {
return c.json({ error: result.error.code }, 400);
}
// Handle event...
return c.json({ received: true });
});

The timestamp tolerance (default: 300 seconds / 5 minutes) protects against replay attacks. Timestamps that are too old or too far in the future are rejected.

// Override per-call
const result = await ca.verifyWebhook(body, headers, {
tolerance: 60, // Only accept webhooks from the last 60 seconds
});

Maximum tolerance is 600 seconds (10 minutes).

interface WebhookEvent {
id: string; // Delivery ID
type: string; // Event type (e.g., 'user.created')
timestamp: string; // ISO 8601 timestamp
tenantId: string; // Tenant ID
userId?: string; // User ID (optional for system events)
severity: 'critical' | 'warning' | 'info';
data: Record<string, unknown>; // Event-specific data
meta: {
api_version: string;
event_language?: string;
tenant_country?: string;
user_country?: string;
};
}

The webhookSecret must be set in the Rakomi constructor:

const ca = new RakomiClient({
apiKey: 'akm_live_xxx',
webhookSecret: 'rksec_xxx', // From your Rakomi dashboard
});

If webhookSecret is not configured, verifyWebhook() returns { ok: false, error } with code config/missing_webhook_secret.

CodeWhen
webhook/timestamp_too_oldTimestamp exceeds tolerance window (too far in the past)
webhook/timestamp_too_newTimestamp is too far in the future
webhook/invalid_signatureHMAC signature doesn’t match. Are you passing the raw request body?
webhook/invalid_secretWebhook secret is corrupted (invalid key length after decode)
webhook/missing_headerRequired headers (webhook-id, webhook-signature, webhook-timestamp) are missing
webhook/invalid_bodyBody is not valid JSON
config/missing_webhook_secretwebhookSecret not configured

See Error Codes for full details.