# Standard Webhooks

Verify BuildBase event webhooks with any Standard Webhooks library, using the webhook-id, webhook-timestamp and webhook-signature headers they carry.

Every event delivery carries two sets of headers. One is BuildBase's own (`x-buildbase-signature`, `x-buildbase-timestamp`, `x-buildbase-event`, `x-buildbase-event-id`), which `parseWebhookEvent` and `verifyWebhookSignature` read. The other follows [Standard Webhooks](https://www.standardwebhooks.com/), the scheme OpenAI, Anthropic, Clerk, Resend and Twilio use, so you can verify a BuildBase webhook with the same library you already use for theirs.

| Header              | Value                                                                         |
| ------------------- | ----------------------------------------------------------------------------- |
| `webhook-id`        | The event id (`evt_...`), the same on every retry and for every endpoint      |
| `webhook-timestamp` | Unix seconds, set when this attempt was signed                                |
| `webhook-signature` | `v1,` and the base64 HMAC-SHA256 of `{webhook-id}.{webhook-timestamp}.{body}` |

Both sets sign the same body with the same endpoint secret, so you can switch from one to the other without rotating anything.

## The secret a Standard Webhooks library takes

Standard Webhooks libraries take the secret as `whsec_` followed by the key in base64. The key is your endpoint's signing secret as text, so turn it into that form once:

```typescript
const whsec = `whsec_${Buffer.from(process.env.BUILDBASE_WEBHOOK_SECRET!).toString('base64')}`;
```

## Verify with the library

```typescript
import { Webhook } from 'standardwebhooks';

const wh = new Webhook(whsec);

export async function POST(req: Request) {
  const body = await req.text(); // the raw body, exactly as received
  try {
    const event = wh.verify(body, {
      'webhook-id': req.headers.get('webhook-id') ?? '',
      'webhook-timestamp': req.headers.get('webhook-timestamp') ?? '',
      'webhook-signature': req.headers.get('webhook-signature') ?? '',
    }) as { id: string; event: string; data: unknown };
    // Handle event.event; use event.id (or webhook-id) to process each event once.
    return Response.json({ received: true });
  } catch {
    return Response.json({ error: 'Invalid signature' }, { status: 401 });
  }
}
```

The library also refuses a timestamp more than five minutes from now, which stops a recorded request from being replayed later.

From `@buildbase/sdk` 1.0.0 you can skip the library: `verifyStandardWebhook({ body, id, timestamp, signature, secret })` takes the three headers and the endpoint secret, raw or as `whsec_`, accepts any `v1` signature in the list and refuses a timestamp more than `maxAgeSeconds` (default 300) away. `parseStandardWebhookEvent` runs the same check and returns the parsed event, or `null` when the check fails.

## Which to use

Use `parseWebhookEvent` from `@buildbase/sdk` when you already use the SDK: it is one call and returns the typed event. Use the Standard Webhooks headers when your receiver is not in JavaScript, when it already verifies other providers' webhooks with a Standard Webhooks library, or when a gateway in front of it expects them. Both stay supported.
