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:
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 alwaysPOST. - The path is the URL path only, with no query string. For
https://shop.example.com/api/webhooks/hashry?x=1it 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.
| Header | Meaning |
|---|---|
X-Hashry-Timestamp | When Hashry sent it, in seconds since 1970. Part of the signed bytes. |
X-Hashry-Signature | The signature with the current secret. |
X-Hashry-Signature-Previous | Only while the secret is being rotated: the signature with the old secret. |
X-Hashry-Event-Id | The 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.
- Read the raw body bytes.
- Refuse a timestamp more than 300 seconds old, or more than 300 seconds ahead.
- 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. - Only now parse the JSON. Check that
event_idmatchesX-Hashry-Event-Id. - If you already handled this event ID, answer 200 and stop.
- 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. - Save the event ID and the order change in one database transaction. Answer 2xx. Any other answer makes Hashry send the event again.
Never start a new payment or a refund because of a webhook. Read the payment and update the order.
Events
payment.succeededpayment.failedpayment.expiredpayment.refundedpayment.review_requiredpayment.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.
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
- The merchant rotates the signing secret in the admin. The new secret is shown once.
- For a while, Hashry signs with both: the new secret in
X-Hashry-Signature, the old one inX-Hashry-Signature-Previous. - Set
HASHRY_WEBHOOK_SECRETto the new secret, andHASHRY_WEBHOOK_SECRET_PREVIOUSto the old one. The code on each stack page reads both. - When the overlap ends, remove
HASHRY_WEBHOOK_SECRET_PREVIOUS.