Your endpoint receives Square events but WebhooksHelper.verifySignature()
returns false (or your hand-rolled check never matches
x-square-hmacsha256-signature). Square's scheme has one property almost no
other provider has: the signature covers the notification URL itself, then the raw
body. That single fact explains most failed verifications — your code can be
byte-perfect and still fail because the URL string you're verifying with isn't the exact
URL Square signed.
The x-square-hmacsha256-signature header is a base64 HMAC-SHA256
over {notification_url}{raw request body} — concatenated directly,
no separator. The key is your webhook subscription's signature key (shown under
Webhooks in the Developer Console), used as a plain UTF-8 string.
You can prove the shape to yourself with the example values from Square's own docs
(key asdf1234, URL https://example.com/webhook):
printf '%s' 'https://example.com/webhook{"hello":"world"}' \
| openssl dgst -sha256 -hmac 'asdf1234' -binary | base64
# 2kRE5qRU2tR+tBGlDwMEw2avJ7QM4ikPYD/PJ3bd9Og=
If that one-liner matches the header for a real capture (with your real key and URL), your inputs are right. When it doesn't, one of the causes below is in play.
Square signs the notification URL exactly as configured in the subscription. Your verifier must be given that exact string. The classic mismatches:
…/webhook, verifying with
…/webhook/ (or vice versa). One byte, dead signature.NOTIFICATION_URL constant
(or the reverse: updated the constant, forgot the Developer Console).Host/X-Forwarded-* headers behind a load balancer and gets a
different string than what you configured. Don't reconstruct; use the configured string
verbatim.Same rule as Stripe and
GitHub: the HMAC is over the raw bytes Square sent. If your framework parses JSON and you
re-serialize it (JSON.stringify(req.body)), key order, unicode escaping, or
whitespace can change and the signature dies. Capture the raw body before any body-parser
touches it.
Current webhook subscriptions send x-square-hmacsha256-signature
(HMAC-SHA256, base64). Very old Square v1 webhook code and samples verified an
X-Square-Signature header (HMAC-SHA1). If you copied an old snippet, you're
computing the wrong algorithm against the wrong header.
square-retry-number and square-retry-reason headers — if you see
those on a capture, an earlier delivery already failed.created event lands before the updated one.The URL-in-signature property makes Square uniquely awkward to debug with generic HMAC testers — they hash only the body, so they can never validate a Square signature. A Hookden bin can, because the bin knows the exact URL the request hit, which is precisely what Square signed when the subscription points straight at the bin:
Verifying on localhost? Point the subscription at a bin and
relay captures to your machine — the relay re-delivers the
byte-identical body and the x-square-* headers, and your local verifier keeps
using the bin URL as its NOTIFICATION_URL, so the signature still passes even
though the request reached you over the relay instead of a tunnel.
Related: HubSpot signature validation (the other URL-signing scheme), Stripe signature verification failed, Clerk/Svix verification, and testing webhook retry behavior.
No signup needed. Or from your terminal: curl https://hookden.pages.dev/new
← All guides · Signature debugger · Payload examples · Docs · Hookden vs webhook.site