How to Send Tally Form Response Alerts to Your Team's Phones
Verify Tally's signed form webhook, keep answer content out of the alert, and send one fixed Ping when a response arrives.

To send a Tally form response alert to a shared phone-alert Room, connect Tally's Webhooks integration to a small HTTPS relay. The relay should verify Tally's signature, accept responses from one expected form, discard the answer content, and send one fixed message to a PingRoom incoming webhook.
This guide sends New Tally response — Workshop request received a response. The answers, respondent details, PDF link, and preview link stay in Tally. This is a webhook recipe you configure, not a native Tally integration.
Choose email, Slack, or a shared Room
Tally already offers automatic email notifications. Its Slack integration can send a submission notification to a channel or direct message. Use those first when they already reach the right people.
| Need | Better fit |
|---|---|
| One person checks an inbox | Tally email notification |
| The team already works in a Slack channel or DM | Tally's native Slack integration |
| A small opt-in Room needs one short phone signal | The signed Tally-to-PingRoom relay below |
| Another system needs the submitted answers | An approved backend with its own access controls |
The Room should contain only the people who need the signal. It does not replace Tally's response record, permissions, retention policy, or follow-up process.
Decide what the phone is allowed to reveal
Use a fixed, non-sensitive label such as Workshop request, Volunteer signup, or Equipment check. Do not build the lock-screen message from Tally's fields array or its response links.
For an illustrative workshop form, the Room receives:
New Tally response
Workshop request received a response.
The team follows its normal authorized route to Tally for the details. The alert does not name the respondent, quote an answer, or claim that anyone has reviewed or accepted the submission.
Pingo the Pigeon, our fictional CMO, appears beside the sealed response box in the illustrations. His contribution is a suspicious look at the duplicate signal tile. He cannot read the form, access a private Room, or send a Ping; the human team chooses the copy, membership, and follow-up.
Prepare the two webhook ends
Tally says webhooks are available free to all Tally users. Publish the form, open Integrations, connect Webhooks, and enter your relay's HTTPS endpoint. Add a signing secret and keep it in your relay as TALLY_SIGNING_SECRET.
The destination Room's owner needs active PingRoom Pro for incoming webhooks. Follow the incoming webhook setup: open the Room's Settings → Connections → From another app, add a webhook named Tally, choose Custom message, and copy its URL.
The complete PingRoom webhook URL is a credential. Store it as PINGROOM_WEBHOOK_URL in server-side configuration; never place it in the Tally form, browser code, source control, screenshots, or logs. Also store the expected Tally form ID as TALLY_FORM_ID and your approved public-safe label as TALLY_FORM_LABEL.
Verify, filter, and forward
Tally documents a Tally-Signature header calculated as an HMAC-SHA256 over JSON.stringify(payload), encoded in Base64. This Node-compatible handler follows that expression, compares the digest in constant time, accepts only FORM_RESPONSE from the configured form, and never reads the fields, submissionPdfUrl, or submissionPreviewUrl values.
import { createHash, createHmac, timingSafeEqual } from "node:crypto"; function secureEqual(left, right) { const a = Buffer.from(left); const b = Buffer.from(right); return a.length === b.length && timingSafeEqual(a, b); } function validPingRoomUrl(value) { return /^https:\/\/api\.pingroom\.io\/api\/webhooks\/[^/?#\s]+\/[^/?#\s]+$/.test( value || "", ); } export async function handleTally(request, env, fetchImpl = fetch) { const label = (env.TALLY_FORM_LABEL || "").trim(); if ( !env.TALLY_SIGNING_SECRET || !env.TALLY_FORM_ID || !validPingRoomUrl(env.PINGROOM_WEBHOOK_URL) || !label || Array.from(label).length > 70 || /[\r\n\t]/.test(label) ) { return new Response("server configuration error", { status: 500 }); } let event; try { event = JSON.parse(await request.text()); } catch { return new Response("invalid JSON", { status: 400 }); } const received = request.headers.get("tally-signature") || ""; const expected = createHmac("sha256", env.TALLY_SIGNING_SECRET) .update(JSON.stringify(event)) .digest("base64"); if (!secureEqual(received, expected)) { return new Response("invalid signature", { status: 401 }); } if ( event.eventType !== "FORM_RESPONSE" || event.data?.formId !== env.TALLY_FORM_ID ) { return new Response("ignored", { status: 202 }); } if (typeof event.eventId !== "string" || !event.eventId) { return new Response("invalid event", { status: 400 }); } const digest = createHash("sha256").update(event.eventId).digest("hex"); const eventKey = `tally-${digest}`; const pingBody = JSON.stringify({ action: null, title: "New Tally response", message: `${label} received a response.`, correlation_id: eventKey, }); let pingResponse; try { pingResponse = await fetchImpl(env.PINGROOM_WEBHOOK_URL, { method: "POST", headers: { "Content-Type": "application/json", "Idempotency-Key": eventKey, }, body: pingBody, redirect: "error", signal: AbortSignal.timeout(8000), }); } catch { return new Response("upstream unavailable", { status: 502 }); } let accepted = false; try { accepted = (await pingResponse.json()).success === true; } catch { // A non-JSON response cannot confirm acceptance. } if (pingResponse.status !== 200 || !accepted) { return new Response("upstream unavailable", { status: 502 }); } return new Response("accepted", { status: 200 }); }
The eventId becomes a stable, opaque PingRoom Idempotency-Key after hashing. If Tally repeats the same event, the relay sends the same body with the same key, so PingRoom can return the prior accepted result instead of creating another Ping. Do not add the current time or any response field to that body: either would weaken the duplicate boundary or expose content.

AI-generated editorial illustration. Pingo is PingRoom's fictional mascot; the people and workflow are illustrative.
Test privacy, filtering, and retries
Tally requires a successful 2xx response within 10 seconds. Its documented retries begin after 5 minutes, then continue after 30 minutes, 1 hour, 6 hours, and 1 day when delivery still fails. The handler gives PingRoom eight seconds and returns non-2xx when acceptance cannot be confirmed, leaving time for the relay to respond before Tally's deadline.
Before connecting a real form, use harmless sample data and verify:
- A valid signed
FORM_RESPONSEfrom the configured form produces one fixed Ping. - The Ping body contains no respondent data, answers, form name, PDF URL, preview URL, submission ID, or Tally secret.
- Replaying the same event produces no second Ping.
- A different form or event type is ignored without calling PingRoom.
- A missing or altered signature returns
401and sends nothing. - A PingRoom rejection makes the relay return non-2xx so Tally can retry.
For this article, we imported the exact fenced handler and ran it locally with signed Tally fixtures plus a stateful mock of PingRoom's documented webhook contract. The check covered valid delivery, exact replay, changed events, wrong form, wrong event type, malformed JSON, missing and altered signatures, invalid configuration, PingRoom rejection, malformed upstream responses, and network failure. It made no live Tally, PingRoom, or phone request.
Read acceptance correctly
A PingRoom response with HTTP 200 and success: true means the Ping was accepted and queued. It does not guarantee that every phone displayed an alert under every network, Focus, mute, battery, or notification setting. Current request limits, Room cooldown behavior, and accepted fields are documented in the webhook reference.
Tally remains the source of truth for the response. If the form controls healthcare, safeguarding, payment, legal, emergency, or regulated work, keep the approved case system, access controls, audit record, and escalation path authoritative. The Ping is an attention cue, not proof that someone reviewed or acted on the response.
Start with one harmless form and a small PingRoom Room. If your form lives in Typeform instead, use the separate signed Typeform response recipe.
PingRoom
Short-form communication.


