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.

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

Migrating an existing legacy integration?

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.

  1. Open your survey in the editor and select Save. A new, unsaved survey displays “Save this survey before configuring a webhook.”
  2. Open Settings, then find Integrations → Signed webhooks. No active webhook means the new signed endpoint is not active.
  3. 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.
  4. 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.
  5. Confirm the Pending · [host] destination, then select Send pending test. This sends test: true with a synthetic sample answer. It creates no survey response and consumes no response or media quota. No respondent answers are sent by this test.
  6. 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.
  7. 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.
  8. 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

  1. Read X-iSurvey-Timestamp as Unix seconds and X-iSurvey-Signature as v1= followed by 64 lowercase hexadecimal characters. Reject missing or malformed values.
  2. Reject timestamps more than five minutes in the past or future. Keep your receiver clock synchronized.
  3. 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.
  4. Compare the 32-byte digests in constant time. Only then parse JSON, validate the event and deduplicate its id before 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" }
  }
}
Fields in signed webhook events
FieldMeaning
idStable event ID across retries. Tests receive a new ID each time you send one.
eventresponse.created: a newly created response, or a synthetic test of that shape.
testtrue for synthetic tests; false for real responses.
surveyIdThe survey that owns this webhook.
responseIdPresent for live events; absent from synthetic tests.
createdAtEvent creation time. This remains unchanged when a delivery is retried.
responseThe 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.

Limits in the current signed-webhook implementation
LimitBehavior
Delivery timeout10 seconds including destination resolution and connection.
Payload sizeMaximum 1 MiB (1,048,576 bytes). Larger bodies fail before sending.
Delivery rate60 attempts per minute per owner, shared across surveys, tests and retries.
Tests and manual retriesShare a limit of 5 requests per minute per owner.
Endpoint preparation and rotationShare a limit of 10 requests per minute per owner.
Live retry windowUp to 7 days and 20 total recorded attempts, including the initial attempt. Failures before an HTTP request, including delivery-rate limits, can consume attempts.
HistoryNewest 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

  1. Select Rotate signing secret on an active webhook. This prepares a new secret for the same stored URL.
  2. 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.
  3. Select Send pending test, confirm delivery and verification, then select Activate pending.
  4. 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

Symptoms and next steps
SymptomWhat to check
Signed webhooks is missing or management is unavailableConfirm the application and backend versions with your administrator. Source availability does not prove deployment readiness.
Save prompt or survey not foundSave the survey and configure it while signed in as its owner.
Sign in againComplete the fresh sign-in prompt, then retry the action. Credential changes and tests require recent authentication.
Invalid endpointUse 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 405Use the final URL and correct POST route. The demo only accepts POST at /webhook.
HTTP 401 from the example receiverCheck 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 receiverIt intentionally rejects live events. Use a production receiver for real answers.
HTTP 503 from the example receiverIts 1,000-event demo deduplication store is full. Restart it for synthetic testing; use durable storage in production.
Failed with no HTTP statusCheck 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 attemptsWait until the next minute before another test or preparation. Check owner-wide traffic across surveys.
Activate pending is rejectedInstall the current pending secret, send a successful pending test, then activate that same version. Preparing again invalidates the previous pending version.
Retry is rejectedConfirm 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 resultA 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.