Skip to main content

Import

The webhook helper is a separate export, import only what you need:
TypeScript
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.
TypeScript
const payload = await verifyWebhook(rawBody, signature, secret);
ParameterTypeDescription
rawBodystringRaw request body, not parsed JSON
signaturestringValue of the X-BabySea-Signature header
secretstringYour webhook secret from the dashboard
toleranceSecondsnumberMax age in seconds (default: 300)
Throws if the signature is invalid or the timestamp is outside the tolerance window.

Next.js App Router

TypeScript
// 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.
TypeScript
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:
TypeScript
// 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

EventDescription
generation.startedGeneration submitted to a provider and processing
generation.completedGeneration finished, output files available
generation.failedGeneration failed, see generation_error and generation_error_code
generation.canceledGeneration was canceled
webhook.testTest event sent when verifying webhook URL ownership

Payload type

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