A signed POST, the moment somebody finishes.
Plumeform sends a response.completed event to your endpoint as JSON, signed with HMAC-SHA256. Delivery starts after the response is saved.
Pro. Saving a webhook URL provisions the signing secret for that survey.
01Save the URL, keep the secret
https:// endpoint. Saving generates a signing secret for that survey, shown below the URL to owners and editors (never to viewers). Store it securely on your server. Secrets are unique to each survey. Private IP addresses and internal hostnames are refused, including localhost while developing: use a tunnel with a public hostname.02Fire a test event
"test": true in the body and a zeroed response ID. Its answers hold one example per question of the published survey, keyed and shaped like a real response, so a tool that maps fields from it sees your actual fields. It reports your endpoint's HTTP status so you can test signature verification. Like live delivery, it follows no redirects and times out after five seconds. Owners and editors can send it.03Read the payload
answers is keyed by question id, and the values are stored values rather than labels: a choice is its option id, a rating is its number. That keeps a payload stable when you rename an option. To turn ids into readable text, fetch the survey once from GET /api/v1/surveys/:id and cache its questions.
{
"event": "response.completed",
"survey_id": "8f1c…",
"response_id": "4b90…",
"submitted_at": "2026-09-28T16:47:24.172913+00:00",
"answers": {
"q_satisfaction": 4,
"q_recommend": 9,
"q_comment": "The checkout was quick."
}
}submitted_at is the database timestamp, with microseconds and a +00:00 offset. The test event sends the shorter …Z form, so parse the value as a timestamp rather than matching its text.
04Verify the signature
Every request carries X-Plumeform-Signature: sha256=<hex>: an HMAC-SHA256 signature of the raw request body using the survey’s secret. Compute it over the received bytes before parsing JSON, then compare in constant time. Reject unsigned requests and mismatched signatures.
import { createHmac, timingSafeEqual } from "node:crypto";
// Verify against the RAW body, before any JSON parsing.
export function verify(rawBody, header, secret) {
const sent = (header ?? "").replace(/^sha256=/, "");
const mine = createHmac("sha256", secret).update(rawBody).digest("hex");
const a = Buffer.from(sent, "hex");
const b = Buffer.from(mine, "hex");
return a.length === b.length && timingSafeEqual(a, b);
}05Acknowledge before processing
What gets delivered, and what does not
Only completed, non-test responses fire. Partial responses, screened-out participants and anything submitted with ?test=1 do not, which means a webhook cannot be used to watch drop-off. There is one event type; there is no response.started, no update event and no delete event.
Each event is delivered once, with no automatic retries. We record the outcome of each attempt. To request a replay, email us with the survey and time window. Use scheduled Results API requests to reconcile responses if your integration must recover missed events.
Delivery logs store the hostname only, keeping any tokens in the URL path out of those logs.
See also: the results API · Google Sheets sync · security & data practices