Agent skill
folk Webhooks Skill
Receive and verify folk (folk.app CRM) webhooks, which follow the Standard Webhooks spec. Use when setting up a folk webhook endpoint, saving the whsec_ signing secret, debugging webhook-id / webhook-timestamp / webhook-signature verification, or handling events like person.created, person.updated, person.deleted, person.groups_updated, company.created, company.updated, object.created, note.created and reminder.triggered.
Install this skill
npx skills add hookdeck/webhook-skills --skill folk-webhooks
folk is a CRM. Its webhooks report changes to people, companies, custom objects (such as deals), notes, reminders and person enrichments. They follow the Standard Webhooks spec, so every delivery is signed with webhook-id, webhook-timestamp and webhook-signature headers, and folk recommends the official Standard Webhooks libraries for verification.
When to Use This Skill
- How do I receive folk webhooks?
- How do I verify a folk webhook signature?
- Where do I find my folk webhook signing secret? (It is shown only once.)
- How do I handle
person.created,person.updated,company.updatedorobject.createdevents? - Why is my folk
webhook-signatureverification failing? - Why are Hookdeck retries or replays of folk webhooks rejected after 5 minutes?
Verification (core)
| Header | Value |
|---|---|
webhook-id | Unique event ID (same as the payload's id), stable across redeliveries |
webhook-timestamp | Unix seconds of the delivery |
webhook-signature | v1,<base64 HMAC-SHA256>, possibly several space-separated entries |
Signed content is {webhook-id}.{webhook-timestamp}.{raw body}. The key is the base64-decoded part of the secret after whsec_ (for example whsec_). folk shows the secret only once, when the webhook is created.
const { Webhook } = require('standardwebhooks');
// rawBody: exact request body string, NOT re-serialised JSON
function verifyFolkWebhook(rawBody, headers, secret = process.env.FOLK_WEBHOOK_SECRET) {
return new Webhook(secret).verify(rawBody, {
'webhook-id': headers['webhook-id'],
'webhook-timestamp': headers['webhook-timestamp'],
'webhook-signature': headers['webhook-signature'],
}); // returns the parsed event; throws → respond 400
}
Python: from standardwebhooks.webhooks import Webhook, then Webhook(secret).verify(await request.body(), dict(headers)).
5-minute window. The
standardwebhooksandsvixlibraries always enforce a fixed 5-minute timestamp window, and it can't be changed. Hookdeck retries and replays resend the original timestamp, so the library rejects any retry or replay that arrives more than 5 minutes after folk signed it. If you receive folk webhooks through Hookdeck and need late retries or replays to verify, use the dependency-free verifier in references/verification.md: it checks every space-separatedv1,<base64>entry in constant time, and lets you drop or widen the window.
For complete handlers with tests, see examples/express/, examples/nextjs/, examples/fastapi/. The Express example reads the raw body with
express.raw({ type: '*/*' })so any content type verifies.
Event Envelope
{ "id": "evt_6004f2ef-dcc3-4c56-a704-9326730ad100", "type": "person.updated",
"createdAt": "2025-09-30T09:55:52.715Z",
"source": "https://api.folk.app/v1/webhooks/wbk_2f15ec4f-0f70-4884-a43d-1f88ef7a6665",
"data": { "id": "per_05837e2d-9bf7-4f17-8729-e95f1be4de67",
"url": "https://api.folk.app/v1/people/per_05837e2d-9bf7-4f17-8729-e95f1be4de67",
"changes": [{ "path": ["firstName"], "type": "set", "value": "John" }] } }
Payloads are thin: data.id and data.url identify the resource (GET data.url for the full record). Updates add data.changes, deletions add data.details. Dispatch on the body's type.
Common Event Types
| Event | Fired when |
|---|---|
person.created / person.updated / person.deleted | A person is created, updated (native attributes or custom fields) or deleted |
person.groups_updated | A person's groups change |
person.workspace_ | A person's workspace interaction metadata changes |
company.created / company.updated / company.deleted | A company is created, updated or deleted |
company.groups_updated | A company's groups change |
object.created / object.updated / object.deleted | A custom object (for example a deal) is created, updated or deleted |
note.created / note.updated / note.deleted | A note is created, updated or deleted |
reminder.created / reminder.updated / reminder.deleted / reminder.triggered | A reminder changes or fires |
person_ | A person enrichment completes or fails |
Full payloads: references/overview.md and folk's events documentation.
Delivery Semantics
- Return a
2xxquickly and process asynchronously. Any other status is a failure. - folk retries for up to one day (immediately, 5s, 5m, 30m, 2h, 5h, 10h, 14h, 20h, 24h).
- After all retries fail, or on a high error rate, folk deactivates the webhook and emails you. A
410 Goneresponse deactivates it immediately. - Events can arrive out of order and more than once. Dedupe on
webhook-id.
Environment Variables
FOLK_WEBHOOK_SECRET=whsec_xxxxx # the webhook's signing secret; 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 folk --path /webhooks/folk
Create a webhook in your workspace API settings (or with POST https://api.folk.app/v1/webhooks) that targets the printed URL, and subscribe to person.created to start.
Reference Materials
- references/overview.md - Event catalog, payload fields, retries
- references/setup.md - Creating webhooks (dashboard and API), saving the secret, deactivation
- references/verification.md - Library and manual verification, dropping or widening the 5-minute window, debugging