Get a Phone Alert When a File Reaches a Box Folder
Verify one Box V2 webhook, filter it to a selected folder, and send a privacy-minimal Ping to the people waiting for a file.

To get a shared phone alert when a file reaches one Box folder, create a Box V2 webhook for that folder, verify Box's signatures in a small relay, and send a fixed message to a PingRoom Room. Start with Box's own folder email setting when one collaborator's email is enough. The relay is useful when a stable group needs the same short signal without receiving the filename, uploader, path, contents, or link.
Box defines FILE.UPLOADED as a file being uploaded or moved into the watched folder. The alert therefore says that a file reached the folder, not that a brand-new file was created.
Choose the smaller route first
| What you need | Best starting point |
|---|---|
| One person's basic upload alert by email | Box folder notification settings |
| One fixed signal for an opt-in group | Box V2 webhook → signed relay → PingRoom |
| Filename, preview, comments, or approval | Keep the work in Box or its workflow tools |
| Audit history, guaranteed escalation, or life-safety response | Use a system designed and governed for that requirement |
Box lets collaborators control email notifications at the account and folder level. It also notes that a person does not receive an email notification for their own action. If that route already fits, stop there: the custom relay adds secrets, a queue, monitoring, and rotation work.
Set the privacy boundary first
Use one fixed Ping:
- Title:
Box file ready - Message:
A file reached the selected Box folder
Do not copy Box's source.name, uploader, path, file ID, contents, or shared link into the Ping. Box remains the source of truth; PingRoom carries only the moment the Room needs.

