How to Send Jotform Submission Alerts to a Shared Phone Room
Validate one Jotform form in a fast relay, discard the response payload, and send fixed, replay-safe copy to a small PingRoom Room.

To send a Jotform submission alert to a shared group of phones, point one form's Webhooks integration at a small private relay. The relay should accept only the expected form, discard the submitted answers, enqueue one event before Jotform's timeout, and send fixed copy to a PingRoom incoming webhook.
This guide sends New Jotform submission — Workshop request received a response. The Ping contains no name, email, answer, upload, form title, or submission link. Jotform remains the response record. This is a webhook recipe you configure, not a native Jotform integration.
Use Jotform's own notifications first
Jotform already offers mobile push for selected forms and documents email, SMS, WhatsApp, Slack, and Microsoft Teams options. Use one of those when it reaches the right people.
| Need | Better fit |
|---|---|
| A Jotform account owner wants normal submission push | Jotform Mobile |
| A team already handles submissions in Slack or Teams | Jotform's matching integration |
| A small, stable group needs one terse signal in a private Room | Jotform webhook → private relay → PingRoom |
| People need answers, files, identity, review state, or an audit log | Jotform, not the lock-screen alert |
The relay path has an important privacy tradeoff: Jotform sends the submission payload to the relay. The code below reads only the form and submission IDs, but your infrastructure still receives the other fields. Do not use this path if policy requires the response to stay inside Jotform.
Treat the relay URL as a credential
Jotform's basic form webhook does not provide a signed request that this recipe can verify. Jotform's public support guidance suggests putting a secret in the webhook URL. That is bearer-URL authentication, not cryptographic proof that Jotform sent the request.
Use this pattern only for a low-risk attention cue. Generate a long random secret for the path, configure ingress and access logs to redact it, and rotate it if exposed. If a false alert could authorize work, move money, release a person, change care, or create another harmful action, use a source with verifiable delivery and keep the authoritative workflow outside the Ping.
For an illustrative workshop-request form, the Room receives:
New Jotform submission
Workshop request received a response.
Pingo the Pigeon, PingRoom's fictional Chief Marketing Officer (CMO), appears beside a closed response hatch in the illustrations. His contribution is to keep the detailed cards on the other side. He cannot inspect Jotform, enter a private Room, or send a Ping. People control the form, relay, Room membership, and follow-up.
Prepare one form, one Room, and one queue
You need:
- one Jotform form and its numeric form ID;
- a relay that can durably enqueue an event and return within Jotform's default five-second webhook timeout;
- a high-entropy route secret generated with a cryptographic random source;
- a private PingRoom Room whose owner has active PingRoom Pro for incoming webhooks.
Follow the incoming-webhook setup: open the Room's Settings → Connections → From another app, add a webhook named Jotform, choose Custom message, and copy its complete URL into server-side secret storage. The webhook reference carries the current fields, limits, replay behavior, cooldown, and errors.
Configure these relay secrets:
| Variable | Value |
|---|---|
JOTFORM_ROUTE_SECRET | At least 32 random URL-safe characters |
JOTFORM_FORM_ID | The one numeric form ID allowed to create this signal |
JOTFORM_FORM_LABEL | Fixed non-sensitive copy such as Workshop request |
PINGROOM_WEBHOOK_URL | The complete PingRoom incoming-webhook URL |
In Jotform, open Form Builder → Settings → Integrations, search for Webhooks, and add:
https://relay.example.com/jotform/<JOTFORM_ROUTE_SECRET>
Jotform's current webhook setup guide says the integration posts submission data to the endpoint and uses a five-second timeout by default. An encrypted form sends encrypted data. Test this recipe with a disposable form that contains no sensitive fields before considering a real workflow.
Validate metadata, then enqueue
The receiver below accepts Jotform's form payload, reads only formID and submissionID, and does not parse rawRequest. It caps the complete request at 128 KiB, validates the one allowed form, hashes the two IDs into a stable event key, and writes a fixed job to a durable idempotent queue.
import { createHash } from "node:crypto"; const MAX_REQUEST_BYTES = 128 * 1024; function validPingRoomUrl(value) { return /^https:\/\/api\.pingroom\.io\/api\/webhooks\/[^/?#\s]+\/[^/?#\s]+$/.test( value || "", ); } function validLabel(value) { const label = (value || "").trim(); return label && Array.from(label).length <= 70 && !/[\r\n\t]/.test(label) ? label : null; } export async function acceptJotform(request, env, queue) { if ( !/^[A-Za-z0-9_-]{32,}$/.test(env.JOTFORM_ROUTE_SECRET || "") || !/^\d{6,30}$/.test(env.JOTFORM_FORM_ID || "") || !validLabel(env.JOTFORM_FORM_LABEL) || !validPingRoomUrl(env.PINGROOM_WEBHOOK_URL) ) { return new Response("server configuration error", { status: 500 }); } const expectedPath = `/jotform/${encodeURIComponent(env.JOTFORM_ROUTE_SECRET)}`; if (new URL(request.url).pathname !== expectedPath) { return new Response("not found", { status: 404 }); } if (request.method !== "POST") { return new Response("method not allowed", { status: 405 }); } const contentType = request.headers.get("content-type") || ""; if ( !contentType.startsWith("multipart/form-data;") && !contentType.startsWith("application/x-www-form-urlencoded") ) { return new Response("unsupported content type", { status: 415 }); } const rawBody = await request.arrayBuffer(); if (rawBody.byteLength > MAX_REQUEST_BYTES) { return new Response("payload too large", { status: 413 }); } let form; try { form = await new Request("https://relay.invalid/parse", { method: "POST", headers: { "Content-Type": contentType }, body: rawBody, }).formData(); } catch { return new Response("invalid form payload", { status: 400 }); } const formId = String(form.get("formID") || ""); const submissionId = String(form.get("submissionID") || ""); if (formId !== env.JOTFORM_FORM_ID || !/^\d{6,30}$/.test(submissionId)) { return new Response("ignored", { status: 202 }); } const digest = createHash("sha256") .update(`${formId}:${submissionId}`) .digest("hex"); const eventKey = `jotform-${digest}`; await queue.putOnce(eventKey, { eventKey, label: validLabel(env.JOTFORM_FORM_LABEL), }); return new Response("accepted", { status: 200 }); } export async function deliverJotform(job, env, fetchImpl = fetch) { if (!validPingRoomUrl(env.PINGROOM_WEBHOOK_URL)) { throw new Error("Invalid PingRoom webhook URL."); } const body = JSON.stringify({ action: null, title: "New Jotform submission", message: `${job.label} received a response.`, correlation_id: job.eventKey, }); let response; try { response = await fetchImpl(env.PINGROOM_WEBHOOK_URL, { method: "POST", headers: { "Content-Type": "application/json", "Idempotency-Key": job.eventKey, }, body, redirect: "error", signal: AbortSignal.timeout(8000), }); } catch { throw new Error("PingRoom request failed."); } let accepted = false; try { accepted = (await response.json()).success === true; } catch { // A non-JSON response cannot confirm acceptance. } if (response.status !== 200 || !accepted) { throw new Error( `PingRoom did not accept the Ping (HTTP ${response.status}).`, ); } }
queue.putOnce is an application-owned durable operation: the first write for an event key stores the job; an exact repeat returns success without inserting another job. Do not replace it with an in-memory set in production. The worker retries failed PingRoom calls with the same key and identical body.
Jotform assigns a unique submission ID to each entry. Hashing that ID with the allowed form ID gives the relay stable replay identity without placing either number in the Ping. It does not prove that the submission is genuine; the route secret remains this recipe's source-authentication boundary.

AI-generated editorial illustration. Pingo is PingRoom's fictional mascot; the people and workflow are illustrative.
Test five boundaries with a harmless form
Before enabling a real form, submit disposable data and check:
- The expected secret path, form ID, and numeric submission ID enqueue one job and return HTTP
200within five seconds. - A wrong path returns
404; a different form returns202; neither creates a job. - Replaying the same form and submission IDs creates no second queue entry.
- The queued job and Ping contain no
rawRequest, name, email, answer, upload, title, IP address, username, or submission link. - A PingRoom rejection keeps the job retryable with the same event key and identical body.
For this article, we ran the exact embedded receiver and consumer against Jotform-shaped multipart fixtures, an idempotent in-memory queue, and a local mock of PingRoom's documented contract. The check covered accepted delivery, exact replay, a second submission, wrong path, wrong form, invalid identifiers, malformed form data, request size and type limits, private-field exclusion, invalid configuration, PingRoom rejection, malformed upstream output, and network failure. It made no live Jotform, public-relay, PingRoom, customer-data, or phone request.
Know what the alert proves
HTTP 200 from the relay means the event was durably accepted for processing. HTTP 200 with success: true from PingRoom means the Ping was accepted and queued. Neither response proves that a person reviewed the submission or that every phone displayed an alert under every network, Focus, mute, battery, or notification setting.
Keep Jotform's submission table, permissions, retention, and review process authoritative. Do not use a shared lock-screen alert for healthcare, safeguarding, payment, identity, legal, emergency, or other sensitive response details.
If the boundary fits, open PingRoom, create a small private Room, and test one disposable Jotform submission from end to end.
PingRoom
Short-form communication.


