Import
The webhook helper is a separate export, import only what you need:
import { verifyWebhook } from 'babysea-sdk/webhooks';
Verification
verifyWebhook parses the X-BabySea-Signature header, validates the HMAC-SHA256 signature, and rejects replays older than 5 minutes.
const payload = await verifyWebhook(rawBody, signature, secret);
| Parameter | Type | Description |
|---|
rawBody | string | Raw request body, not parsed JSON |
signature | string | Value of the X-BabySea-Signature header |
secret | string | Your webhook secret from the dashboard |
toleranceSeconds | number | Max age in seconds (default: 300) |
Throws if the signature is invalid or the timestamp is outside the tolerance window.
Next.js App Router
// app/api/webhooks/babysea/route.ts
import { verifyWebhook } from 'babysea-sdk/webhooks';
import { NextResponse } from 'next/server';
export async function POST(req: Request) {
const rawBody = await req.text();
const signature = req.headers.get('x-babysea-signature')!;
try {
const payload = await verifyWebhook(
rawBody,
signature,
process.env.BABYSEA_WEBHOOK_SECRET!,
);
if (payload.webhook_event === 'generation.completed') {
const { generation_id, generation_output_file } = payload.webhook_data;
// save to database, notify user, etc.
}
return NextResponse.json({ received: true });
} catch {
return NextResponse.json({ error: 'Invalid signature' }, { status: 400 });
}
}
Express / Node.js
You must use express.text() instead of express.json() on your webhook route. The default JSON parser re-serializes the body, which changes whitespace and breaks the HMAC signature.
import express from 'express';
import { verifyWebhook } from 'babysea-sdk/webhooks';
// Use express.text() to preserve the raw body for signature verification
app.post(
'/webhooks/babysea',
express.text({ type: 'application/json' }),
async (req, res) => {
const rawBody = req.body; // raw string, not parsed JSON
const signature = req.headers['x-babysea-signature'] as string;
const secret = process.env.BABYSEA_WEBHOOK_SECRET!;
try {
const payload = await verifyWebhook(rawBody, signature, secret);
switch (payload.webhook_event) {
case 'generation.completed':
console.log('Output files:', payload.webhook_data.generation_output_file);
break;
case 'generation.failed':
console.log('Error code:', payload.webhook_data.generation_error_code);
break;
case 'generation.started':
console.log('Processing:', payload.webhook_data.generation_id);
break;
case 'generation.canceled':
console.log('Canceled:', payload.webhook_data.generation_id);
break;
}
res.status(200).json({ received: true });
} catch (err) {
res.status(400).json({ error: 'Invalid signature' });
}
},
);
Replay protection
By default, webhooks older than 5 minutes are rejected. You can customize:
// Accept webhooks up to 10 minutes old
const payload = await verifyWebhook(rawBody, signature, secret, 600);
// Disable replay protection (not recommended)
const payload = await verifyWebhook(rawBody, signature, secret, Infinity);
Webhook events
| Event | Description |
|---|
generation.started | Generation submitted to a provider and processing |
generation.completed | Generation finished, output files available |
generation.failed | Generation failed, see generation_error and generation_error_code |
generation.canceled | Generation was canceled |
webhook.test | Test event sent when verifying webhook URL ownership |
Payload type
interface WebhookPayload {
webhook_event:
| 'generation.started'
| 'generation.completed'
| 'generation.failed'
| 'generation.canceled'
| 'webhook.test';
webhook_timestamp: string;
webhook_delivery_id: string;
webhook_data: {
account_id: string;
model_identifier: string;
generation_provider_initialize?: string;
generation_provider_used?: string;
generation_status: 'processing' | 'succeeded' | 'failed' | 'canceled';
generation_prediction_id?: string;
generation_id: string;
generation_output_file?: string[];
generation_error?: string;
generation_error_code?: string;
credits_refunded?: boolean;
};
}
For the full event payload reference, see Events.