# Send Square Completed-Payment Alerts to Your Team's Phones

> Validate Square's payment webhook, alert once per completed payment, and keep buyer and payment details out of the shared Ping.

- Published: 2026-10-31
- Author: PingRoom
- Category: Developers
- Topics: Square webhook, Square payment alert, webhook push notification, team payment notification
- Canonical: https://pingroom.io/blog/square-payment-completed-push-notifications
- Cover image: https://pingroom.io/blog/square-payment-completed-push-notifications-cover.webp
- Cover description: Two PingRoom avatars guide a plain clay token through a brass verification hoop while Pingo watches beside a wooden rail.

To send one Square completed-payment signal to a group of phones, subscribe a Square application to `payment.updated`, validate each delivery against the exact notification URL and raw body, and atomically save one job per payment the first time you observe `COMPLETED`. A worker can then send fixed, privacy-minimal copy to a PingRoom Room.

This is a webhook recipe, not a native Square integration. Square remains the source of truth for the payment, refund, dispute, settlement, and account record. PingRoom carries one selected operational signal to the people who need it.

## Start with Square's own notifications

Square already sends account and transactional notifications to account owners and full-access team members. If that reaches the right person with the right context, keep it. A relay adds infrastructure and a second place to manage access.

Use the relay when a bounded team needs one shared signal for one seller and location, without copying a Payment object onto several lock screens.

| Need                                                                                | Better starting point                                 |
| ----------------------------------------------------------------------------------- | ----------------------------------------------------- |
| One account owner or full-access team member needs Square's standard notifications  | Use Square's notification settings                    |
| A small operating team needs one shared “payment completed” signal for one location | Use the verified relay below                          |
| The alert needs buyer, amount, currency, receipt, order, or note detail             | Keep that detail in Square and open the payment there |
| The event changes fulfillment, inventory, access, or accounting state               | Use your application's durable payment workflow       |

Square says transactional notification emails cannot be redirected to alternate addresses, while its push and email options vary by notification type. That limitation can make a narrow shared signal useful, but it does not make PingRoom the payment record.

## Prepare one Room and one Square subscription

You need:

- a private PingRoom Room whose members should receive this operational signal;
- PingRoom Pro for the Room's incoming webhook;
- a public HTTPS receiver backed by an atomic durable store or queue;
- one Square application and webhook subscription;
- the subscription's exact notification URL and signature key; and
- the seller merchant ID and one location ID the receiver should accept.

In PingRoom, create or choose the Room, then add an incoming webhook under **Settings → Connections → From another app**. Store the complete URL as a server-side secret. Its final path segment is a credential; do not put it in browser code, source control, screenshots, or logs. The [incoming-webhook walkthrough](/blog/webhook-to-push-notification) covers the Room-side setup.

In the Square Developer Console, open the intended application, add the receiver as a notification URL, and subscribe to `payment.updated`. Copy the signature key for that exact subscription. Record the URL as one fixed configuration value, including its path and query string if it has one. Do not reconstruct it later from request headers at a proxy.

Square payment webhooks can originate from its Point of Sale app, Terminal, invoices, and other Square products, not only payments created by your application. The merchant and location checks below are therefore part of the boundary, not optional labels.

## Validate the raw body before reading the event

Square signs the exact notification URL followed by the raw request body with HMAC-SHA-256. The result arrives in `x-square-hmacsha256-signature`. Capture the raw bytes before a framework parses, normalizes, or re-serializes JSON, then compare the signature with a constant-time crypto function.

This framework-neutral receiver expects the host framework to supply the raw body and an atomic `putIfAbsent` operation:

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

const MAX_BODY_BYTES = 128 * 1024;

function verifySquare({ rawBody, signature, signatureKey, notificationUrl }) {
  const expected = createHmac("sha256", signatureKey)
    .update(notificationUrl, "utf8")
    .update(rawBody)
    .digest();
  const received = Buffer.from(signature ?? "", "base64");
  return (
    received.length === expected.length && timingSafeEqual(received, expected)
  );
}

