Overview
Receive real-time event notifications when users sign up, subscribe, pay, or trigger actions.
BuildBase fires webhook events for every significant action - user signups, subscription changes, payments, credit usage, workspace updates, and more. Verify the signature and handle the event in your backend.
import { parseWebhookEvent } from '@buildbase/sdk';
export async function POST(req) {
const body = await req.text();
const event = parseWebhookEvent({
body,
signature: req.headers.get('x-buildbase-signature'),
timestamp: req.headers.get('x-buildbase-timestamp'),
secret: process.env.WEBHOOK_SECRET,
});
if (!event) {
return Response.json({ error: 'Invalid signature' }, { status: 401 });
}
switch (event.event) {
case 'subscription.upgraded':
await enablePremiumFeatures(event.data.workspaceId);
break;
case 'credit.low_balance':
await notifyTeam(event.data.workspaceId);
break;
case 'workspace.member_added':
await syncToExternalCRM(event.data);
break;
}
return Response.json({ received: true });
}Before you start
Create a webhook endpoint in the BuildBase dashboard and copy the signing secret.
Two functions
| Function | What it does |
|---|---|
verifyWebhookSignature({ body, signature, timestamp, secret }) | Returns true if the signature is valid |
parseWebhookEvent({ body, signature, timestamp, secret }) | Verifies and parses - returns the event object or null |
Use parseWebhookEvent for most cases. Use verifyWebhookSignature if you want to parse the body yourself.
Express / Hono example
import { parseWebhookEvent } from '@buildbase/sdk';
app.post('/webhooks', (req, res) => {
const event = parseWebhookEvent({
body: req.body,
signature: req.headers['x-buildbase-signature'],
timestamp: req.headers['x-buildbase-timestamp'],
secret: process.env.WEBHOOK_SECRET,
});
if (!event) return res.status(401).json({ error: 'Invalid' });
// Handle event.event and event.data
res.json({ received: true });
});Event categories
| Category | Events |
|---|---|
| User | user.registered, user.logged_in, user.new_device_login, user.email_verified, user.password_changed, user.password_reset_requested, user.profile_updated, user.tag_added, user.tag_removed, user.blocked, user.unblocked |
| Organization | organization.member_invited, organization.member_accepted, organization.member_role_changed, organization.member_removed |
| Workspace | workspace.created, workspace.updated, workspace.deleted, workspace.settings_updated, workspace.features_updated, workspace.member_added, workspace.member_removed, workspace.member_role_changed |
| Subscription | subscription.created, subscription.updated, subscription.upgraded, subscription.canceled, subscription.cancel_scheduled, subscription.resumed, subscription.suspended, subscription.trial_started, subscription.trial_will_end, subscription.trial_expired, subscription.downgraded |
| Payment | payment.succeeded, payment.failed, payment.action_required |
| Quota | quota.limit_exceeded |
| Credit | credit.purchased, credit.consumed, credit.expired, credit.granted, credit.revoked, credit.low_balance |
| Workflow | workflow.created, workflow.deleted, workflow.published, workflow.paused, workflow.resumed, workflow.instance_completed, workflow.instance_failed |
| Audience | audience.member_created, audience.member_updated, audience.added_to_list, audience.removed_from_list, audience.attribute_changed, audience.tag_added, audience.tag_removed, audience.unsubscribed, audience.resubscribed, audience.email_invalidated, audience.blocked, audience.unblocked, audience.import_completed, audience.list_created, audience.list_updated, audience.list_deleted |
email.domain_added, email.domain_verified, email.sent, email.opened, email.clicked, email.bounced, email.unsubscribed, email.campaign_sent | |
| Push | push.campaign_sent |
| Auth | auth.method_deleted, auth.domain_added, auth.domain_verified, auth.domain_deleted, auth.client_self_registered |
| Token | token.created, token.updated, token.revoked |
| Group_version | group_version.published |
| Form | form.submitted |
86+ event types total. See the SDK README for the full list.
Event payload structure
Every webhook POST body has this shape - three fields, nothing else:
{
"event": "subscription.upgraded",
"timestamp": 1719000000,
"data": {
"workspaceId": "ws_xyz789",
"userId": "usr_def456",
"planId": "plan_starter",
"planVersionId": "pv_v2",
"billingInterval": "monthly",
"previousPlanId": "plan_free"
}
}timestamp is Unix seconds, set when the event fires. Each delivery also carries three headers:
| Header | Contents |
|---|---|
x-buildbase-signature | sha256=<hex> - HMAC-SHA256 of {timestamp}.{body} with your signing secret |
x-buildbase-timestamp | Same Unix timestamp as the body |
x-buildbase-event | The event name (e.g. subscription.upgraded) |
The data object varies by event type. Some common patterns:
| Event | Key data fields |
|---|---|
user.registered | userId, email, name |
subscription.created | workspaceId, planId, billingInterval, trialEnd |
subscription.trial_started | workspaceId, subscriptionId, planVersionId, trialEnd |
payment.failed | workspaceId, amount, currency, failureReason |
credit.consumed | workspaceId, amount, description, remaining |
workspace.member_added | workspaceId, userId, role |
Delivery guarantees
- At-least-once delivery - events may be delivered more than once. Deliveries carry no unique event ID; retries repeat the same
eventandtimestamp, so deduplicate on a hash of the raw request body. - No ordering guarantee - events may arrive out of order. Use
event.timestampif ordering matters. - Automatic retries - a non-2xx response is retried 4 more times with exponential backoff, at roughly 10s, 20s, 40s and 80s, so the last attempt lands about 2.5 minutes after the first. A destination that is refused by our outbound safety checks is not retried at all; the delivery log says
blocked. - Signature expiry - signatures are valid for 5 minutes by default. Reject older payloads to prevent replay attacks.
Limits
Webhooks fan out: one event reaching ten endpoints is ten calls to your servers, and some events fire per recipient rather than per action. These bounds keep one busy hour from becoming a queue nobody can drain.
| Limit | Value | What happens at the limit |
|---|---|---|
| Endpoints per organization | 10 (excluding archived) | Creating an eleventh is refused. Delete one first |
| Deliveries per minute | 600 per organization | Further deliveries that minute are not sent, and appear in the delivery log as throttled |
| Consecutive failures | 50 per endpoint | The endpoint is disabled automatically and shows Auto-disabled in the console. Any 2xx resets the count |
A throttled delivery never left BuildBase, so it is not a failure of your
receiver and is not retried. email.sent fires once per recipient, so a large
campaign is the usual way to meet the per-minute limit; subscribe an endpoint to
the specific events you need rather than to * if that matters to you.
Re-enabling an auto-disabled endpoint from the console clears its failure count.
Next Steps
- Server SDK - Backend setup and all server methods.
- Billing - Subscription events and payment handling.