Receive Square webhooks reliably
Square sends its event notifications to a Railhook source. The source verifies the signature, stores each notification as it arrived and forwards it to your service with retries. Railhook answers Square straight away, so a slow or failing app is no longer a reason for Square to turn your subscription off.
Why Square webhooks need a buffer
Section titled “Why Square webhooks need a buffer”Square expects a 2xx, and treats a response that takes longer than 10 seconds as a failure. It then retries with exponential backoff for up to 24 hours, adding square-retry-number and square-retry-reason headers as it goes. If a subscription fails to return a single 2xx for three weeks, Square warns by email and then disables it. (Square: Webhooks overview)
With Railhook in front:
- Square is answered as soon as the notification is stored, well inside the 10 seconds.
- Your service gets up to 5 attempts per forward, with waits from 1 minute to 1 hour, and what still fails lands in Failed Forwards.
- A retried notification with the same
event_idis answered with the stored copy and not forwarded twice. - Anything stored can be replayed long after Square’s 24 hours are up.
Connect Square
Section titled “Connect Square”You need a Railhook project and an API key. The same steps are available under Incoming in the dashboard.
export RAILHOOK_URL=https://railhook.io # or your own instanceexport RAILHOOK_API_KEY=...export PROJECT_ID=...-
Create the source without a secret. Square shows the signature key only once the subscription exists:
Terminal window curl -X POST "$RAILHOOK_URL/api/v1/projects/$PROJECT_ID/incoming-sources" \-H "X-API-Key: $RAILHOOK_API_KEY" \-H "Content-Type: application/json" \-d '{"name":"Square","providerType":"SQUARE","verificationMode":"NONE"}'Keep the
idand theingressUrlfrom the response. -
Subscribe in the Square Developer Console. Open your application, go to Webhooks → Subscriptions, choose Sandbox or Production at the top of the page (the two keep separate subscriptions), and click Add subscription. Paste the
ingressUrlexactly as the source shows it as the notification URL, and select the event types you want. (Square: Subscribe to event notifications) -
Copy the signature key. Open the subscription’s endpoint details and click Show next to Signature Key. That key is what Square signs with.
-
Give Railhook the key and turn verification on:
Terminal window curl -X PUT "$RAILHOOK_URL/api/v1/projects/$PROJECT_ID/incoming-sources/$SOURCE_ID" \-H "X-API-Key: $RAILHOOK_API_KEY" \-H "Content-Type: application/json" \-d '{"name":"Square","verificationMode":"PROVIDER","hmacSecret":"'"$SQUARE_SIGNATURE_KEY"'"}'nameis required on an update; everything else you leave out keeps its current value. -
Add a destination, the URL of your own service:
Terminal window curl -X POST "$RAILHOOK_URL/api/v1/projects/$PROJECT_ID/incoming-sources/$SOURCE_ID/destinations" \-H "X-API-Key: $RAILHOOK_API_KEY" \-H "Content-Type: application/json" \-d '{"url":"https://api.example.com/webhooks/square","authType":"BEARER","authConfig":"{\"token\":\"...\"}","enabled":true}' -
Send a test event from the subscription’s page in the Developer Console. It appears under Incoming → Received, with one forward per destination.
How the signature is checked
Section titled “How the signature is checked”Square’s x-square-hmacsha256-signature is a base64 HMAC-SHA256 over the notification URL followed immediately by the raw body, with no separator, keyed with the signature key. Railhook computes the same thing and compares it in constant time. A request that does not match is answered 401 and is not stored.
Square’s scheme carries no timestamp, so an unchanged body always produces the same signature. What separates Square’s own retry from a replay is the notification’s event_id, which Square documents as an idempotency value: Railhook stores it, and a second notification carrying one it already has is answered with the stored event instead of being forwarded again.
What your service receives
Section titled “What your service receives”The body exactly as Square sent it, byte for byte, with Square’s own headers passed through, including square-environment, which tells you whether the notification came from Sandbox or Production. x-square-hmacsha256-signature is not forwarded: your service authenticates Railhook through the destination’s credentials and has no use for the signature key. See Destinations.
Develop locally with the CLI
Section titled “Develop locally with the CLI”-
Install the CLI and log in, as in CLI, then open a tunnel to your app’s port:
Terminal window railhook listen 3000 -
Add a second destination to the source with the printed public URL and your route appended, such as
https://<host>/tunnel/<slug>/webhooks/square. It reacheshttp://localhost:3000/webhooks/square. -
Take a payment in the Sandbox, or send a test event, and it arrives in your local app. Disable that destination when you stop the tunnel.
Replay after an outage
Section titled “Replay after an outage”Each replayed notification goes to every destination of the source as a new forward:
curl -X POST "$RAILHOOK_URL/api/v1/projects/$PROJECT_ID/incoming-events/bulk-replay" \ -H "X-API-Key: $RAILHOOK_API_KEY" \ -H "Content-Type: application/json" \ -d '{"sourceId":"'"$SOURCE_ID"'","from":"2026-09-18T10:00:00Z","to":"2026-09-18T14:00:00Z","verified":true}'Replay reaches back as far as events are kept: 7 days on Railhook Cloud, and whatever you configure when self-hosting. Keep your handler idempotent on event_id, since a replay can send notifications your service already processed. See Replay.