How to Send CircleCI Workflow Completion Alerts to Your Team's Phones
Verify CircleCI's signed workflow-completed webhook, filter one project and workflow, and send a privacy-minimal success Ping to a shared team Room.

To send a CircleCI workflow-completion alert to a small team's phones, create a signed workflow-completed webhook, verify its raw body in a relay, filter one project and workflow, and send fixed success copy to a PingRoom incoming webhook. Use CircleCI's event ID for retry identity, and leave repository, branch, commit, URL, and log details in CircleCI.
This is a webhook recipe, not a native CircleCI integration. The CircleCI user creating the project webhook needs organization-admin permission. The destination Room owner needs PingRoom Pro.
Check CircleCI's own notifications first
CircleCI already offers email, Slack, deployment, and approval notifications. Use those when they reach the right people in a surface the team already follows.
| Need | Better starting point |
|---|---|
| A developer needs normal build details | CircleCI dashboard or email |
| The engineering team already works in Slack | CircleCI's Slack integration |
| A deployment or approval needs its native project context | CircleCI deployment or approval notifications |
| One selected success should reach a separate bounded group of phones | CircleCI outbound webhook → relay → PingRoom Room |
| People need logs, reruns, artifacts, or an audit history | Keep CircleCI as the system of record |
| A failed release needs paging or escalation | Keep the established incident system |
This guide sends only status: "success" for one named workflow. CircleCI also reports failed, error, canceled, and unauthorized terminal states. Do not quietly route those into a cheerful completion message. Give failures their existing engineering or incident path.
Create the destination Room
Create or choose a private PingRoom Room for the stable group that needs the completion signal. Open Settings → Connections → From another app, add a webhook named CircleCI completion, choose Custom message, and copy the full URL.
The incoming-webhook setup guide covers the Room-side steps. The webhook reference documents current fields, limits, idempotency behavior, and errors.
Store these five values in protected server configuration:
CIRCLECI_WEBHOOK_SECRET: a strong secret that you will also enter in CircleCI;CIRCLECI_PROJECT_ID: the exact CircleCI project UUID allowed to send;CIRCLECI_WORKFLOW_NAME: the exact workflow name, such asbuild-test-deploy;PINGROOM_WEBHOOK_URL: the complete secret PingRoom webhook URL;PINGROOM_PROJECT_LABEL: short, reviewed copy such asWebsite.
Do not place either secret in browser code, source control, screenshots, build logs, or the Ping itself.
Verify the raw request before parsing it
CircleCI's circleci-signature header can contain versioned signatures. The current v1 value is an HMAC-SHA256 digest of the raw request body. Verify that exact body before parsing JSON.
The following dependency-free handler uses Web Crypto, so the core verification works in a modern Worker-style runtime. Pass the platform request and environment bindings to handleCircleCIWebhook.
const MAX_BODY_BYTES = 256_000; const HEX_SHA256 = /^[a-f0-9]{64}$/i; const SAFE_EVENT_ID = /^[a-z0-9._:-]{1,200}$/i; function reply(status, body = null) { if (body === null) return new Response(null, { status }); return Response.json(body, { status }); } function getV1Signature(header) { if (!header) return null; const matches = header .split(",") .map((part) => part.trim()) .filter((part) => part.startsWith("v1=")) .map((part) => part.slice(3)); return matches.length === 1 && HEX_SHA256.test(matches[0]) ? matches[0].toLowerCase() : null; } function fromHex(value) { return Uint8Array.from(value.match(/../g), (byte) => Number.parseInt(byte, 16), ); } async function verifyCircleCISignature(header, rawBody, secret) { const signature = getV1Signature(header); if (!signature) return false; const key = await crypto.subtle.importKey( "raw", new TextEncoder().encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["verify"], ); return crypto.subtle.verify( "HMAC", key, fromHex(signature), new TextEncoder().encode(rawBody), ); } export async function handleCircleCIWebhook(request, env, fetchImpl = fetch) { if (request.method !== "POST") { return reply(405, { error: "method_not_allowed" }); } const { CIRCLECI_WEBHOOK_SECRET, CIRCLECI_PROJECT_ID, CIRCLECI_WORKFLOW_NAME, PINGROOM_WEBHOOK_URL, PINGROOM_PROJECT_LABEL, } = env; if ( !CIRCLECI_WEBHOOK_SECRET || !CIRCLECI_PROJECT_ID || !CIRCLECI_WORKFLOW_NAME || !PINGROOM_WEBHOOK_URL?.startsWith("https://") || !PINGROOM_PROJECT_LABEL || PINGROOM_PROJECT_LABEL.length > 48 ) { return reply(500, { error: "server_configuration_error" }); } const declaredLength = Number(request.headers.get("content-length") ?? 0); if (declaredLength > MAX_BODY_BYTES) { return reply(413, { error: "body_too_large" }); } const rawBody = await request.text(); if (new TextEncoder().encode(rawBody).byteLength > MAX_BODY_BYTES) { return reply(413, { error: "body_too_large" }); } if ( !(await verifyCircleCISignature( request.headers.get("circleci-signature"), rawBody, CIRCLECI_WEBHOOK_SECRET, )) ) { return reply(401, { error: "invalid_signature" }); } let event; try { event = JSON.parse(rawBody); } catch { return reply(400, { error: "invalid_json" }); } if ( request.headers.get("circleci-event-type") !== "workflow-completed" || event.type !== "workflow-completed" ) { return reply(204); } if (!SAFE_EVENT_ID.test(String(event.id ?? ""))) { return reply(400, { error: "invalid_event_id" }); } if ( event.project?.id !== CIRCLECI_PROJECT_ID || event.workflow?.name !== CIRCLECI_WORKFLOW_NAME || event.workflow?.status !== "success" ) { return reply(204); } const eventKey = `circleci-${event.id}`; const payload = { title: "CircleCI workflow complete", message: `${PINGROOM_PROJECT_LABEL} finished successfully. Check CircleCI before acting.`, correlation_id: eventKey, }; try { const response = await fetchImpl(PINGROOM_WEBHOOK_URL, { method: "POST", headers: { Accept: "application/json", "Content-Type": "application/json", "Idempotency-Key": eventKey, }, body: JSON.stringify(payload), signal: AbortSignal.timeout(8_000), }); if (!response.ok) { return reply(503, { error: "pingroom_rejected" }); } const accepted = await response.json().catch(() => null); if (accepted?.success !== true) { return reply(503, { error: "pingroom_invalid_response" }); } return reply(200, { ok: true }); } catch { return reply(503, { error: "pingroom_unavailable" }); } }
The handler deliberately ignores the project name, repository URL, branch, commit, author, workflow URL, and timing fields. PINGROOM_PROJECT_LABEL is reviewed server configuration, not text copied from an event. If even the project label is sensitive, use neutral copy such as Selected workflow.
Add the CircleCI webhook
CircleCI currently limits a project to five outbound webhooks, and organization-admin permission is required to create or change them.
- In CircleCI, choose the organization and open Projects.
- Open the intended project's Project Settings → Webhooks.
- Select Add Webhook and give it a descriptive name.
- Enter the public HTTPS URL of the relay and keep certificate validation enabled.
- Enter the same strong secret stored as
CIRCLECI_WEBHOOK_SECRET. - Select Workflow Completed and save.
- Use Test Ping Event. A signed test ping should receive
204because it is not a workflow completion. - Run one harmless successful workflow and confirm that exactly one fixed Ping is accepted.
CircleCI's webhook payload reference describes id as the unique event identifier. The relay reuses it in both correlation_id and Idempotency-Key. A duplicate delivery therefore produces the same request identity and exact body.
Keep the completion claim narrow
“Workflow complete” means the selected CircleCI workflow reached its terminal success state. It does not prove that a deployment is reachable, every dependency is healthy, a release was approved, or every phone displayed the Ping.
| Relay outcome | HTTP result | Meaning |
|---|---|---|
| Signed selected workflow success accepted | 200 | PingRoom accepted the fixed event |
| Signed test ping, another workflow/project, or non-success | 204 | No Ping is needed |
| Invalid signature or altered body | 401 | Sender identity is not trusted |
| Invalid event ID or malformed JSON | 400 | The event cannot be mapped safely |
| PingRoom rejects, times out, or returns bad JSON | 503 | Handoff failed; CircleCI may retry |
CircleCI expects a 2xx response, currently times out a delivery after ten seconds, retries non-2xx or timed-out attempts later, and warns that requests can be duplicated. The relay's eight-second downstream timeout leaves a small margin, but a production system with stronger delivery requirements should acknowledge into a durable queue and process the PingRoom handoff separately.