export async function receiveCompletedPayment({
  rawBody,
  headers,
  env,
  queue,
}) {
  const bytes = Buffer.isBuffer(rawBody) ? rawBody : Buffer.from(rawBody);
  if (bytes.length > MAX_BODY_BYTES) return { status: 413 };

  const signature = headers.get("x-square-hmacsha256-signature") ?? "";
  if (
    !verifySquare({
      rawBody: bytes,
      signature,
      signatureKey: env.SQUARE_SIGNATURE_KEY,
      notificationUrl: env.SQUARE_NOTIFICATION_URL,
    })
  ) {
    return { status: 403 };
  }

  let event;
  try {
    event = JSON.parse(bytes.toString("utf8"));
  } catch {
    return { status: 400 };
  }

  if (event?.type !== "payment.updated" || event?.data?.type !== "payment") {
    return { status: 200, code: "ignored_event" };
  }
  if (event?.merchant_id !== env.SQUARE_MERCHANT_ID) {
    return { status: 200, code: "ignored_merchant" };
  }

  const payment = event?.data?.object?.payment;
  if (payment?.location_id !== env.SQUARE_LOCATION_ID) {
    return { status: 200, code: "ignored_location" };
  }
  if (payment?.status !== "COMPLETED") {
    return { status: 200, code: "ignored_status" };
  }
  if (event?.data?.id !== payment?.id) return { status: 400 };

  const eventKey = `square:payment-completed:${payment.id}`;
  try {
    const inserted = await queue.putIfAbsent(eventKey, { eventKey });
    return { status: 200, code: inserted ? "queued" : "duplicate" };
  } catch {
    return { status: 503, code: "queue_unavailable" };
  }
}
```

Validate the configured notification URL at startup as HTTPS, without credentials or a fragment. Treat a missing signature, invalid signature, mismatched payment identity, or storage failure as unfinished work rather than silently acknowledging it.

## Alert once per completed payment

`payment.updated` does not mean “a payment just completed.” Square can emit it when any Payment field changes, including authorization, cancellation, completion, or a later addition such as processing fees. Delivery order is not guaranteed, and one event can arrive more than once.

The useful idempotency boundary is therefore the payment, not only the webhook event. The key `square:payment-completed:<payment-id>` sends the first verified completed state your receiver observes and suppresses later completed updates for the same payment. It does not claim to reconstruct the full transition history.

Keep refund and dispute signals separate. A completed-payment Ping is not undone by deleting it or sending an opposite-looking update. If a team needs those states, give each one its own verified event, meaning, response policy, and Room decision.

![Two PingRoom avatars keep a deep detail drawer closed while placing one plain signal token in a shallow tray, with Pingo watching a duplicate remain behind a divider.](/blog/square-payment-completed-push-notifications-1.webp)

Pingo, PingRoom's fictional CMO, has one restrained job in this illustrated example: watch the duplicate stay behind the divider. He cannot inspect Square data, access the Room, or send a Ping. The human team chooses the signal and remains responsible for its meaning.

## Send fixed copy from a worker

The durable job contains only its derived key. The worker builds the Ping from reviewed constants rather than from the Payment object:

```js
export async function sendSquareSignal({ job, webhookUrl, fetchImpl = fetch }) {
  const body = JSON.stringify({
    title: "Square payment completed",
    message: "Open Square to review the payment",
    correlation_id: job.eventKey,
  });

  let response;
  try {
    response = await fetchImpl(webhookUrl, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "Idempotency-Key": job.eventKey,
      },
      body,
    });
  } catch {
    return { outcome: "retry" };
  }

  if (response.status === 429 || response.status >= 500) {
    return { outcome: "retry" };
  }
  if (!response.ok) return { outcome: "configuration_failure" };

  const result = await response.json().catch(() => null);
  return result?.success === true
    ? { outcome: "delivered" }
    : { outcome: "retry" };
}
```

Keep the PingRoom request body and idempotency key unchanged across retries. Store the destination only as an HTTPS server-side secret. A 429 or server error belongs on the retry path; another 4xx usually needs a person to fix the configuration.

The queue's atomic insert prevents repeated Square deliveries and later updates for the same completed Payment from creating several jobs. PingRoom's idempotency handling lets the worker retry one request without intentionally creating another Ping. That is replay-safe processing, not an exactly-once guarantee.

## Test the boundaries

Use Square's sandbox, a test Room, and non-sensitive data.

1. Complete one sandbox payment for the configured seller and location. Confirm the signature passes, the queue stores only the derived payment key, and the Room receives the fixed two-line Ping.
2. Replay the same webhook event. Confirm the receiver reports `duplicate` and keeps one job.
3. Send another correctly signed `payment.updated` event for the same already completed Payment. Confirm it is also a duplicate.
4. Change one raw-body byte or the configured notification URL. Confirm signature validation fails and nothing is stored.
5. Try an authorized, pending, canceled, or failed payment, a different merchant, and a different location. Confirm each valid but irrelevant event is acknowledged and ignored.
6. Make the durable store unavailable. Confirm the receiver returns non-2xx so Square can retry for its documented retry window.
7. Make the PingRoom call return 429, 500, and a network error. Confirm the worker retries with the same body and key. Treat another 4xx as a configuration failure for human review.

Our recorded check covers these paths with Square-shaped fixtures, local HMAC generation, a stateful in-memory queue, and a mocked PingRoom receiver. It also asserts that buyer name, email, amount, currency, order ID, receipt number, and note never enter the stored job or outbound Ping. No live Square seller, live PingRoom Room, or physical phone was used, so the harmless end-to-end sandbox test remains part of your rollout.

## Read the signal conservatively

The Ping means only **the relay validated a Square event for the configured seller and location, observed that Payment as completed, and queued the fixed signal for PingRoom**. Open Square to review the payment and its current state. Do not treat the Ping as proof of settlement, fulfillment, inventory movement, accounting completion, or staff acknowledgement.

Square documents the [Payments API webhook events](https://developer.squareup.com/docs/payments-api/webhooks), the [raw-body signature validation input](https://developer.squareup.com/docs/webhooks/step3validate), and the [duplicate, ordering, acknowledgement, and retry behavior](https://developer.squareup.com/docs/webhooks/overview). Its [notification settings guide](https://squareup.com/help/us/en/article/3843-email-notifications-from-square) explains when the native path may already be enough.

Review PingRoom's [complete webhook contract](/documents/webhooks) for current fields, errors, cooldowns, and acknowledgements. The [incoming webhook feature page](/features/incoming-webhook) describes the Room connection without presenting it as Square-native.

When the seven tests pass, [open PingRoom](/app/rooms), choose the private destination Room, and activate only the completed-payment signal its members are prepared to act on.

_The two editorial illustrations were created with AI using approved PingRoom character references and reviewed by the PingRoom team._
