Create endpoints, verify the X-PickFu-Signature HMAC, and react to survey.completed and order.paid events.
v2 delivers events to HTTPS endpoints you register with POST /v2/webhook-endpoints. Deliveries are signed with an HMAC secret so you can verify they came from PickFu.
The /v2/webhook-endpoints resource is a standard CRUD collection under the write scope for mutations and read for reads.
Create
POST /v2/webhook-endpoints with { url, enabledEvents } returns the endpoint and the signing secret — this is the only time the secret is returned. Store it before you exit the response.
List and read
GET /v2/webhook-endpoints (cursor-paginated) and GET /v2/webhook-endpoints/{id}. Neither includes the signing secret.
Update
PATCH /v2/webhook-endpoints/{id} changes the URL or the subscribed events.
Delete
DELETE /v2/webhook-endpoints/{id} stops all deliveries to that endpoint.
Endpoint IDs are prefixed whk_…. Each team may register up to 10 endpoints.
{ "id": "evt_01J9C2VZH8...", "type": "survey.completed", "createdAt": "2026-09-10T18:25:00.000Z", "data": { "object": { /* full survey or order resource */ } }}
id is stable across retries of the same occurrence — use it to dedupe. Reply with any 2xx status within your timeout to acknowledge; a non-2xx or timeout is retried.
import crypto from "node:crypto";function verifyPickFuSignature(header: string, rawBody: string, secret: string) { const parts = Object.fromEntries( header.split(",").map((p) => p.trim().split("=")) ); const t = parts["t"]; const v1 = parts["v1"]; if (!t || !v1) return false; const ageSeconds = Math.abs(Date.now() / 1000 - Number(t)); if (ageSeconds > 5 * 60) return false; // Reject anything that isn't a 64-char hex digest before comparing — // timingSafeEqual throws on length mismatch, which would surface an // attacker-controlled header value as a request-triggered error. if (!/^[0-9a-f]{64}$/i.test(v1)) return false; const expected = crypto .createHmac("sha256", secret) .update(`${t}.${rawBody}`) .digest("hex"); return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1));}
Verify against the raw request body (bytes on the wire), not the parsed JSON. Re-serialising the parsed body will change whitespace, key order, or number formatting and break the signature.
Retries. Non-2xx responses and timeouts are retried with backoff. The envelope id stays the same across retries, so idempotent processing keys off id.
Ordering. Deliveries are best-effort in-order per endpoint but not strictly ordered — always trust createdAt and the current resource state in data.object over delivery order.
Rotating the secret. Delete the endpoint and create a new one. The signing secret is only ever returned at creation time.
Was this page helpful?
Assistant
Responses are generated using AI and may contain mistakes.