In the illustrated relay, the duplicate drawer closes before one clean signal leaves the gate. Pingo, PingRoom's fictional CMO, is there as an editorial observer—not as a Box collaborator, webhook owner, or autonomous sender. The human team still chooses the folder, Room, copy, and access policy.
Prepare Box and PingRoom
This is a webhook recipe, not a native Box 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 Box Developer Console, create an application with Manage Webhooks scope and authorize the account that owns the selected folder. Box recommends a service account or another durable owner so the webhook does not disappear when a person's access changes.
- Create a V2 webhook on the selected folder with the
FILE.UPLOADEDtrigger. Box does not allow the root folder (0) as the target. The notification URL must be public HTTPS on port 443. - Store the Box webhook ID, selected folder ID, primary and secondary signature keys, and PingRoom URL as secrets. Never put them in browser code, logs, screenshots, or a public repository.
The PingRoom Room owner needs active Pro for incoming webhooks, and a Room can have up to four. The incoming-webhook walkthrough covers Room setup; the webhook reference carries current fields, limits, idempotency behavior, and errors.
Verify, filter, and queue before replying
Box V2 webhooks cascade into subfolders. If the requirement is the selected folder's direct children only, check source.parent.id instead of trusting the webhook target alone.
The dependency-free module below targets a server runtime with Fetch and Web Crypto APIs. Its queue.putOnce(key, value) adapter must durably deduplicate by key before it resolves. A queue worker calls deliverBoxEvent; failed PingRoom responses must retry the same job and event ID.
// box-relay.mjs const encoder = new TextEncoder(); function json(status, body) { return new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json; charset=utf-8" }, }); } function constantTimeEqual(left, right) { if (left.length !== right.length) return false; let difference = 0; for (let index = 0; index < left.length; index += 1) { difference |= left.charCodeAt(index) ^ right.charCodeAt(index); } return difference === 0; } async function hmacBase64(secret, rawBody, timestamp) { const key = await crypto.subtle.importKey( "raw", encoder.encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"], ); const signed = new Uint8Array( await crypto.subtle.sign( "HMAC", key, new Uint8Array([...rawBody, ...encoder.encode(timestamp)]), ), ); return btoa(String.fromCharCode(...signed)); } async function hasValidSignature(request, rawBody, env) { const timestamp = request.headers.get("box-delivery-timestamp") ?? ""; const deliveredAt = Date.parse(timestamp); if (!Number.isFinite(deliveredAt)) return false; if (Math.abs(Date.now() - deliveredAt) > 10 * 60 * 1_000) return false; const candidates = [ ["box-signature-primary", env.BOX_PRIMARY_SIGNATURE_KEY], ["box-signature-secondary", env.BOX_SECONDARY_SIGNATURE_KEY], ]; for (const [header, secret] of candidates) { const supplied = request.headers.get(header) ?? ""; if (!supplied || typeof secret !== "string" || !secret) continue; const expected = await hmacBase64(secret, rawBody, timestamp); if (constantTimeEqual(supplied, expected)) return true; } return false; } function selectedFileEvent(payload, env) { return ( payload?.type === "webhook_event" && typeof payload.id === "string" && payload.id.length > 0 && payload.trigger === "FILE.UPLOADED" && payload.webhook?.id === env.BOX_WEBHOOK_ID && payload.source?.type === "file" && typeof payload.source.id === "string" && payload.source.parent?.id === env.BOX_FOLDER_ID ); } export async function receiveBoxWebhook(request, env, queue) { if (request.method !== "POST") { return new Response("Method not allowed", { status: 405, headers: { allow: "POST" }, }); } const rawBody = new Uint8Array(await request.arrayBuffer()); if (rawBody.byteLength > 65_536) { return json(413, { error: "payload_too_large" }); } if (!(await hasValidSignature(request, rawBody, env))) { return json(403, { error: "invalid_signature" }); } let payload; try { payload = JSON.parse(new TextDecoder().decode(rawBody)); } catch { return json(400, { error: "invalid_json" }); } if (!selectedFileEvent(payload, env)) { return new Response(null, { status: 204 }); } await queue.putOnce(payload.id, { eventId: payload.id }); return new Response(null, { status: 204 }); } export async function deliverBoxEvent(job, env, fetchImpl = fetch) { const response = await fetchImpl(env.PINGROOM_WEBHOOK_URL, { method: "POST", headers: { "content-type": "application/json", "Idempotency-Key": `box:${job.eventId}`, }, body: JSON.stringify({ title: "Box file ready", message: "A file reached the selected Box folder", correlation_id: `box:${job.eventId}`, }), }); if (!response.ok) throw new Error(`PingRoom returned ${response.status}`); }
Box signs the exact raw body followed by BOX-DELIVERY-TIMESTAMP with HMAC-SHA256. During key rotation, a valid request may verify with either configured signature. Verify before parsing, compare without an early content exit, and reject a delivery outside the ten-minute freshness window.
The body id remains stable when Box retries an event; the delivery ID header changes. That is why the body ID owns both queue deduplication and PingRoom's Idempotency-Key. Queueing must finish before the receiver returns 204. Box documents retries after failures or timeouts—currently up to five attempts over roughly an hour, with that schedule subject to change.
Test the exact boundaries
The production check for this article extracted the module above and ran it against mocks. It covered primary and secondary signatures, stale and invalid signatures, the selected webhook and trigger, direct-parent filtering, subfolder rejection, duplicate event IDs, fixed private copy, PingRoom acceptance, and retry after a non-success response. No live Box app, public relay, PingRoom Room, customer data, or phone was used.
Before connecting real traffic, use a disposable folder and confirm:
- Upload one harmless file directly into the selected folder; expect one fixed Ping.
- Move one harmless file directly into it; expect the same “reached” copy.
- Upload into a child folder; expect no Ping from this direct-child recipe.
- Deliver the same Box event body twice; expect one queued job and one PingRoom event identity.
- Return an error from the queue and then from PingRoom; confirm Box or the worker retries without changing the event ID.
- Rotate one Box signing key at a time and confirm the other valid signature still passes.
Also test a locked phone. PingRoom acceptance means the event entered the Room's normal delivery path; it does not prove that a particular phone displayed it. Connectivity, notification permissions, Focus settings, Room settings, and device state still apply.
Keep the signal smaller than the file workflow
The Room should answer one question: did a file reach the handoff folder? Box still owns the file, permissions, version history, comments, and review. Keep durable processing state and audit records in the systems built for them.
If that boundary fits, create or choose the destination Room, add one incoming webhook, and rehearse the six cases above with non-sensitive files.
Sources
PingRoom
Short-form communication.


