Webhooks
A webhook sends your system a message the moment something happens in SurveyMethods, such as a respondent finishing a survey. You don’t have to keep checking for new responses.
Webhooks are included in the Professional, Team Professional and Enterprise plans.
Events
| Event | Sent when |
|---|---|
response.completed | A respondent finishes a survey |
response.created | A respondent opens a survey, before answering anything |
survey.created | A survey is created |
collector.created | A collector (web link, email, embed) is created |
You choose which events each webhook receives, for all your surveys or just one.
Setting up a webhook
- Go to Account > Webhooks.
- Click Create Webhook.
- Enter your endpoint URL and choose the event types. You can also limit the webhook to one survey and add a description.
- Save it, then copy the signing secret you’re shown and store it securely. You need it to verify deliveries.
You can also manage webhooks with the API (GET, POST and DELETE on /api/v1/webhooks). An API-created webhook’s secret is returned only once, in the reply to the request that creates it.
Endpoint requirements
- Address: a full
https://address on the public internet. Addresses on private networks and redirects aren’t followed. - Reply: a
2xxstatus within 30 seconds. Do slow work after replying, not before. - Limit: up to 25 active webhooks per account.
What we send
Each delivery is a POST with a JSON body:
{
"eventId": "6a1f0c3e-2b9d-4f7a-9c51-0e8d2f4b7a10",
"eventType": "response.completed",
"eventTime": "2026-10-23T10:15:02Z",
"data": {
"surveyId": "12345",
"respondentId": 6789,
"collectorId": 456,
"completedDateTimeUtc": "2026-10-23T10:15:00Z",
"responses": {
"910001": "3",
"910002": "Great support team."
}
}
}
The headers include:
| Header | Contents |
|---|---|
X-SurveyMethods-Signature | sha256= followed by the signature (see below) |
X-SurveyMethods-Event-Type | The event, e.g. response.completed |
X-SurveyMethods-Event-Id | The same eventId as in the body |
User-Agent | SurveyMethods-Webhooks/1.0 |
Event data
response.completed: surveyId, respondentId, collectorId, completedDateTimeUtc, and responses. The responses object maps each question id to its stored answer: a choice id, the text entered, or JSON for checkboxes, rankings and matrices. For question text and readable answers, fetch the response with the API:
GET /api/v1/surveys/{surveyId}/responses/{respondentId}?includeAnswers=true
response.created: surveyId, respondentId, collectorId, startedDateTimeUtc.
survey.created: surveyId, title, createdDateTimeUtc.
collector.created: surveyId, collectorId, collectorType (web, email, embed or other), name, createdDateTimeUtc.
Verifying deliveries
Anyone who knows your endpoint’s address could send it requests, so check that each delivery came from SurveyMethods. Each one is signed with your webhook’s secret: X-SurveyMethods-Signature is sha256= followed by the hex HMAC-SHA256 of the raw request body, keyed with the secret.
Compute the same value from the body exactly as you received it (before parsing the JSON) and compare. Reject the request if they don’t match.
Node.js (Express):
const crypto = require('crypto');
app.post('/webhooks/surveymethods', express.raw({ type: 'application/json' }), (req, res) => {
const expected = 'sha256=' + crypto
.createHmac('sha256', process.env.SURVEYMETHODS_WEBHOOK_SECRET)
.update(req.body)
.digest('hex');
const received = req.get('X-SurveyMethods-Signature') || '';
if (received.length !== expected.length ||
!crypto.timingSafeEqual(Buffer.from(received), Buffer.from(expected))) {
return res.status(401).end();
}
const event = JSON.parse(req.body);
res.status(200).end(); // reply first...
handleEvent(event); // ...then do the work
});
Python (Flask):
import hashlib, hmac, os
from flask import request, abort
@app.post("/webhooks/surveymethods")
def surveymethods_webhook():
secret = os.environ["SURVEYMETHODS_WEBHOOK_SECRET"].encode()
expected = "sha256=" + hmac.new(secret, request.get_data(), hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, request.headers.get("X-SurveyMethods-Signature", "")):
abort(401)
event = request.get_json()
# handle the event
return "", 200
C#:
using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
var expected = "sha256=" + Convert.ToHexString(hmac.ComputeHash(Encoding.UTF8.GetBytes(rawBody))).ToLowerInvariant();
var valid = CryptographicOperations.FixedTimeEquals(
Encoding.UTF8.GetBytes(expected), Encoding.UTF8.GetBytes(signatureHeader ?? ""));
Retries
If your endpoint doesn’t reply with a 2xx within 30 seconds, or can’t be reached, we try again, up to 5 attempts in all. Retries are spaced further apart each time.
A delivery can occasionally arrive more than once, so use eventId to skip events you’ve already handled.
To see each delivery, go to Account > Webhooks and click View delivery logs. The logs show the HTTP status, the response body, the attempt number and when the next retry is due.
Testing
- Local testing: use a tunnelling tool such as ngrok to give your local server a public
https://address, and use that address as the endpoint. - Trying it out: create a webhook for one survey, take the survey yourself, and check the delivery logs.
- Request inspectors: a service such as webhook.site shows exactly what we send.
Troubleshooting
Nothing arrives
- Check the webhook is active and subscribed to the right events. If it’s limited to one survey, check it’s the right one.
- Check your plan includes webhooks.
- Look at the delivery logs for the status code your endpoint returned.
Signature doesn’t match
- Sign the raw body exactly as received. Re-serialising parsed JSON changes it.
- Make sure you’re using this webhook’s secret. Each webhook has its own.
Timeouts
- Reply with
200straight away and process the event afterwards, for example from a queue.
Related articles:
Was this article helpful?