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.updated or object.created events?
  • Why is my folk webhook-signature verification failing?
  • Why are Hookdeck retries or replays of folk webhooks rejected after 5 minutes?

Verification (core)

HeaderValue
webhook-idUnique event ID (same as the payload's id), stable across redeliveries
webhook-timestampUnix seconds of the delivery
webhook-signaturev1,<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_QWFSUzl1QUVoQW1kdWtpTnJRTUFpbXNlZmxLTg==). 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 standardwebhooks and svix libraries 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-separated v1,<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

EventFired when
person.created / person.updated / person.deletedA person is created, updated (native attributes or custom fields) or deleted
person.groups_updatedA person's groups change
person.workspace_interaction_metadata_updatedA person's workspace interaction metadata changes
company.created / company.updated / company.deletedA company is created, updated or deleted
company.groups_updatedA company's groups change
object.created / object.updated / object.deletedA custom object (for example a deal) is created, updated or deleted
note.created / note.updated / note.deletedA note is created, updated or deleted
reminder.created / reminder.updated / reminder.deleted / reminder.triggeredA reminder changes or fires
person_enrichment.finishedA person enrichment completes or fails

Full payloads: references/overview.md and folk's events documentation.

Delivery Semantics

  • Return a 2xx quickly 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 Gone response 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


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Nov 6, 2026

View on GitHub →