48-hour launch: 40% off with code LAUNCH40, Guide $5.40 / Kit $11.40

Square webhook signature verification failed: fixing x-square-hmacsha256-signature

Square sends an x-square-hmacsha256-signature header. It's an HMAC-SHA256 built from three things: your subscription's signature key, the subscription's notification URL, and the raw request body. The signed string is the URL followed directly by the body, and the result is base64-encoded. Unlike most providers, Square includes the URL, and that's where most failures come from.

Common causes

  1. The URL doesn't match. Use the notification URL exactly as it's configured on the subscription, not the URL your framework reconstructs. Behind a proxy or tunnel your app may see http://, an internal host or a different path. A trailing-slash difference also breaks it. Hard-code the configured URL or load it from config.
  2. Wrong key. Use the subscription's signature key, shown under Webhooks in the Developer Console. Your access token or application secret won't work. Each subscription has its own key, so check you're using the one for the subscription that sent the event (sandbox or production).
  3. Parsed body. Hash the raw body string exactly as received, not re-serialized JSON.
  4. Old header. Some older code checks x-square-signature (HMAC-SHA1). Verify x-square-hmacsha256-signature instead.

Node: the SDK helper

import { WebhooksHelper } from 'square';
app.post('/square', express.raw({ type: 'application/json' }), async (req, res) => {
  const ok = await WebhooksHelper.verifySignature({
    requestBody: req.body.toString('utf8'),
    signatureHeader: req.get('x-square-hmacsha256-signature') || '',
    signatureKey: process.env.SQUARE_SIGNATURE_KEY,
    notificationUrl: process.env.SQUARE_NOTIFICATION_URL,   // exactly as configured
  });
  if (!ok) return res.sendStatus(403);
  res.sendStatus(200);                                       // then process; de-dupe on event_id
});

Manual (Python)

expected = base64.b64encode(hmac.new(key.encode(), (NOTIFICATION_URL + raw_body).encode(), hashlib.sha256).digest()).decode()
ok = hmac.compare_digest(expected, request.headers.get('x-square-hmacsha256-signature', ''))

The guide is Stripe-only. The kit is a self-hosted Cloudflare Worker relay that adds retries, replay and a dead-letter list in front of your endpoint. It verifies Square's x-square-hmacsha256-signature out of the box. Set SCHEME_<ID>=square and your notification URL. It works the same way for Stripe, Lemon Squeezy, Shopify, Paddle Billing, Twilio (status callbacks) and GitHub.

Fix Your Stripe Webhooks guide · Self-Hosted Webhook Relay Kit. Code LAUNCH40 = 40% off until Sun Oct 11, 6:40 AM MT.

After it verifies

Square's docs say the notification URL must use HTTPS and must respond with a 2xx as soon as possible. Notifications can arrive more than once, so de-duplicate on the event_id in the body.

Free download: Webhook debugging cheat sheet.

Fighting webhooks on more than one platform? The Fix Your Stripe Webhooks guide ($9) covers raw-body and signature problems in depth, and the Self-Hosted Webhook Relay Kit ($19) adds retries, replay and a dead-letter list in front of any endpoint.