Webhooks

Developers · Hashry Pay

Webhooks

Hashry sends a signed POST to your webhook URL when a payment changes. The webhook route on each stack page already handles it.

Read this page to review those checks, or to write your own in another language. To get webhooks on your own machine, see Test and go live.

The signature

Hashry signs these bytes with your webhook signing secret:

What is signed
POST\n<path of your webhook URL>\n<X-Hashry-Timestamp>\n<raw body bytes>

signature = lowercase hex of HMAC-SHA256(webhook signing secret, the bytes above)
  • Newlines are LF (\n). The method is always POST.
  • The path is the URL path only, with no query string. For https://shop.example.com/api/webhooks/hashry?x=1 it is /api/webhooks/hashry.
  • Sign the raw body bytes, exactly as received. Never parse the JSON and turn it back into text: the bytes change and the check fails.
HeaderMeaning
X-Hashry-TimestampWhen Hashry sent it, in seconds since 1970. Part of the signed bytes.
X-Hashry-SignatureThe signature with the current secret.
X-Hashry-Signature-PreviousOnly while the secret is being rotated: the signature with the old secret.
X-Hashry-Event-IdThe event ID. The signed body has it too, as event_id. They must match.

How to handle one

Delivery is at least once. The same event can come twice, or out of order. These steps make that safe.

  1. Read the raw body bytes.
  2. Refuse a timestamp more than 300 seconds old, or more than 300 seconds ahead.
  3. Work out the signature with each secret you hold. Compare it with each signature header in constant time (crypto.timingSafeEqual, hmac.compare_digest). If nothing matches, answer 400.
  4. Only now parse the JSON. Check that event_id matches X-Hashry-Event-Id.
  5. If you already handled this event ID, answer 200 and stop.
  6. Treat the event as a cue, not as the truth. Read the payment with GET /v1/channel/payment-intents/{uid}. Then update the order with the usual checks: status, amount, currency and reference.
  7. Save the event ID and the order change in one database transaction. Answer 2xx. Any other answer makes Hashry send the event again.
Don't:

Never start a new payment or a refund because of a webhook. Read the payment and update the order.

Events

  • payment.succeeded
  • payment.failed
  • payment.expired
  • payment.refunded
  • payment.review_required
  • payment.canceled, only when the merchant ticks it in the notification settings.
  • subscription.* events, for subscription billing.

The body's event_type names the event. Here is a delivery. The keys can come in another order.

A payment.succeeded delivery
POST /api/webhooks/hashry
Content-Type: application/json
X-Hashry-Event-Id: evt_00000000000000000000000c0ffee000
X-Hashry-Timestamp: 1760436000
X-Hashry-Signature: 6b8c755f493f3887cd433a869a6a4278cbeca8cfd9de0e77bac0b20ec2144071

{"event_type":"payment.succeeded","payment_intent_uid":"3kTMd9pQxV7bWzR2nLc8Fh","external_reference":"order-1042","status":"succeeded","delivery_method":"redirect","amount":"2500.00","customer_total":"2575.00","currency":"JMD","event_id":"evt_00000000000000000000000c0ffee000"}

Older deliveries may have no event_id in the body. Then use a SHA-256 hash of the raw body as the event ID. The SDKs do this.

Rotating the secret

  1. The merchant rotates the signing secret in the admin. The new secret is shown once.
  2. For a while, Hashry signs with both: the new secret in X-Hashry-Signature, the old one in X-Hashry-Signature-Previous.
  3. Set HASHRY_WEBHOOK_SECRET to the new secret, and HASHRY_WEBHOOK_SECRET_PREVIOUS to the old one. The code on each stack page reads both.
  4. When the overlap ends, remove HASHRY_WEBHOOK_SECRET_PREVIOUS.

Next: test it

Back to top

© 2026 Hashry Solutions Limited. All rights reserved.