Guide to Tally Webhooks: Features and Best Practices
Tally webhooks notify your application when someone submits one of your forms. If you're routing form submissions into a CRM, a database, or a downstream workflow, webhooks are how you react in real time without polling.
This guide covers how Tally webhooks work, the single event you'll handle, how to verify the optional Tally-Signature (and what to do when there's no secret), the retry behavior, and the best practices for production.
What are Tally webhooks?
Tally webhooks are HTTP POSTs sent to a URL you configure per form, delivered when a form is submitted. Tally webhooks are free on all plans. Signing is optional: if you set a signing secret, deliveries carry a Tally-Signature header; if you don't, requests are unsigned, so your verification code has to handle both cases.
Tally webhook features
| Feature | Details |
|---|---|
| Configuration | Form's Integrations tab (or the API signingSecret field) |
| Event | FORM_RESPONSE (the only event type) |
| Signature header | Tally-Signature (case-insensitive); present only if a secret is set |
| Signature scheme | base64 HMAC-SHA256 over the payload, keyed with the signing secret |
| Secret | Optional; unsigned when unset |
| Acknowledgement | Return 2xx within a 10-second timeout |
| Retries | 5 min, 30 min, 1 hr, 6 hr, 1 day |
| Plan | Free on all plans |
| SDK | No official SDK |
The event
There's one event type, FORM_RESPONSE. The payload looks like:
{
"eventId": "...",
"eventType": "FORM_RESPONSE",
"createdAt": "...",
"data": {
"responseId": "...",
"submissionId": "...",
"respondentId": "...",
"formId": "...",
"fields": [ /* answers */ ]
}
}
Answers live in data.fields. Use data.responseId (or data.submissionId) as an idempotency key.
Setting up Tally webhooks
Add a webhook in the form's Integrations tab (or via the API). Optionally set a signing secret there (or with the API signingSecret field). If you set one, verify the Tally-Signature; if you don't, requests are unsigned and you should authenticate the endpoint another way.
Securing Tally webhooks
When a signing secret is set, each delivery carries a Tally-Signature header: a base64 HMAC-SHA256 over the payload, keyed with the secret. Verify against the raw body and compare in constant time. Crucially, handle the unsigned case: if no secret is configured, there's no header to check, so decide explicitly whether to accept unsigned requests (and if so, add another layer such as a secret path).
const crypto = require("crypto");
const SIGNING_SECRET = process.env.TALLY_SIGNING_SECRET; // may be unset
function verify(rawBody, signature) {
if (!SIGNING_SECRET) return false; // no secret: don't treat as verified
const expected = crypto
.createHmac("sha256", SIGNING_SECRET)
.update(rawBody)
.digest("base64");
const a = Buffer.from(expected);
const b = Buffer.from(signature || "");
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
app.post("/webhook", express.raw({ type: "application/json" }), (req, res) => {
const signature = req.headers["tally-signature"];
if (SIGNING_SECRET && !verify(req.body, signature)) {
return res.sendStatus(401);
}
// If SIGNING_SECRET is unset, requests are unsigned -- gate the endpoint another way.
res.sendStatus(200); // acknowledge within 10s
processQueue.add(JSON.parse(req.body)); // process asynchronously
});
Tally's own example signs JSON.stringify(payload); verifying against the raw body avoids re-serialization differences, so prefer the raw body where your framework preserves it.
Tally webhook limitations and pain points
Signing is optional
The Problem: If no secret is set, deliveries are unsigned, and an endpoint that assumes a signature will either reject everything or (worse) accept anything. Any POST to an unsigned webhook URL looks legitimate.
Why It Happens: Tally makes the signing secret opt-in per webhook.
Workarounds:
- Always set a signing secret and verify
Tally-Signature. - If you must accept unsigned deliveries, gate the endpoint with a secret path or a header your infrastructure checks.
How Hookdeck Can Help: Hookdeck gives every source a dedicated URL and can enforce its own authentication, so an unsigned Tally webhook isn't your only line of defense.
One event, answers in data.fields
The Problem: There's only FORM_RESPONSE, and the answers are nested in data.fields. Handlers expecting top-level answer fields miss the data.
Why It Happens: Tally models every submission as a single event with a fields array.
Workarounds:
- Read answers from
data.fields, mapping by field key/label.
How Hookdeck Can Help: Hookdeck's transformations can reshape the payload before it reaches your app, so downstream code gets the structure it expects.
Retries bring duplicates
The Problem: Failed deliveries retry on a 5 min, 30 min, 1 hr, 6 hr, 1 day ladder, so the same submission can arrive more than once.
Why It Happens: At-least-once delivery favors eventual delivery.
Workarounds:
- Dedupe on
data.responseId(ordata.submissionId) and make side effects idempotent.
How Hookdeck Can Help: Hookdeck deduplicates deliveries at the edge, so retries don't double-process submissions. See our guide to webhook idempotency.
The 10-second timeout
The Problem: You must return 2xx within 10 seconds or the delivery is retried. Slow synchronous work (CRM writes, downstream calls) risks the window.
Why It Happens: Tally caps how long it waits for an ack.
Workarounds:
- Acknowledge fast and process off a queue.
How Hookdeck Can Help: Hookdeck acknowledges Tally within the window and durably queues submissions, decoupling the ack from your processing time.
Best practices
Set a secret and verify (and handle unsigned)
Always configure a signing secret and verify Tally-Signature over the raw body in constant time; if a webhook is unsigned, gate the endpoint another way rather than trusting it.
Read answers from data.fields and dedupe
Map answers from data.fields, and dedupe on data.responseId.
Acknowledge within 10 seconds, process asynchronously
Return 2xx immediately and defer work to a queue. See why to process webhooks asynchronously.
Make Tally webhooks production-ready
Hookdeck verifies Tally-Signature, adds auth when a webhook is unsigned, deduplicates, and durably queues every submission
Conclusion
Tally webhooks are simple, free on every plan, one FORM_RESPONSE event, with answers in data.fields, but the details that matter are that signing is optional (so handle the unsigned case deliberately), the retry ladder brings duplicates, and there's a 10-second ack window.
Hookdeck verifies the signature, adds authentication when a webhook is unsigned, deduplicates, and durably queues every submission at the edge, so your app only ever processes trustworthy, unique form responses.
Get started with Hookdeck for free and handle Tally webhooks reliably in minutes.