Docs
Webhook Ingest
Webhook Ingest This example receives signed provider events and stores each event once. Tables Create integration_settings as a single-record table. Field Type Notes providerWebhookSecret string isEncrypted=true , isPublished=false Create provider_event . Field Type Notes provide
Webhook Ingest
This example receives signed provider events and stores each event once.
Tables
Create integration_settings as a single-record table.
| Field | Type | Notes |
|---|---|---|
providerWebhookSecret |
string | isEncrypted=true, isPublished=false |
Create provider_event.
| Field | Type | Notes |
|---|---|---|
providerEventId |
string | Unique |
kind |
string | Event type |
payload |
simple-json | Raw event payload |
processedAt |
datetime | Nullable |
Route
Create a custom POST /webhooks/provider route. Make it public only if the signature check is enforced by the handler.
const settingsResult = await #integration_settings.find({
fields: 'providerWebhookSecret',
limit: 1
});
const settings = settingsResult.data?.[0];
if (!settings?.providerWebhookSecret) {
@THROW500('webhook secret is not configured');
}
const signature = @REQ.headers['x-provider-signature'];
const expected = await @HELPERS.$crypto.hmacSha256(
@REQ.rawBody,
settings.providerWebhookSecret,
'hex'
);
if (signature !== expected) {
@THROW401('invalid signature');
}
const eventId = @BODY.id;
if (!eventId) {
@THROW400('event id is required');
}
const existing = await #provider_event.find({
filter: { providerEventId: { _eq: eventId } },
fields: 'id',
limit: 1
});
if (existing.data?.[0]) {
return { received: true, duplicate: true };
}
await #provider_event.create({
data: {
providerEventId: eventId,
kind: @BODY.type || 'unknown',
payload: @BODY
}
});
await @TRIGGER('process-provider-event', { eventId });
return { received: true };
Use the exact raw body when the provider signs raw request bytes.