Types
WebhookVerifyInput
WebhookVerifyResult
DEFAULT_TOLERANCE_SECONDS
toleranceSeconds field on WebhookVerifyInput to override.
Methods
tally.webhooks.verifySignature(input)
Verifies a tally-signature header against the raw request body.
WebhookVerifyResult. Synchronous; no HTTP, no allocation beyond the HMAC buffer.
verifySignature(input) (standalone)
The same function, exported directly for consumers who don’t want to instantiate a Tally client just to verify a webhook.
tally.webhooks.list()
Returns every webhook endpoint in the API key’s account + mode. Revoked endpoints are included (with revoked_at set) for audit.
Promise<Webhook[]>.
tally.webhooks.create(input)
Creates a new webhook endpoint. The signing secret is returned in webhook.secret exactly once — store it now; it’s not recoverable later. If you lose it, revoke and recreate.
| Field | Type | Description |
|---|---|---|
input.url | string | Receiver URL. Must be https:// (or http://localhost for dev). |
input.events | WebhookEventType[] | Event types to subscribe to. |
Promise<CreatedWebhook> (a Webhook extended with secret: string).
tally.webhooks.revoke(id)
Revokes a webhook endpoint by id. No further deliveries will be enqueued; deliveries already in-flight are not cancelled. Idempotent: revoking an already-revoked endpoint is a no-op.
Promise<Webhook> (the revoked webhook with revoked_at set).
Full receiver example
Next.js App Router handler:Why pass the raw body
The HMAC is computed over the exact bytes Tally signed —<unix_ts>.<body>. Any transformation (parsing, re-stringifying, whitespace normalization) breaks the match.
In most frameworks, getting the raw body is await req.text() or equivalent. Don’t call req.json() and then JSON.stringify the result back — round-tripping through your runtime’s JSON parser changes key ordering, whitespace, or both, depending on the runtime.
If you’re behind a middleware or framework that consumes the body automatically (Express’s body-parser, for example), you’ll need to opt out for the webhook route. Most frameworks support a “give me the raw bytes” mode for this exact case.
Rejection reasons
WhenverifySignature returns { ok: false, reason }, the reason tells you exactly what failed:
| Reason | Meaning | Likely cause |
|---|---|---|
missing_header | No tally-signature header on the request. | Wrong endpoint, or the proxy stripped headers. |
malformed_header | The header doesn’t parse — couldn’t extract t=. | Manual testing with the wrong format. |
no_v1_signature | The v1= segment is absent. | Same — manual tests, or a future signature version Tally adds. |
timestamp_out_of_tolerance | The t= timestamp is more than toleranceSeconds away from now. | Clock skew on either side, or a replayed payload. |
signature_mismatch | The header parsed and the timestamp is in window, but the HMAC doesn’t match. | Wrong secret, or the body was modified in transit. |
Tolerance window
The default 5-minute tolerance is calibrated for the realities of clock drift across servers and Tally’s retry schedule (the first retry is 1 minute after the initial attempt). Don’t widen it casually:timestamp_out_of_tolerance, fix the clock skew on your receiving server first.
Not yet in the SDK
tally.webhooks.deliveries.list()/replay()— delivery log access. Dashboard-driven today.