# How to Send WooCommerce New-Order Alerts to Your Team's Phones

> Verify WooCommerce's signed order.created webhook, keep order and customer data out of the alert, and send one fixed Ping to a shared fulfillment Room.

- Published: 2026-10-24
- Author: PingRoom
- Category: Automation
- Topics: WooCommerce new order push notification, WooCommerce webhook phone alert, webhook push notifications, automation notifications to phone
- Canonical: https://pingroom.io/blog/woocommerce-new-order-push-notifications
- Cover image: https://pingroom.io/blog/woocommerce-new-order-push-notifications-cover.webp
- Cover description: Two PingRoom avatars and Pingo watch one plain order token pass through a brass verification gate into a shared tray.

To send a WooCommerce new-order alert to a shared group of phones, create an **Order created** webhook in WooCommerce and point it at a small HTTPS relay. The relay should verify WooCommerce's signature, accept only `order.created`, discard the order payload, and send fixed copy to a PingRoom incoming webhook.

This guide sends **New WooCommerce order — An order was created. Check WooCommerce before acting.** It does not put a customer name, address, order number, total, payment method, product, or note on the lock screen.

`order.created` means the order record exists. It does **not** prove that payment cleared or that fulfillment should begin. WooCommerce remains the system of record. This is a webhook recipe you configure, not a native WooCommerce integration.

## Use Woo's own alert when it already fits

