Send a Shared Phone Alert When a Zoom Recording Is Ready
Verify Zoom's recording-completed webhook, queue one privacy-minimal event, and send a shared Ping without exposing recording details.

To send one shared phone alert when a Zoom cloud recording is ready, subscribe a Zoom app to recording.completed, verify the signed webhook in a small relay, and send a fixed message to a PingRoom Room. Use Zoom's own recording notification when the host or another named person only needs email. The relay is for a stable group that needs the same short signal without copying the meeting topic, recording link, transcript, or participants into the alert.
Zoom remains the place to open, review, share, and manage the recording. The Ping only answers: is the cloud recording ready for review?
Choose the smaller route first
| What you need | Best starting point |
|---|---|
| The host needs to know when processing finishes | Zoom's native recording notification |
| A named person should also receive the recording email | Zoom's recording-email settings |
| One fixed signal for an opt-in group | Zoom webhook → verified relay → PingRoom |
| Recording access, playback, transcript, or approval | Keep that work in Zoom and your governed review system |
| Guaranteed escalation or emergency response | Use a system designed and governed for that requirement |
Zoom's New recording available setting can notify the host and, where the account setting allows it, selected alternatives such as an alternative host. If that covers the people who need the update, stop there. A custom relay introduces a public endpoint, secrets, a durable queue, retries, and monitoring.
Keep recording details out of the Ping
Use one fixed message:
- Title:
Zoom recording ready - Message:
Open Zoom to review the cloud recording
Do not forward the meeting topic, UUID, host name, participant data, recording files, transcript, passcode, or share URL. The relay below uses the Zoom account ID and meeting UUID only to derive a one-way event key, then queues that opaque key. Zoom retains the useful details and permissions.

