Webhooks, Zapier and Make
A webhook sends each response to a URL as a POST request with JSON, within seconds of submission. “Failed deliveries are retried for a day.”
Add a webhook
Section titled “Add a webhook”- Open the form, then Settings → Integrations → Webhooks, and select Add webhook.
- URL to send to: a public
https://address. “For Zapier or Make, paste the URL their webhook step gives you.” - Description (optional), such as CRM sync.
- Send when: the events to send (see below).
- Save. Copy this signing secret now. It’s shown only once. Select I’ve saved it.
- Select Send test to send a sample delivery and check that your endpoint receives it.
Each webhook has Edit, New secret (rotate the signing secret) and Delete.
Events
Section titled “Events”| Event | Sent when |
|---|---|
submission.created | A response is accepted. On forms that take payment after submitting, it’s sent once the payment settles. |
payment.succeeded | A payment is confirmed. |
payment.failed | A payment is declined or cancelled. |
The request
Section titled “The request”Headers:
| Header | Meaning |
|---|---|
X-KyraForms-Event | The event name |
X-KyraForms-Delivery | Unique ID of this delivery. A delivery can occasionally arrive twice, so use it to ignore repeats. |
X-KyraForms-Signature | t=<unix seconds>,v1=<hex HMAC-SHA256> |
Until recently these headers were called X-EasyForm-Event, X-EasyForm-Delivery and X-EasyForm-Signature. Deliveries still include the old names, with the same values, so nothing you’ve already built breaks. The old names are deprecated, so switch to the X-KyraForms-* ones when you can.
Body:
{ "id": "cmvh…", "event": "submission.created", "api_version": "2026-10-01", "created_at": "2026-10-21T09:30:00.000Z", "form": { "id": "…", "name": "Apply", "slug": "apply" }, "submission": { "id": "…", "created_at": "…", "answers": [ { "field_id": "field_…", "key": "name", "label": "Full name", "type": "text", "value": "Asha", "text": "Asha" }, { "field_id": "field_…", "key": "field_…", "label": "Plan", "type": "radio", "value": "pro", "text": "Pro plan" } ], "answers_by_key": { "name": "Asha", "field_…": "pro", "utm_source": "linkedin" }, "hidden": { "utm_source": "linkedin" }, "meta": { "ip": "203.0.113.5", "user_agent": "…" } }, "payment": { "id": "…", "status": "paid", "amount": 499, "currency": "INR", "gateway": "razorpay", "order_id": "…" }}keyis the question’s Link parameter when it has one, otherwise its ID. Set link parameters for readable keys that don’t change when you edit a label.textis the answer as a person reads it (option labels, not values).- File answers include a download
urlandurl_expires_at. Download the file before the link expires. metais left out on forms that mask identifying information.payment.amountis in the main unit ofpayment.currency(499 rupees, 19.99 dollars, 500 yen), not in paise or cents.gatewayisrazorpay,stripe,paypal,payu,paytmorbilldesk.- New fields may be added. Existing ones won’t be removed or renamed without a new
api_version.
Verify the signature
Section titled “Verify the signature”Check every request really came from KyraForm. v1 is the hex HMAC-SHA256 of "<t>.<raw request body>", keyed with the webhook’s signing secret. Check it against the raw body before parsing JSON, and refuse requests whose t is more than 5 minutes old. After you rotate the secret, requests carry two v1 values for 24 hours, so accept either.
const crypto = require('crypto');
function verify(header, rawBody, secret) { const t = Number((header.match(/(?:^|,)t=(\d+)/) || [])[1]); if (!t || Math.abs(Date.now() / 1000 - t) > 300) return false; const expected = crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex'); return header.split(',').some((part) => part === `v1=${expected}`);}import hmac, hashlib, time
def verify(header: str, raw_body: bytes, secret: str) -> bool: parts = [p.split("=", 1) for p in header.split(",")] t = next((int(v) for k, v in parts if k == "t"), 0) if abs(time.time() - t) > 300: return False expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest() return any(k == "v1" and hmac.compare_digest(v, expected) for k, v in parts)Retries and the delivery log
Section titled “Retries and the delivery log”- Any
2xxreply counts as delivered. Reply within 10 seconds, and do slow work afterwards. - Anything else, or a timeout, is retried after 1 min, 5 min, 30 min, 2 h, 6 h and 24 h. Then it’s marked Failed.
- Replying
410 Goneswitches the webhook off at once. It’s also switched off after 50 failed deliveries in a row, and the Dashboard’s Needs attention tells you. Switching it back on resets the count. - Redirects aren’t followed. Private, loopback and cloud-metadata addresses are refused.
The delivery log under each webhook shows every delivery’s status (Delivered, Retrying, Sending, Failed, Skipped), Attempts, Next try, how long it Took, and the Problem. Select Send again to resend one. Refresh reloads the log.
Zapier and Make
Section titled “Zapier and Make”- In Zapier, add Webhooks by Zapier → Catch Hook. In Make, add Webhooks → Custom webhook.
- Copy the URL it gives you.
- Add it in KyraForm as a webhook, then select Send test so Zapier or Make can see a sample response.
- Map the answers into the next steps of your Zap or scenario.
n8n works the same way with its Webhook node.