These AI-generated editorial illustrations feature approved PingRoom avatars and Pingo the Pigeon, PingRoom's fictional Chief Marketing Officer. They are not CircleCI or PingRoom screenshots and do not depict customers.
Pingo the Pigeon is watching a sealed capsule settle beside the finished track. He has no CircleCI account, private-Room access, credentials, or authority to send Pings. People choose the project, workflow, destination, copy, and response procedure.
Test the boundary before relying on it
Use locally signed fixtures first, then one harmless real workflow:
- Sign a selected
successfixture and confirm one fixed Ping is accepted. - Replay the same event ID and confirm the request uses the same key and body.
- Change the project ID and confirm the relay returns
204without calling PingRoom. - Change the workflow name and confirm it is ignored.
- Change the status to
failedand confirm no success Ping is sent. - Alter the body after signing and confirm the relay returns
401. - Make PingRoom return
429and confirm the relay reports503to keep the failed handoff visible. - Check the test phone and confirm the Ping contains no repository, branch, commit, author, URL, log, or error detail.
For this article, we imported the exact handler above and ran it against locally signed CircleCI-style fixtures with a stateful PingRoom mock. Eight cases covered success, exact replay, project filtering, workflow filtering, non-success, test ping, invalid signature, and downstream rejection. No live CircleCI project, PingRoom Room, phone, or network request was used.
If one CircleCI success deserves a separate shared signal, create the destination Room and webhook, deploy the relay, and run all eight checks with non-sensitive data. Use the broader CI/CD guide before adding any other workflow state.
CircleCI is a trademark of Circle Internet Services, Inc. PingRoom is not affiliated with or endorsed by CircleCI.
PingRoom
Short-form communication.