In the illustration, one token reaches the accepted tray while its duplicate stays behind the divider. Pingo, PingRoom's fictional CMO, observes the boundary; he cannot enter a private Room, access Zoom, or send a Ping. The human team chooses the Zoom account, host, Room, message, and access policy.
Prepare Zoom and PingRoom
This is a webhook recipe, not a native Zoom integration in PingRoom.
- Create or choose a private PingRoom Room for the people who need the signal. Open Settings → Connections → From another app, add an incoming webhook with Custom message, and copy its complete URL into protected server-side configuration.
- In the Zoom App Marketplace, create an app that can subscribe to webhook events for the account you control. Add a public HTTPS event endpoint and subscribe to Recording → All Recordings have completed (
recording.completed). Complete Zoom's endpoint validation and obtain the app's secret token. - Limit the recipe to one expected Zoom account and one expected host. Store those IDs, the Zoom secret token, and the PingRoom webhook URL as server-side secrets. Do not put them in browser code, logs, screenshots, or a public repository.
- Enable cloud recording only for the Zoom users who need it. Zoom documents cloud recording for licensed users on paid accounts; account and group administrators can lock recording and notification settings. Confirm the plan, license, retention policy, consent practice, and app approval with the account owner before connecting live meetings.
The PingRoom Room owner needs active Pro for incoming webhooks, and a Room can have up to four. The incoming-webhook walkthrough covers the Room-side setup; the webhook reference carries current fields, limits, idempotency behavior, and errors.
Verify, filter, and queue before replying
Zoom signs the exact raw request body using the app secret token, the x-zm-request-timestamp, and HMAC-SHA256. Verify that signature before parsing JSON. This example also rejects a request more than three minutes from the server clock; keep the relay clock synchronized.
The dependency-free Node module below expects a Fetch-compatible runtime. Its queue.putOnce(key, value) adapter must durably insert once by key before it resolves. A queue worker calls deliverRecordingReady; a non-success PingRoom response must retry the same job and event key.
// zoom-relay.mjs import { createHash, createHmac, timingSafeEqual } from "node:crypto"; function json(status, body) { return new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json; charset=utf-8" }, }); } function equalText(left, right) { const leftBytes = Buffer.from(left); const rightBytes = Buffer.from(right); return ( leftBytes.length === rightBytes.length && timingSafeEqual(leftBytes, rightBytes) ); } function validZoomSignature(request, rawBody, secret, now = Date.now()) { const timestamp = request.headers.get("x-zm-request-timestamp") ?? ""; const supplied = request.headers.get("x-zm-signature") ?? ""; if (!/^\d{10}$/.test(timestamp)) return false; if (Math.abs(now / 1_000 - Number(timestamp)) > 3 * 60) return false; const expected = `v0=${createHmac("sha256", secret) .update(`v0:${timestamp}:${rawBody}`) .digest("hex")}`; return equalText(supplied, expected); } function selectedRecording(payload, env) { const recording = payload?.payload?.object; return ( payload?.event === "recording.completed" && payload.payload?.account_id === env.ZOOM_ACCOUNT_ID && recording?.host_id === env.ZOOM_HOST_ID && typeof recording.uuid === "string" && recording.uuid.length > 0 && recording.uuid.length <= 256 ); } function eventKey(accountId, meetingUuid) { return `zoom-recording:${createHash("sha256") .update(`${accountId}\0${meetingUuid}`) .digest("hex")}`; } export async function receiveZoomWebhook(request, env, queue) { if (request.method !== "POST") { return new Response("Method not allowed", { status: 405, headers: { allow: "POST" }, }); } const rawBody = await request.text(); if (Buffer.byteLength(rawBody) > 65_536) { return json(413, { error: "payload_too_large" }); } if (!validZoomSignature(request, rawBody, env.ZOOM_SECRET_TOKEN)) { return json(403, { error: "invalid_signature" }); } let payload; try { payload = JSON.parse(rawBody); } catch { return json(400, { error: "invalid_json" }); } if (payload?.event === "endpoint.url_validation") { const plainToken = payload?.payload?.plainToken; if ( typeof plainToken !== "string" || plainToken.length === 0 || plainToken.length > 256 ) { return json(400, { error: "invalid_plain_token" }); } return json(200, { plainToken, encryptedToken: createHmac("sha256", env.ZOOM_SECRET_TOKEN) .update(plainToken) .digest("hex"), }); } if (!selectedRecording(payload, env)) { return new Response(null, { status: 204 }); } const key = eventKey(payload.payload.account_id, payload.payload.object.uuid); await queue.putOnce(key, { eventKey: key }); return new Response(null, { status: 204 }); } export async function deliverRecordingReady(job, env, fetchImpl = fetch) { const response = await fetchImpl(env.PINGROOM_WEBHOOK_URL, { method: "POST", redirect: "error", headers: { "content-type": "application/json", "Idempotency-Key": job.eventKey, }, body: JSON.stringify({ title: "Zoom recording ready", message: "Open Zoom to review the cloud recording", correlation_id: job.eventKey, }), }); if (!response.ok) throw new Error(`PingRoom returned ${response.status}`); }
Zoom's endpoint-validation event carries a plainToken; the response must echo it and add an HMAC-SHA256 encryptedToken. The code verifies the request first, then handles that challenge. For recording events, it requires the exact event, account, host, and nonempty meeting UUID.
The opaque SHA-256 key gives retries for the same account and meeting the same queue identity without retaining the UUID in the queued value. The worker reuses that key as PingRoom's Idempotency-Key and correlation_id. Queue insertion must finish before the receiver returns 204; otherwise an acknowledgement could arrive before the event is durable.
Test the boundaries, not just the happy path
The production check for this article extracted the module above and ran it against mocks. It covered the valid signature, three-minute freshness window, endpoint validation, exact event/account/host filters, stable deduplication, fixed private copy, PingRoom acceptance, and retry after a non-success response. No live Zoom app, meeting, public relay, PingRoom Room, customer data, or phone was used.
Before connecting real traffic, use a disposable Zoom app and a harmless recorded meeting:
- Complete endpoint validation; expect the plain and encrypted tokens Zoom requires.
- Deliver a signed
recording.completedevent for the selected account and host; expect one queued job and one fixed Ping. - Change the event, account, or host; expect no Ping.
- Deliver the same recording event twice; expect one durable queue key and one PingRoom event identity.
- Change the body after signing it, then replay an otherwise valid event after the freshness window; expect both requests to fail verification.
- Make the queue write fail, then make PingRoom return an error; confirm the webhook receiver and worker retry without changing the event key.
- Lock a test phone and check the real delivery conditions your Room depends on.
PingRoom accepting an event means it entered the Room's normal delivery path. It does not prove that a particular phone displayed it: connectivity, notification permission, Focus settings, Room settings, and device state still matter.
Keep the signal smaller than the recording workflow
Do not turn the Room into a second recording index. Zoom should continue to own the recording, access control, retention, sharing, and review. PingRoom carries one short handoff moment to a known group.
If that boundary fits, create or choose the destination Room, add one incoming webhook, and rehearse the seven cases above with a disposable meeting before enabling the live subscription.
Sources
- Zoom: Changing cloud recording settings
- Zoom: Changing recording notification settings
- Zoom Developer Platform: Meeting webhook events
- Zoom Developer Platform: Webhook reference
- PingRoom webhook reference
PingRoom authored this walkthrough. Zoom is a trademark of its owner. PingRoom is not affiliated with or endorsed by Zoom.
PingRoom
Short-form communication.


