Creating a subscription against a test URL
Create a URL above, then open the Square Developer Dashboard, pick your application, switch to Sandbox, and go to Webhooks → Subscriptions → Add subscription. Paste the URL, choose the API version and the events, and save. Square shows the signature key on the subscription — copy it.
Then press Send Test Event on the subscription. Square delivers a synthetic payload with a real signature, which is enough to get verification working before any money is involved. For a genuine event, take a sandbox payment through the Payments API or the sandbox Point of Sale.
Why the URL is in the signature
Most providers sign the body. Square signs the notification URL followed immediately by the body, with no separator:
https://trywebhook.com/w/your-bin-id{"merchant_id":"MLFAKEMERCHANT01","type":"payment.updated",...}
HMAC-SHA256 that with the signature key and base64-encode it.
The design intent is to bind a delivery to the endpoint it was addressed to, so a payload captured from one subscription cannot be replayed into another. The practical consequence is that verification depends on a value your server does not receive in the request. Reconstructing the URL from the incoming request is the wrong move — behind a proxy it will differ in scheme or host, and the digest will fail for reasons the body gives no hint about.
const SQUARE_NOTIFICATION_URL = 'https://api.example.com/webhooks/square'; // exactly as configured
const signed = SQUARE_NOTIFICATION_URL + rawBody;
Hard-code it, or read it from configuration alongside the signature key. Those two values belong together.
Configuring the Custom HMAC tab
The settings table above gives the exact values. The only field you have to change is the signed payload: replace YOUR-BIN-ID with the id in your own URL. Everything before {body} is treated as a literal, so what you type must be the URL Square has stored, byte for byte.
Reading the payload
{
"merchant_id": "MLFAKEMERCHANT01",
"type": "payment.updated",
"event_id": "00000000-1111-2222-3333-444444444444",
"created_at": "2026-06-15T10:04:00.000Z",
"data": { "type": "payment", "id": "FAKEPAYMENTID0000000000", "object": { "payment": { ... } } }
}
Route on type. Then note the double nesting: data.type names the resource, and data.object is an envelope with that name as its key. So the payment is at data.object.payment and an order would be at data.object.order.
merchant_id matters if your application is used by more than one Square seller — it is the only thing in the payload identifying whose account this came from. event_id is your idempotency key.
Statuses, not events
Square’s event names are coarse and its status fields are precise. A single card payment walks through several states and emits payment.updated at each:
payment.updated → status: APPROVED (authorised, money not taken)
payment.updated → status: COMPLETED (captured)
Anything that provisions on the event name will run twice. The rule that holds across Square’s whole event catalogue: the event tells you something changed, and the status field inside tells you what it changed to. Only the second one is a business decision.