Webhooks
Receive a signed submission.created event on your own HTTPS endpoint for every new submission, and verify it with any Standard Webhooks library.
Webhooks are available on Pro. Nisuform sends a POST request with a JSON body to your URL for every new submission. Each delivery is signed following the Standard Webhooks spec, so you can prove it came from Nisuform.
Connect a webhook
- Build an endpoint that accepts a
POSTwith a JSON body and answers with any2xxstatus. - Open the form's Integrations tab and choose Webhook.
- Paste your endpoint's URL. It must start with
https://. - Copy the Signing secret that appears. It starts with
whsec_. Store it on your server, for example asNISUFORM_WEBHOOK_SECRET. - Choose Send test to check that your endpoint receives and verifies it.
You can find the secret again under Signing secret and payload on the webhook. Changing the URL later keeps the same secret.
The request
POST /webhooks/nisuform HTTP/1.1
Content-Type: application/json
User-Agent: nisuform-webhooks/1
webhook-id: msg_3f2c8a61-5d4e-4b7a-9c1f-2a6e8d0b4c17
webhook-timestamp: 1790244001
webhook-signature: v1,K5oZfzN95Z9UVu1EsfQmfVNQhnkZ2pj9o9NDN/H/pI4={
"type": "submission.created",
"timestamp": "2026-09-24T10:00:01.000Z",
"data": {
"formId": "8d7e6f5a-4b3c-4d2e-9f1a-0b9c8d7e6f5a",
"submissionId": "3f2c8a61-5d4e-4b7a-9c1f-2a6e8d0b4c17",
"submittedAt": "2026-09-24T10:00:00.000Z",
"test": false,
"fields": {
"name": "Jane Doe",
"email": "jane@example.com",
"message": "Hello!"
}
}
}| Field | Meaning |
|---|---|
type | Always submission.created. |
timestamp | When this delivery was sent. |
data.formId | The form's ID. |
data.submissionId | The submission's ID, the same one the submit endpoint returned. |
data.submittedAt | When the submission was received. |
data.test | true for test submissions sent from the dashboard. |
data.fields | The answers by field name. Every value is a string. Special fields and files are not included. |
The dashboard shows a sample payload with your form's own fields under Signing secret and payload.
Verify the signature
Always verify deliveries before you trust them. Use a Standard Webhooks library for your language with the whsec_ secret. It checks the signature and rejects deliveries older than 5 minutes, which stops replays.
import { Webhook } from 'standardwebhooks'
const webhook = new Webhook(process.env.NISUFORM_WEBHOOK_SECRET)
export async function POST(request) {
const payload = await request.text()
let event
try {
event = webhook.verify(payload, Object.fromEntries(request.headers))
} catch {
return new Response('Invalid signature', { status: 400 })
}
console.log(event.data.fields)
return new Response(null, { status: 204 })
}Verify the raw body exactly as it arrived. Parsing the JSON and serializing it again changes the bytes and breaks the signature.
Verify without a library
The signature is an HMAC-SHA256 of {webhook-id}.{webhook-timestamp}.{body}, keyed with the secret after removing the whsec_ prefix and base64-decoding it. The header holds v1, followed by the base64 digest.
import { createHmac, timingSafeEqual } from 'node:crypto'
function verify(secret, headers, body) {
const id = headers.get('webhook-id')
const timestamp = headers.get('webhook-timestamp')
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) {
return false
}
const key = Buffer.from(secret.replace(/^whsec_/, ''), 'base64')
const expected = createHmac('sha256', key).update(`${id}.${timestamp}.${body}`).digest('base64')
return headers
.get('webhook-signature')
.split(' ')
.some((part) => {
const [version, signature] = part.split(',')
return version === 'v1' && signature.length === expected.length &&
timingSafeEqual(Buffer.from(signature), Buffer.from(expected))
})
}Respond quickly
- Answer with any
2xxstatus within 10 seconds. The body is ignored. - Do slow work, like sending emails or calling other APIs, after you respond or in a background job.
- Redirects are not followed. A
3xxresponse counts as a failure, so use the final URL.
Retries and duplicates
Timeouts, network errors, 408, 429 and 5xx responses are retried with growing delays for about two hours. Other responses, like 400, 401 or 404, are not retried and mark the webhook as failing on the Integrations tab. See Delivery status and retries.
Every attempt for the same submission carries the same webhook-id, so your endpoint may receive a submission more than once. Store the ID, or the submissionId, and skip events you have already handled.
Rotate the secret
If the secret leaks, choose Rotate secret under Signing secret and payload. Deliveries are signed with the new secret right away, and your server rejects them until you update it there. Disconnecting the webhook deletes its secret, so reconnecting always issues a new one.