WooCommerce has a supported mobile app with [new-order push notifications](https://woocommerce.com/document/woo-mobile-notifications/). That is the simpler choice when the people who need the alert already manage the store through Woo Mobile. Current setup and version requirements apply, and Woo says some notification preferences are still rolling out.

For multi-recipient email, WooCommerce also offers an [Advanced Notifications extension](https://woocommerce.com/document/advanced-notifications/) that can route purchase and stock emails to selected recipients.

Use a PingRoom relay for the narrower case where a small, opt-in team needs one shared short-form signal, while order review and action stay in WooCommerce.

| Need                                                | Better first choice                      |
| --------------------------------------------------- | ---------------------------------------- |
| One store manager wants native order alerts         | Woo Mobile                               |
| Selected people need detailed purchase emails       | WooCommerce Advanced Notifications       |
| A small team needs one shared privacy-minimal alert | Signed webhook relay to a PingRoom Room  |
| Staff need to inspect, refund, or fulfill the order | WooCommerce, not the notification itself |

## Prepare the two webhook ends

The destination Room's owner needs active **PingRoom Pro** for incoming webhooks. Follow the [incoming-webhook setup](/blog/webhook-to-push-notification): open the Room's **Settings → Connections → From another app**, add a webhook named **WooCommerce**, choose **Custom message**, and copy its URL.

Treat the complete PingRoom URL as a credential. Store it as `PINGROOM_WEBHOOK_URL` in server-side configuration. Do not put it in WordPress code, a public repository, screenshots, or logs.

Also configure:

- `WOO_WEBHOOK_SECRET`: a new random secret used only for this WooCommerce webhook;
- `WOO_STORE_KEY`: a stable private label such as `north-shop`, not a customer-facing store name or domain.

In WooCommerce, open **WooCommerce → Settings → Advanced → Webhooks** and add this webhook:

| Field        | Value                              |
| ------------ | ---------------------------------- |
| Name         | `PingRoom new-order alert`         |
| Status       | `Active`                           |
| Topic        | `Order created`                    |
| Delivery URL | Your relay's public HTTPS endpoint |
| Secret       | The value in `WOO_WEBHOOK_SECRET`  |

WooCommerce sends a ping when you first save an active webhook. The relay below verifies and acknowledges that setup ping without sending a phone alert.

## Verify before you forward

WooCommerce signs the exact request bytes with HMAC-SHA256 and sends the Base64 result in `X-WC-Webhook-Signature`. Verify those raw bytes before parsing JSON. Do not place a body parser in front of this handler.

```js
import { createHash, createHmac, timingSafeEqual } from "node:crypto";

const TITLE = "New WooCommerce order";
const MESSAGE = "An order was created. Check WooCommerce before acting.";
const MAX_BODY_BYTES = 1_000_000;
const UPSTREAM_TIMEOUT_MS = 8_000;

function text(status, body) {
  return new Response(status === 204 ? null : body, {
    status,
    headers: { "content-type": "text/plain; charset=utf-8" },
  });
}

function sameText(left, right) {
  const a = Buffer.from(left, "utf8");
  const b = Buffer.from(right, "utf8");
  return a.length === b.length && timingSafeEqual(a, b);
}

function validPingRoomWebhookUrl(value) {
  return /^https:\/\/api\.pingroom\.io\/api\/webhooks\/[^/?#\s]+\/[^/?#\s]+$/.test(
    value,
  );
}

export async function handleWooOrder(request, env = process.env) {
  if (request.method !== "POST") return text(405, "Method not allowed");

  const secret = env.WOO_WEBHOOK_SECRET || "";
  const storeKey = env.WOO_STORE_KEY || "";
  const pingRoomUrl = env.PINGROOM_WEBHOOK_URL || "";
  if (!secret || !storeKey || !validPingRoomWebhookUrl(pingRoomUrl)) {
    return text(500, "Invalid server configuration");
  }

  const rawBody = Buffer.from(await request.arrayBuffer());
  if (!rawBody.length || rawBody.length > MAX_BODY_BYTES) {
    return text(413, "Invalid body size");
  }

  const providedSignature = request.headers.get("x-wc-webhook-signature") || "";
  const expectedSignature = createHmac("sha256", secret)
    .update(rawBody)
    .digest("base64");
  if (!sameText(expectedSignature, providedSignature)) {
    return text(401, "Invalid signature");
  }

  const topic = (request.headers.get("x-wc-webhook-topic") || "").toLowerCase();
  const resource = (
    request.headers.get("x-wc-webhook-resource") || ""
  ).toLowerCase();
  const event = (request.headers.get("x-wc-webhook-event") || "").toLowerCase();

  if (
    topic === "action.woocommerce_webhook_ping" ||
    event === "woocommerce_webhook_ping"
  ) {
    return text(204, "");
  }

  if (
    topic !== "order.created" ||
    resource !== "order" ||
    event !== "created"
  ) {
    return text(202, "Ignored event");
  }

  let order;
  try {
    order = JSON.parse(rawBody.toString("utf8"));
  } catch {
    return text(400, "Invalid JSON");
  }
  if (!Number.isSafeInteger(order?.id) || order.id < 1) {
    return text(400, "Missing order identity");
  }

  const eventKey = createHash("sha256")
    .update(`${storeKey}:order.created:${order.id}`, "utf8")
    .digest("hex");
  const body = JSON.stringify({ title: TITLE, message: MESSAGE });

  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), UPSTREAM_TIMEOUT_MS);
  let upstream;
  try {
    upstream = await fetch(pingRoomUrl, {
      method: "POST",
      headers: {
        "content-type": "application/json",
        "idempotency-key": `woo-order-created-${eventKey}`,
      },
      body,
      signal: controller.signal,
    });
  } catch {
    return text(502, "PingRoom request failed");
  } finally {
    clearTimeout(timer);
  }

  let result;
  try {
    result = await upstream.json();
  } catch {
    return text(502, "Invalid PingRoom response");
  }
  if (!upstream.ok || result?.success !== true) {
    return text(502, "PingRoom rejected the request");
  }

  return Response.json({ accepted: true });
}
```

The code reads `order.id` only to build a stable, opaque duplicate key. It never forwards the order ID or any other order field. Hashing the store key and order ID is not encryption; the privacy boundary comes from sending a fixed body that contains none of the WooCommerce payload.

![Two PingRoom avatars compare one accepted order token with a stopped duplicate while Pingo stands beside a closed delivery ledger.](/blog/woocommerce-new-order-push-notifications-1.webp)

Pingo the Pigeon, PingRoom's fictional CMO, is guarding the stopped duplicate in this example. The actual duplicate rule belongs to the relay and PingRoom's idempotency contract, not to a fictional bird or an inferred order state.

## Keep retries exact

One WooCommerce order gets one key and the same fixed PingRoom body every time. If WooCommerce repeats the delivery, PingRoom can return the first accepted result instead of creating a second Ping. Do not add the current time, order total, customer name, or changing status to the forwarded body.

The relay returns a non-2xx response when PingRoom acceptance cannot be confirmed. That makes the failure visible in WooCommerce's delivery log, but it also needs an operating response: WooCommerce documents automatic disabling after repeated failed deliveries. Check **WooCommerce → Status → Logs**, repair the cause, re-enable the webhook if necessary, and send a controlled test.

PingRoom's incoming webhook normally accepts one new Ping every five seconds. If your store can create several orders inside that window, put events in a durable queue and send a bounded summary instead of turning every order into a phone interruption. The [webhook reference](/documents/webhooks) documents current limits, errors, cooldown behavior, and idempotency.

## Test six boundaries before activation

Use a staging store or signed local fixtures. Do not place a real customer order merely to test a phone alert.

1. Send a correctly signed setup-ping fixture. The relay should return 204 and create no Ping.
2. Send one correctly signed `order.created` fixture. PingRoom should accept exactly one fixed alert.
3. Replay the same signed order. PingRoom should return the first accepted result without a second Ping.
4. Send a correctly signed different order. The relay should use a different idempotency key.
5. Alter the body without recomputing the signature. The relay should return 401 and make no PingRoom request.
6. Make the PingRoom test endpoint reject the request. The relay should return non-2xx so the failure remains visible in WooCommerce logs.

For this article, we imported the exact fenced handler and ran it locally with signed WooCommerce-style fixtures plus a stateful mock of PingRoom's documented contract. The check covered the setup ping, first acceptance, exact replay, a second order, wrong topic, invalid signature, malformed JSON, missing order identity, private-field exclusion, upstream rejection, malformed upstream JSON, and network failure. It made no live WooCommerce, PingRoom, or phone request.

## Treat the Ping as an attention signal

The alert says an order was created. It does not say **paid**, **fraud checked**, **ready to ship**, or **delivered**. Staff should open WooCommerce and follow the store's normal payment, inventory, tax, fraud, fulfillment, and customer-service procedures.

Keep customer names, addresses, email addresses, phone numbers, payment details, product names, order notes, coupon data, and totals out of the shared Ping. If a person needs those details, WooCommerce is the right place to view them with the right access.

A successful PingRoom response means the event was accepted and queued through PingRoom's normal delivery path. What a phone displays still depends on connectivity, operating-system policy, notification permission, Focus settings, and Room mutes.

If this shared-room path fits your team, [open PingRoom](/app/rooms), create a small private fulfillment Room, and test the complete path with a harmless fixture before enabling the WooCommerce webhook.

For a paid-order workflow on another platform, read the separate [Shopify Flow guide](/blog/shopify-order-paid-push-notifications). The event semantics are different, so do not copy its “paid” wording into this WooCommerce `order.created` recipe.

_Illustrations are AI-generated editorial scenes featuring [Pingo the Pigeon](/Pingo), PingRoom's official mascot and fictional Chief Marketing Officer._
