# Guide to Square Webhooks: Features and Best Practices

Square webhooks notify your application about payments and commerce activity: a payment is created or updated, an order changes, an invoice is paid, a customer record changes. If you're building on Square, webhooks are how you react to these events without polling.

This guide covers how Square webhooks work, the events you'll handle, how to verify the `x-square-hmacsha256-signature`, and the best practices for production.

## What are Square webhooks?

Square webhooks are JSON POSTs delivered to a URL you register per subscription. Each is signed with an `x-square-hmacsha256-signature` header: a base64 (not hex) HMAC-SHA256. Square is one of the few providers that signs the notification URL as well as the raw body, so the same payload delivered to a different URL won't validate. The exact concatenation order isn't stated anywhere in Square's docs, all the samples call an SDK helper that takes the URL and body as separate parameters, so use the SDK rather than hand-rolling an order.

## Square webhook features

| Feature | Details |
| --- | --- |
| Configuration | Developer dashboard > your application > Webhooks (per subscription) |
| Signature header | `x-square-hmacsha256-signature` |
| Signature scheme | Base64 HMAC-SHA256 over the notification URL + raw body (order undocumented; use the SDK) |
| Event field | Top-level `type` (dotted) |
| Retries | 11 attempts, exponential backoff over 24h, then discarded (recover via the Events API) |
| Keys | Each subscription has its own signature key |
| SDK | npm `square` (v40+ rewrite); pip `squareup` |

## Common events

Square reports the event type in the top-level `type` field (dotted):

| Event | Fires when |
| --- | --- |
| `payment.created` / `payment.updated` | A payment is created or updated |
| `order.created` / `order.updated` | An order changes |
| `invoice.created` / `invoice.payment_made` | An invoice is created or paid |
| `customer.created` / `customer.updated` / `customer.deleted` | A customer record changes |
| `refund.created` | A refund is created |

The envelope carries `merchant_id`, `type`, `event_id`, `created_at`, and `data{type, id, object}`.

## Setting up Square webhooks

In the Square Developer dashboard, open your application > Webhooks, add a subscription with your HTTPS notification URL, and choose the events. Copy that subscription's signature key into `SQUARE_WEBHOOK_SIGNATURE_KEY`, and set your exact notification URL as `SQUARE_WEBHOOK_URL`. Each subscription has its own key, so don't reuse one key across subscriptions.

## Securing Square webhooks

Because the signed content includes the notification URL and the exact concatenation order isn't documented, use the official SDK helper, which takes the signature key, the notification URL, and the raw body as separate parameters. Verify against the raw body before parsing.

```javascript
const { WebhooksHelper } = require("square");

const KEY = process.env.SQUARE_WEBHOOK_SIGNATURE_KEY;
const URL = process.env.SQUARE_WEBHOOK_URL; // the exact notification URL for this subscription

app.post("/webhooks/square", express.raw({ type: "application/json" }), async (req, res) => {
  const ok = await WebhooksHelper.verifySignature({
    requestBody: req.body.toString("utf8"),
    signatureHeader: req.headers["x-square-hmacsha256-signature"],
    signatureKey: KEY,
    notificationUrl: URL,
  });
  if (!ok) return res.sendStatus(403);

  res.sendStatus(200); // acknowledge fast
  processQueue.add(JSON.parse(req.body.toString())); // branch on type, async
});

```

The same check in Python, using the SDK helper:

```python
import os
from square.utilities.webhooks_helper import is_valid_webhook_event_signature

KEY = os.environ["SQUARE_WEBHOOK_SIGNATURE_KEY"]
URL = os.environ["SQUARE_WEBHOOK_URL"]

def verify(raw_body: str, signature_header: str) -> bool:
    return is_valid_webhook_event_signature(raw_body, signature_header, KEY, URL)

```

## Square webhook limitations and pain points

### The concatenation order is undocumented

The Problem: The signature covers the notification URL plus the raw body, but Square's docs never state the order or operator, so a from-scratch implementation is guessing.

Why It Happens: All of Square's samples delegate to an SDK helper that takes the URL and body as separate parameters.

Workarounds:

* Use the SDK helper (`WebhooksHelper.verifySignature` / `is_valid_webhook_event_signature`) rather than reconstructing a string yourself.

How Hookdeck Can Help: Hookdeck verifies Square deliveries at the edge, so your app doesn't depend on an undocumented construction.

### The signature depends on the exact URL

The Problem: Because the notification URL is signed, the same payload delivered to a different URL won't validate, and whether the URL must match byte-for-byte (trailing slash, query, scheme) isn't documented.

Why It Happens: Square binds the destination URL into the signature.

Workarounds:

* Pass the exact notification URL you registered to the SDK helper, and keep it stable.

How Hookdeck Can Help: Hookdeck gives you one stable endpoint and verifies before forwarding, so URL sensitivity isn't your app's problem.

### Each subscription has its own key

The Problem: Pointing multiple subscriptions at one listener URL and validating every delivery against a single key fails, because each subscription has its own signature key.

Why It Happens: Square scopes the signature key per subscription.

Workarounds:

* Track which subscription a delivery belongs to and verify with that subscription's key.

How Hookdeck Can Help: Hookdeck can verify each source with its own key, so overlapping subscriptions don't collide.

### Retries expire after 24 hours

The Problem: Square retries 11 times with exponential backoff over 24 hours, then discards the event. A prolonged outage means events are gone from the webhook stream.

Why It Happens: Square caps redelivery at 24 hours.

Workarounds:

* Acknowledge reliably, and recover missed events via the Events API rather than redelivery.

How Hookdeck Can Help: Hookdeck durably queues events and retries on its own schedule, so a downstream outage doesn't drop events at Square's 24-hour cap.

## Best practices

### Verify with the SDK helper over the raw body

Use `WebhooksHelper.verifySignature` / `is_valid_webhook_event_signature`, passing the signature key, exact notification URL, and raw body.

### Use the right key per subscription

Each subscription has its own signature key; don't share one across subscriptions.

### Recover via the Events API

Retries expire after 24 hours, so reconcile missed events through the Events API.

### Acknowledge fast, process asynchronously

Return 2xx quickly and defer work to a queue; dedupe on `event_id`. See [why to process webhooks asynchronously](/webhooks/guides/why-implement-asynchronous-processing-webhooks).

## Conclusion

Square webhooks are verified with an `x-square-hmacsha256-signature` base64 HMAC-SHA256 over the notification URL plus the raw body. Because the concatenation order is undocumented, use the official SDK helper with the URL and body as separate parameters, verify with each subscription's own key, dispatch on the dotted `type`, and recover expired retries via the Events API.

[Hookdeck](https://hookdeck.com) verifies the signature, deduplicates, and durably queues every event at the edge, so your app only ever processes verified, unique events.

[Get started with Hookdeck](https://dashboard.hookdeck.com/signup) for free and handle Square webhooks reliably in minutes.