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. The signed input is the notification URL followed by the raw body, in that order (confirmed by testing a live delivery), so the same payload sent to a different URL, or even the same URL with a trailing slash, won't validate. Square's docs don't spell the construction out, so use the official SDK helper, which takes the URL and body as separate parameters rather than hand-rolling a string. Square also still delivers a legacy x-square-signature (SHA-1) header over the same input, verify the SHA-256 one.
Square webhook features
| Feature | Details |
|---|---|
| Configuration | Developer dashboard > your application > Webhooks (per subscription) |
| Signature header | x-square-hmacsha256-signature (a legacy x-square-signature SHA-1 is also sent) |
| Signature scheme | Base64 HMAC-SHA256 over the notification URL + raw body, in that order (confirmed); 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}.
See Square webhook payloads in action. Inspect and replay sample Square webhook payloads in the Hookdeck Console — no account or setup required.
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. The Signature Key is short and specific to the subscription, it's not your OAuth access token (an EAAA... value), a common mix-up that fails every verification. Each subscription has its own key, so don't reuse one key across subscriptions.
Securing Square webhooks
The signed content is the notification URL followed by the raw body, and the URL must match byte-for-byte. Use the official SDK helper, which takes the signature key, the notification URL, and the raw body as separate parameters, so you don't reconstruct the string yourself. Verify against the raw body before parsing.
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:
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)
Make Square webhooks production-ready. Hookdeck Event Gateway verifies the Square signature, deduplicates, and durably queues every event.
Square webhook limitations and pain points
The signed input is the URL followed by the body
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 looks like guesswork. Testing a live delivery confirms it: base64 HMAC-SHA256 over the notification URL immediately followed by the raw body.
Why It Happens: All of Square's samples delegate to an SDK helper that takes the URL and body as separate parameters, so the concatenation is never written out.
Workarounds:
- Use the SDK helper (
WebhooksHelper.verifySignature/is_valid_webhook_event_signature); if you hand-roll, concatenatenotificationUrl + rawBodyin that order.
How Hookdeck Can Help: Hookdeck verifies Square deliveries at the edge, so your app doesn't depend on reconstructing the input.
The signature depends on the exact URL
The Problem: Because the notification URL is signed, the URL must match byte-for-byte. Testing confirms a trailing slash or an http vs https difference breaks verification, so the same payload delivered to a slightly different URL won't validate.
Why It Happens: Square binds the destination URL, exactly as registered, into the signature.
Workarounds:
- Pass the exact notification URL you registered to the SDK helper, and keep it stable (no trailing-slash or scheme drift).
How Hookdeck Can Help: Hookdeck gives you one stable endpoint and verifies before forwarding, so URL sensitivity isn't your app's problem.
Two signature headers arrive
The Problem: Square sends both x-square-hmacsha256-signature (SHA-256) and a legacy x-square-signature (SHA-1), each a base64 HMAC over the same notification URL plus raw body. Verifying the wrong one, or assuming only one exists, causes confusion.
Why It Happens: Square still delivers the deprecated SHA-1 header alongside the current SHA-256 one.
Workarounds:
- Verify
x-square-hmacsha256-signature(SHA-256); treatx-square-signatureas deprecated.
How Hookdeck Can Help: Hookdeck verifies the current scheme at the edge, so your app isn't tempted by the legacy header.
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.
Conclusion
Square webhooks are verified with an x-square-hmacsha256-signature base64 HMAC-SHA256 over the notification URL followed by the raw body (confirmed by testing), and the URL must match byte-for-byte. Use the official SDK helper with the URL and body as separate parameters, verify with each subscription's own Signature Key (not an access token), dispatch on the dotted type, and recover expired retries via the Events API.
Hookdeck 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 for free and handle Square webhooks reliably in minutes.