Available on Free, Pro and Business
Receive survey responses with webhooks
FormShark sends a signed JSON POST request to your server when a new response is created. Use it to start your own workflow or store incoming answers.
No Business API key is needed to receive webhooks. The webhook signing secret verifies incoming messages. It is separate from the bearer API key used to read data through the Business API.
This guide covers the updated signed-webhook controls. Availability depends on the app version you are using. If you do not see Signed webhooks, check with your FormShark administrator before proceeding.
1. Prepare a receiver
You need a server that accepts JSON POST requests at a public HTTPS URL on port 443, such as https://receiver.example.com/webhook. This is a placeholder for a URL you control, not a FormShark service address.
- Use valid HTTPS with public DNS. Localhost, private or reserved IP addresses, URL usernames/passwords, URL fragments and non-443 ports are rejected. Every resolved address must be public.
- Provide the final receiving URL. Redirects are not followed; a login page or redirect is a failed delivery.
- Preserve the raw request body and signature headers through your proxy. Verify before parsing or processing answers.
- Return a 2xx status within ten seconds after safely accepting the event. For production, atomically record the event ID and enqueue work durably before acknowledging it.
Try the synthetic Node.js receiver
Download webhook-receiver.cjs and install Node.js 22 or later. It uses built-in modules, so there are no packages to install. From the directory where you saved it, run:
node webhook-receiver.cjs --self-test
Expected output: Signature checks passed. This local test checks signature verification without credentials or a FormShark connection.
Next, prepare your HTTPS forwarding service or reverse proxy to forward its public /webhook path to http://127.0.0.1:3000/webhook on this machine. Do not enter the localhost URL into FormShark. The example binds to loopback and provides no TLS itself.
After copying the secret in step 2, inject it into the receiver process as ISURVEY_WEBHOOK_SECRET using your secret manager or hosting environment, then run:
node webhook-receiver.cjs
Expected startup message: Synthetic receiver: http://127.0.0.1:3000/webhook; use your own public HTTPS forwarding endpoint for delivery tests. Keep the receiver and HTTPS forwarding service running while testing.
This example accepts synthetic tests only. It returns 422 for real response events. It keeps at most 1,000 event IDs in memory, loses them on restart, and has no durable queue or business processing. Use it to complete the pending test, then build and test production handling before activating real deliveries. A production receiver needs durable deduplication, access controls, monitored queues and appropriate respondent-data retention; removing the synthetic-only check alone is insufficient.
2. Configure, test and activate
Preparing the first signed endpoint stops delivery through the older custom, Slack or other legacy integration for this survey, even before you activate the pending endpoint. Responses created during that gap are not automatically backfilled. Have the new receiver ready and plan the change with its owner before selecting Prepare endpoint. The uninterrupted replacement steps below apply when a signed endpoint is already active.
- Open your survey in the editor and select Save. A new, unsaved survey displays “Save this survey before configuring a webhook.”
- Open Settings, then find Integrations → Signed webhooks. No active webhook means the new signed endpoint is not active.
- Enter your public receiver URL in New HTTPS endpoint and select Prepare endpoint. This creates a pending endpoint and a new signing secret. An existing active signed endpoint continues receiving events until replacement activation.
- Select Copy secret and store it in your receiver’s secret store. Set up or restart the receiver with that value before continuing. The secret is shown once; Hide secret, leaving the app, authentication changes or another action can clear the display. If you lose it, prepare the endpoint again, copy the replacement and retest.
- Confirm the Pending · [host] destination, then select Send pending test. This sends
test: truewith a synthetic sample answer. It creates no survey response and consumes no response or media quota. No respondent answers are sent by this test. - Select Refresh delivery history. Expect Test · Delivered · HTTP 204 with the example receiver (any 2xx response counts as delivery). A failed test needs a new Send pending test after you fix the receiver. Verify that your receiver actually checks signatures: a 2xx reply alone proves transport, not authentication or downstream processing.
- Once your production receiver is ready for real data and the current pending version has passed a test, select Activate pending. The server requires a successful pending test before activation. The status becomes Active · [host]; subsequent new response events can contain real respondent data.
- Use Send live-secret test to test the active configuration later. Despite its name, it still sends synthetic answers; “live-secret” refers to the active signing secret.
Management actions can request a fresh sign-in. Complete that sign-in yourself and return to the workflow. Never share your password, bearer key or signing secret to get assistance.
3. Verify every incoming request
- Read
X-iSurvey-Timestampas Unix seconds andX-iSurvey-Signatureasv1=followed by 64 lowercase hexadecimal characters. Reject missing or malformed values. - Reject timestamps more than five minutes in the past or future. Keep your receiver clock synchronized.
- Compute HMAC-SHA256 with the signing secret over the timestamp text, a literal period, and the exact raw request body bytes. Use the copied secret as text; do not base64-decode it.
- Compare the 32-byte digests in constant time. Only then parse JSON, validate the event and deduplicate its
idbefore processing.
const expected = createHmac('sha256', secret)
.update(timestamp + '.')
.update(rawBody) // Buffer: unchanged incoming request bytes
.digest();
// Validate timestamp and signature format first.
const valid = timingSafeEqual(
expected,
Buffer.from(signature.slice(3), 'hex')
);
This excerpt illustrates the calculation. Use the complete receiver example for timestamp, format and length checks. Parsing and reserializing JSON, changing whitespace or normalizing line endings before verification will change the signature.
Timestamps limit replay age; durable event-ID deduplication prevents repeated effects within that window and across retries. The body’s id is covered by the signature. Check that X-Idempotency-Key agrees with it if using the header in your implementation.
Request and payload reference
Illustrative headers follow. The timestamp and signature are placeholders; they cannot authenticate the example body.
POST /webhook
Content-Type: application/json
User-Agent: iSurvey-Webhook/3.0
X-iSurvey-Timestamp: <Unix seconds at this delivery attempt>
X-iSurvey-Signature: v1=<64 lowercase hexadecimal characters>
X-Idempotency-Key: test_example
A synthetic test has this shape:
{
"id": "test_example",
"event": "response.created",
"test": true,
"surveyId": "survey_example",
"createdAt": "2026-09-05T12:00:00.000Z",
"response": {
"data": { "sample": "Synthetic test answer" }
}
}
| Field | Meaning |
|---|---|
id | Stable event ID across retries. Tests receive a new ID each time you send one. |
event | response.created: a newly created response, or a synthetic test of that shape. |
test | true for synthetic tests; false for real responses. |
surveyId | The survey that owns this webhook. |
responseId | Present for live events; absent from synthetic tests. |
createdAt | Event creation time. This remains unchanged when a delivery is retried. |
response | The stored response data for live events; a fixed sample object for tests. Real answer fields depend on the survey; do not hardcode the sample field as a live schema. |
Live events use the same envelope with test: false and an additional responseId. Editing an existing response does not create a response.created event. Activation does not backfill earlier responses.
Delivery history, retries and limits
Delivery may occur more than once or out of order. Failed live deliveries are retried by the backend trigger; there is no guaranteed exact retry interval. Retries preserve the event ID and body and use a fresh signing timestamp. They use the currently active endpoint and secret, so a retry after replacement can go to the new destination.
In Refresh delivery history, each row shows Test or Live, Delivered or Failed, HTTP status (or a dash when unavailable), local time, duration and event ID. Select Retry on a failed live row after fixing the cause. The backend rejects events already delivered, test events, events older than seven days, and events that have reached 20 attempts. An old failed row can therefore show Retry even when that event is no longer eligible. Test failures require a new test.
| Limit | Behavior |
|---|---|
| Delivery timeout | 10 seconds including destination resolution and connection. |
| Payload size | Maximum 1 MiB (1,048,576 bytes). Larger bodies fail before sending. |
| Delivery rate | 60 attempts per minute per owner, shared across surveys, tests and retries. |
| Tests and manual retries | Share a limit of 5 requests per minute per owner. |
| Endpoint preparation and rotation | Share a limit of 10 requests per minute per owner. |
| Live retry window | Up to 7 days and 20 total recorded attempts, including the initial attempt. Failures before an HTTP request, including delivery-rate limits, can consume attempts. |
| History | Newest 50 attempt records. Records have a 30-day expiry; automatic removal depends on deployment retention policies being enabled and is not instantaneous. |
History excludes raw destination URLs, secrets and request/response bodies. Keep your own sanitized operational records. Do not log full respondent answers just to diagnose transport issues.
Rotate, replace or disable an endpoint
Rotate the signing secret
- Select Rotate signing secret on an active webhook. This prepares a new secret for the same stored URL.
- Select Copy secret and install the pending secret in your receiver. During the transition, a production receiver may need to accept both old and new secrets so the active endpoint continues to work. The synthetic example supports one secret only.
- Select Send pending test, confirm delivery and verification, then select Activate pending.
- Remove the old secret after in-flight requests have completed. Future attempts use the newly active secret. Retired server versions are disabled by scheduled cleanup after they are at least a day old; this is not a promise that future deliveries use the old secret for a day.
A production receiver should accept valid synthetic tests without running its live workflow. Keep test: true events separate from real answers so future connection checks do not trigger business actions.
To change the destination, enter a new New HTTPS endpoint and repeat Prepare endpoint → Copy secret → Send pending test → Activate pending. Preparing another pending version supersedes the previous pending secret; test the replacement before activation.
Stop delivery
Select Disable webhook on an active webhook. This clears both active and pending configuration. Requests already in flight may still arrive; previously delivered data remains at your receiver. If a secret is compromised, disable promptly, secure the receiver, then prepare, test and activate a replacement. Responses created while disabled are not automatically backfilled.
Keep secrets in server-side secret storage, never browser code, URLs, repositories, screenshots or support tickets. Clear the clipboard after storing a copied secret. Treat received respondent data according to your access and retention requirements.
Troubleshooting
| Symptom | What to check |
|---|---|
| Signed webhooks is missing or management is unavailable | Confirm the application and backend versions with your administrator. Source availability does not prove deployment readiness. |
| Save prompt or survey not found | Save the survey and configure it while signed in as its owner. |
| Sign in again | Complete the fresh sign-in prompt, then retry the action. Credential changes and tests require recent authentication. |
| Invalid endpoint | Use a public HTTPS URL on 443 with no credentials or fragment. A hostname resolving to a private or reserved address also fails delivery. |
| HTTP 3xx, 404 or 405 | Use the final URL and correct POST route. The demo only accepts POST at /webhook. |
| HTTP 401 from the example receiver | Check pending versus active secret, clock skew, exact body bytes and forwarded signature headers. Restart the receiver after updating its environment secret. |
| HTTP 422 from the example receiver | It intentionally rejects live events. Use a production receiver for real answers. |
| HTTP 503 from the example receiver | Its 1,000-event demo deduplication store is full. Restart it for synthetic testing; use durable storage in production. |
| Failed with no HTTP status | Check public DNS, TLS, timeout, receiver reachability, rate limits and payload size. Server history uses a sanitized error and may not distinguish these causes. |
| Too many attempts | Wait until the next minute before another test or preparation. Check owner-wide traffic across surveys. |
| Activate pending is rejected | Install the current pending secret, send a successful pending test, then activate that same version. Preparing again invalidates the previous pending version. |
| Retry is rejected | Confirm an endpoint is active and the event is an undelivered live event within the age and attempt limits. For tests, send a new test. |
| Delivered but no workflow result | A 2xx only acknowledges transport. Inspect your receiver’s durable queue and processing, using event IDs rather than exposing answers or secrets. |
Legacy integrations: Older custom endpoints can use X-Signature-256 and a different payload. This guide’s timestamp-based verifier does not validate that format. Plan migration to Signed webhooks with the endpoint owner, including receiver changes; do not assume the legacy setup follows the pending-replacement behavior described here.
When asking for help, provide the event ID, time and displayed HTTP status. Do not include signing secrets or respondent data.