API and webhooks
Read surveys and responses, sync contacts, and get events pushed to your own endpoint.
The API and webhooks are a Team-plan feature, set up by admins in Settings → API and webhooks.
API keys
Choose Create API key, name it, and give it only the scopes it needs: surveys:read, responses:read, contacts:read or contacts:write. Keys look like wrk_… and are shown once, so store it somewhere safe, because wrkflow can't show it again. Set an expiry to limit the damage if a key ever leaks, and Revoke a key the moment you no longer need it.
Calling the API
Send the key as an Authorization: Bearer wrk_… header to endpoints under /api/v1 for surveys, a single survey, its responses and contacts. List endpoints take limit and cursor (responses also since), and page forward with the next_cursor they return. Each key is limited to 600 requests a minute; past that the API answers 429 with a Retry-After header. Reading a survey’s responses with a key is recorded in the audit log, once a day per key and survey.
List your surveys
curl https://your-site/api/v1/surveys \ -H "Authorization: Bearer wrk_…"
Webhooks
Add a webhook with an endpoint URL and the events it should receive: response.created and survey.published. The URL must be https:// on a public address. Every delivery carries X-Wrkflow-Event, X-Wrkflow-Delivery, X-Wrkflow-Attempt and X-Wrkflow-Signature: t=<unix>,v1=<hex>, an HMAC-SHA256 of <t>.<raw body> using the signing secret you were shown once when you added the webhook.
Verify a delivery
import { createHmac, timingSafeEqual } from "node:crypto";
// header: X-Wrkflow-Signature: t=<unix seconds>,v1=<hex>
export function verify(rawBody: string, header: string, secret: string) {
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
const expected = Buffer.from(createHmac("sha256", secret).update(`${parts.t}.${rawBody}`).digest("hex"));
const given = Buffer.from(parts.v1 ?? "");
const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300;
return fresh && given.length === expected.length && timingSafeEqual(given, expected);
}Use Send test to check your endpoint, and Recent deliveries on the webhook to see what was sent and whether it succeeded. Rotate secret retires the old signing secret in favor of a new one.
When your endpoint is down
Answer with a 2xx within ten seconds. Anything else, a timeout or no answer is tried again after 1 minute, 5 and 30 minutes, 2, 5 and 10 hours and 10 hours again: eight attempts over about a day, each with the same X-Wrkflow-Delivery id and body, so you can ignore one you have already handled. Recent deliveries shows each attempt’s status code and the next try. A delivery that still fails is marked Failed, and Retry sends it again for 30 days. Answer 410 Gone to stop a delivery for good. Pausing a webhook drops the deliveries waiting for a retry.
What a response.created delivery carries
The same data protection as the API: answers to sensitive questions read { "withheld": "sensitive" }, and personal data in written answers (emails, phone numbers, account and ID numbers) is always masked. Anonymous surveys send no attributes and no file names, and only the day a response was given. Their deliveries are held until the survey has 5 responses and go out the day after, in random order; Recent deliveries shows them as Held until then.
Files respondents uploaded aren't part of the event. A key with responses:read can ask for a download link to a file, which works for 60 seconds, and the file itself is returned as the respondent uploaded it, unmasked.
Still stuck? Talk to us and we’ll help you sort it out.