Signed event delivery you can build on.
Most payment platforms treat webhooks as an afterthought. MerasPay treats them as a first-class product: HMAC-SHA256 signatures, exponential retry up to 72 hours, full delivery history, and an inspector that lets your engineers replay events without leaving the dashboard.
At a glance
- Signature
- HMAC-SHA256
- Retry attempts
- Up to 8
- Retry window
- 72h
- Event types
- 60+
Everything you would expect from production-grade webhooks
The defaults are right for a serious integration. The escape hatches are there when you need them.
HMAC-SHA256 signing
Every delivery is signed over {timestamp}.{event_id}.{body} with a per-endpoint secret. Reject deliveries with mismatched signatures in five lines of code on your server.
Exponential retry
Failed deliveries retry on a schedule: 1m, 5m, 15m, 1h, 6h, 24h, 48h, 72h. Configurable per endpoint. After the final attempt the event is parked for manual replay.
Event subscription filters
Subscribe each endpoint to a subset of event types — payment_intent.*, payout.*, dispute.*, subscription.* — instead of firehosing every event to every endpoint.
Idempotent receivers
Every event carries an event_id and a timestamp. Repeat deliveries are common (retries) — your handler should be idempotent on event_id, and we make that easy with stable ids.
Delivery history + replay
Every delivery attempt is stored with HTTP status, response body, latency, and signature. Replay any past event from the dashboard or via /v1/events/:id/replay.
Local testing
Use the sandbox simulator + ngrok to receive signed deliveries on localhost. Or use the webhook inspector to watch deliveries land without leaving the dashboard.
The lifecycle
Four steps. The verification snippet on your end is shorter than this paragraph.
- 1
Register an endpoint
POST /v1/webhook_endpoints with the URL and the event types you care about. You receive an endpoint secret — store it in your secret manager.
- 2
We sign every delivery
Each POST carries MerasPay-Signature: t=<unix>,v1=<hex_hmac>. The signature payload is {t}.{event.id}.{raw_body}.
- 3
You verify in five lines
Recompute the HMAC with your endpoint secret and compare in constant time. Reject anything that does not match. Reject anything older than five minutes.
- 4
You acknowledge with 2xx
Any 2xx response within 30 seconds counts as success. Anything else triggers the next retry slot. The delivery row records every attempt for the audit trail.
Register an endpoint
Per-endpoint secret. Per-endpoint event filter. Per-endpoint retry policy. Three things most platforms make you bolt on as middleware are built in.
# Register an endpoint that wants payment + dispute events only.
$ curl -X POST https://api.meraspay.io/v1/webhook_endpoints \
-u sk_live_xxx: \
-d url=https://ops.example.io/webhooks/meraspay \
-d "enabled_events[]=payment_intent.*" \
-d "enabled_events[]=dispute.*"
{
"id": "wh_01HZF...",
"url": "https://ops.example.io/webhooks/meraspay",
"secret": "whsec_4yV2…", # store this; will not be returned again
"enabled_events": ["payment_intent.*", "dispute.*"]
}Verify a delivery
Constant-time HMAC compare + age check. Five-minute window blocks replay attacks.
// Verify a MerasPay webhook signature in Node.
import crypto from 'crypto';
export function verify(req, secret) {
const header = req.headers['meraspay-signature'];
const [tPart, sigPart] = header.split(',');
const t = tPart.replace('t=', '');
const sig = sigPart.replace('v1=', '');
// Reject anything older than 5 minutes to block replay attacks.
const age = Date.now() / 1000 - Number(t);
if (age > 300) throw new Error('webhook: stale');
const payload = `${t}.${JSON.parse(req.rawBody).id}.${req.rawBody}`;
const expected = crypto.createHmac('sha256', secret).update(payload).digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) {
throw new Error('webhook: signature mismatch');
}
}Sample of the event catalogue
The full list is in the OpenAPI spec. Below are the events most integrations subscribe to first.
| Type | Fires when |
|---|---|
| payment_intent.created | A PaymentIntent was created and is pending or requires_action. |
| payment_intent.requires_action | Customer-side action needed (OTP, redirect, etc.) before the intent can confirm. |
| payment_intent.succeeded | The payment was confirmed and ledger entries posted. |
| payment_intent.payment_failed | The rail or customer declined; the intent is in a terminal failed state. |
| payout.created | A payout was queued. |
| payout.paid | Funds reached the recipient successfully. |
| payout.failed | The payout failed and funds returned to the merchant balance. |
| refund.created / refund.updated | A refund moved through its state machine. |
| dispute.created / dispute.updated / dispute.closed | A dispute opened, evidence was submitted, or it resolved. |
| invoice.payment_succeeded / invoice.payment_failed | Recurring billing collection results. |
| checkout_session.completed / checkout_session.expired | Hosted Checkout session lifecycle. |
| remittance_order.* | Origination or termination lifecycle transitions. |
Frequently asked
What if my endpoint is down for hours?▾
Deliveries retry on an exponential schedule for up to 72 hours. After that the delivery is parked but the event remains stored — replay any time from the dashboard or via /v1/events/:id/replay.
How do I rotate an endpoint secret?▾
POST /v1/webhook_endpoints/:id/rotate. We surface both old and new secrets for a configurable overlap window (default 24 hours) so you can roll secrets without dropping deliveries.
Do you deliver events in order?▾
Best effort. Most events arrive in order, but retries break ordering. Design your handler to be order-independent on a single resource — re-read the resource from /v1 if you need the latest state.
How do I test webhooks locally?▾
Two options. Register an ngrok URL as the endpoint URL. Or use the webhook inspector in the merchant portal — it captures every delivery for any endpoint and lets you replay events to localhost via the CLI.
Can I get inbound webhooks from MerasPay (e.g. when a customer pays)?▾
Yes — that is exactly what this product is. The events listed above include payment_intent.succeeded, checkout_session.completed, qr_payment.succeeded, etc. Subscribe an endpoint to whichever events your downstream systems need.
Ready to integrate?
Get a sandbox account in minutes. Production goes live after a brief KYB review.