Agent skill
Formbricks Webhooks Skill
Receive and verify Formbricks webhooks, which follow the Standard Webhooks specification. Use when setting up a Formbricks webhook endpoint, getting the whsec_ signing secret, debugging webhook-id / webhook-timestamp / webhook-signature verification, or handling survey response events like responseCreated, responseUpdated and responseFinished.
Install this skill
npx skills add hookdeck/webhook-skills --skill formbricks-webhooks
Formbricks is an open-source (self-hostable) survey and experience-management platform. Each environment can have webhooks that POST a JSON event whenever a survey response is created, updated or finished. Deliveries follow Standard Webhooks, so each carries webhook-id, webhook-timestamp and webhook-signature headers.
When to Use This Skill
- How do I receive Formbricks webhooks?
- How do I verify a Formbricks webhook signature (
webhook-signature)? - Where do I find my Formbricks webhook secret (
whsec_...)? - How do I handle
responseCreated,responseUpdatedorresponseFinishedevents? - Why is my Formbricks webhook verification failing, or failing only on retries and replays?
- Why does my Formbricks webhook arrive without a
webhook-signatureheader?
Signing Requirement
Hookdeck's configuration alert for this source reads: "Webhooks created before Formbricks added signing have no secret and are unsigned. If yours has no secret, create a new webhook." The webhook-signature header is sent only if a secret is configured. New webhooks always get a secret, so create a new webhook if yours has none. The examples reject deliveries without a signature (400) and fail closed (500) when the secret env var is unset.
Verification (core)
| Header | Value |
|---|---|
webhook-id | Identifies the message; retries reuse it. Your idempotency key (the body has no event id) |
webhook-timestamp | Unix seconds |
webhook-signature | One or more space-separated v1,<base64> entries |
Signed content is {webhook-id}.{webhook-timestamp}.{raw body}, HMAC-SHA256, base64 output. The key is the base64-decoded part of the whsec_... secret after the prefix. Compare against every v1, entry; ignore other versions. If webhook-id is absent, read svix-id, svix-timestamp and svix-signature instead, as Hookdeck does. The examples use the standardwebhooks library (npm and PyPI):
const { Webhook } = require('standardwebhooks');
// rawBody: exact request body (Buffer or string), NOT re-serialised JSON
function verifyFormbricks(rawBody, headers, secret = process.env.FORMBRICKS_WEBHOOK_SECRET) {
const p = headers['webhook-id'] ? 'webhook' : 'svix'; // fall back to the svix-* names
new Webhook(secret).verify(rawBody, {
'webhook-id': headers[`${p}-id`],
'webhook-timestamp': headers[`${p}-timestamp`],
'webhook-signature': headers[`${p}-signature`],
}, { jsonParse: false }); // throws on a bad signature or stale timestamp → respond 401
}
The library always enforces a fixed 5-minute timestamp window. Hookdeck retries and replays resend the original webhook-timestamp, so the library rejects them once they are more than 5 minutes old. If you receive through Hookdeck and need late retries to pass, use the manual verifier in references/verification.md, which can drop or widen the window. In Express, read the body with express.raw({ type: '*/*' }) so any content type verifies, and parse JSON only after the check.
For complete handlers with tests, see examples/express/, examples/nextjs/, examples/fastapi/.
Event Type and Payload
Every body has the same three top-level fields, and the event name is event:
{
"event": "responseFinished",
"webhookId": "webhookId",
"data": {
"id": "responseId",
"surveyId": "surveyId",
"finished": true,
"data": { "newsletter_consent": "accepted" },
"survey": { "title": "Customer Satisfaction Survey", "status": "inProgress", "type": "link" }
}
}
data is the response object: the answers are in data.data (keyed by question id), data.survey is a summary of the survey, and data.finished tells you whether the respondent completed it. Other fields include contact, contactAttributes, language, meta, tags, ttc and variables.
Event Types
| Event | Fired when |
|---|---|
responseCreated | A survey response is created |
responseUpdated | A response gains or changes answers |
responseFinished | The respondent completes the survey |
These are the three documented triggers (the list may not be exhaustive): ignore types you don't recognise. See references/overview.md.
Delivery Semantics
Return a 2xx within 5 seconds (WEBHOOK_ when self-hosted), then process asynchronously. Network errors, timeouts, redirects and 408, 429 or 5xx responses are retried up to 5 attempts in all, at about 30 seconds, then 1, 2 and 4 minutes, after which the event is dropped. Any other 4xx is not retried, so a 400 or 401 for a bad signature is final. Deduplicate on webhook-id, which is the same on every attempt.
Environment Variables
FORMBRICKS_WEBHOOK_SECRET=whsec_xxxxx # full value, prefix included; handlers fail closed (500) without it
Local Development
# Express / Next.js (use 8000 for FastAPI). No install or account required.
npx hookdeck-cli listen 3000 formbricks --path /webhooks/formbricks
Set the printed URL as the webhook URL under Configuration > Integrations > Webhooks, and click Test Endpoint before saving.
Reference Materials
- references/overview.md - Events, payload structure and delivery behaviour
- references/setup.md - Create a webhook, find the secret, self-hosting notes
- references/verification.md - Library and manual verification, gotchas, debugging