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, responseUpdated or responseFinished events?
  • Why is my Formbricks webhook verification failing, or failing only on retries and replays?
  • Why does my Formbricks webhook arrive without a webhook-signature header?

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)

HeaderValue
webhook-idIdentifies the message; retries reuse it. Your idempotency key (the body has no event id)
webhook-timestampUnix seconds
webhook-signatureOne 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

EventFired when
responseCreatedA survey response is created
responseUpdatedA response gains or changes answers
responseFinishedThe 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_DELIVERY_TIMEOUT_MS 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


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Nov 12, 2026

View on GitHub →