Webhooks need a public URL Tally can POST to. That’s awkward when your handler lives in a localhost-only dev server. This guide walks through the setup — tunneling, registration, verification, iteration — and the gotchas worth knowing about.Documentation Index
Fetch the complete documentation index at: https://docs.tallyforagents.com/llms.txt
Use this file to discover all available pages before exploring further.
The shape
1. Install and start a tunnel
2. Register the webhook in the dashboard
In Tally’s dashboard, go to Webhooks → New endpoint:-
URL:
https://abc123.ngrok-free.app/api/webhooks/tally(or wherever your handler lives). -
Events: pick what you need. For a payment flow, start with
payment.created,payment.confirmed, andpayment.failed. -
Signing secret: copy this. It looks like
whsec_test_<base64url>, shown exactly once. Save it to.env.local:
3. Write the handler
- Raw body, not parsed. The HMAC is over the exact bytes Tally signed. Calling
req.json()and re-stringifying breaks the match. - Return 2xx fast. Tally times out at 10 seconds. If you have expensive downstream work, kick off a background job and return immediately.
- Be idempotent. Tally may deliver an event twice on retries. Key off
tally-delivery-idoridfrom the payload, and short-circuit duplicates.
4. Trigger an event to test
The simplest way to test: send a real payment from another terminal.payment.created, then payment.confirmed (or payment.failed) hit your handler.
5. Iterate with Replay
When your handler throws or returns a 5xx, Tally retries with backoff. While you’re iterating, that’s slow — you don’t want to wait 1m → 5m → 15m for a retry while fixing a bug. Instead, use Replay:- Fix the bug in your handler.
- In the dashboard, open the failed webhook’s detail page.
- Click Replay on the delivery row.
Gotchas
ngrok URL changes between sessions
The free tier gives you a different URL each time you restart ngrok. The webhook you registered againstabc123.ngrok-free.app won’t work tomorrow.
Workarounds:
- Use a paid ngrok plan with a reserved subdomain.
- Use cloudflared with a permanent Cloudflare tunnel.
- Re-register the webhook against the new URL each session.
Body parsing middleware
Some frameworks parse the body automatically (Express withbody-parser, etc.), which makes req.text() return an empty string. You’ll need to opt out for the webhook route — most frameworks have a “give me the raw body” mode for this exact case.
Next.js App Router gets this right by default — req.text() returns the raw bytes.
Clock skew
The signature includes a timestamp the verifier checks against (default tolerance: 5 minutes). If your laptop’s clock is more than 5 minutes off, every signature will fail withtimestamp_out_of_tolerance. Fix the clock; don’t widen the tolerance.
Going to production
Three things change when you move from local dev to a deployed environment:- Switch to a real URL.
https://api.yourapp.com/webhooks/tally. - Re-register the webhook in the dashboard pointing at the production URL. If you used a
tly_test_key for development, also register a separatewhsec_live_endpoint when you opt into live mode. - Move the signing secret to your production secret manager. Same handler code; just a different env var source.
Where to go from here
- Webhooks (concept) — event types, retry schedule, replay model.
- SDK webhooks —
verifySignaturereference. - Handle failures and retries — how to keep your handler robust under load.