Integrate Checkout, A to Z.
The simplest way to get paid with MerasPay. You create a session on your server, send the customer to a Meras-hosted page, and they pay with any Djiboutian rail — Waafi, CAC, SabaPay, EAB, or card. No PCI scope, no provider SDKs. This is the whole thing, start to finish.
When to use Hosted Checkout
How it works
Three moving parts: your server (creates the session + listens for the webhook), the customer's browser (redirected to Meras and back), and MerasPay (hosts the payment page + drives the rail).
your server MerasPay customer
─────────── ──────── ────────
1. POST /checkout/sessions ─────────►
◄──────────── { url, id }
2. redirect browser ───────────────────────────────► opens hosted page
picks rail, pays ◄──┘
3. payment_intent.succeeded ◄─── webhook
4. browser returns to success_url ◄──────────────────
5. you verify status, fulfil the orderStep by step
- 1
Get your API keys
Sign up at merchant.merashub.com, then Settings → Keys. You get a secret key per environment:sk_test_…(sandbox) andsk_live_…(live). The secret key is used server-side only — never ship it to the browser. - 2
Create a checkout session (server-side)
CallPOST /v1/checkout/sessionswith the amount, currency, and the two URLs the customer returns to. You get back a hostedurl. - 3
Redirect the customer to the url
Send the browser tosession.url. MerasPay renders the branded page; the customer picks a rail and pays. - 4
Confirm with the webhook (source of truth)
Listen forpayment_intent.succeeded. This is the only reliable signal a payment completed — never trust the redirect alone. - 5
Go live
Swapsk_test_→sk_live_. Same code, real money.
1 · Create the session
Amounts are in the currency's major unit — for DJF, a 50 DJF charge is "amount": 50.
curl -X POST https://api.merashub.com/v1/checkout/sessions \
-H "Authorization: Bearer sk_test_..." \
-H "Idempotency-Key: order_42" \
-H "Content-Type: application/json" \
-d '{
"amount": 50,
"currency": "DJF",
"allowed_methods": ["waafi", "cac", "sabapay", "eab"],
"success_url": "https://yoursite.example/orders/42/done",
"cancel_url": "https://yoursite.example/orders/42",
"description": "Order #42 — 2x espresso"
}'{
"id": "cs_01HZ8AYJX3D8M7QK0E5V4WJ9K",
"url": "https://checkout.merashub.com/c/cs_01HZ8AYJX3D8M7QK0E5V4WJ9K",
"status": "open",
"amount_minor": 50,
"currency": "DJF",
"expires_at": "2026-06-13T19:15:30Z"
}| Name | Type | Description |
|---|---|---|
amountrequired | integer | Amount to charge, in major units (DJF is whole francs). |
currencyrequired | ISO 4217 | "DJF" today; "USD"/"ETB" on the corridor. |
success_urlrequired | url | Customer returns here on success. We append ?session_id=cs_… |
cancel_urlrequired | url | Customer returns here if they back out. |
allowed_methods | array | Restrict the rails shown, e.g. ["waafi","cac"]. Omit to offer all your live rails. |
description | string | Order summary shown on the payment page. |
metadata | map | Free-form key/value — round-trips on the webhook. |
Always send an Idempotency-Key
2 · Send the customer to the page
Redirect the browser to session.url. That's it — MerasPay handles the rail picker, the OTP / push-approval / hosted-bank step, retries, and the success screen.
app.post('/checkout', async (req, res) => {
const r = await fetch('https://api.merashub.com/v1/checkout/sessions', {
method: 'POST',
headers: {
'Authorization': 'Bearer ' + process.env.MERAS_SECRET_KEY,
'Idempotency-Key': 'order_' + req.body.orderId,
'Content-Type': 'application/json',
},
body: JSON.stringify({
amount: 50,
currency: 'DJF',
success_url: 'https://yoursite.example/orders/42/done',
cancel_url: 'https://yoursite.example/orders/42',
description: 'Order #42',
}),
});
const session = await r.json();
res.redirect(303, session.url); // ← off to the hosted page
});3 · Confirm with a webhook
The redirect back to success_urlmeans "the customer finished" — not "the money settled." Treat the payment_intent.succeededwebhook as the source of truth, and verify the HMAC signature so you know it's really us.
import crypto from 'crypto';
// We POST each event with these headers:
// MerasPay-Event-Id: evt_...
// MerasPay-Event-Type: payment_intent.succeeded
// MerasPay-Signature: t=<unix>,v1=<hex> (HMAC-SHA256)
// The signature is computed over "{t}.{event_id}.{raw body}".
app.post('/webhooks/meras', express.raw({ type: '*/*' }), (req, res) => {
const header = req.headers['meraspay-signature'] || ''; // keys are lowercased
const eventId = req.headers['meraspay-event-id'] || '';
const parts = Object.fromEntries(header.split(',').map((kv) => kv.split('=')));
const signed = parts.t + '.' + eventId + '.' + req.body; // body = raw bytes
const expected = crypto
.createHmac('sha256', process.env.MERAS_WEBHOOK_SECRET)
.update(signed)
.digest('hex');
if (!parts.v1 ||
!crypto.timingSafeEqual(Buffer.from(parts.v1), Buffer.from(expected))) {
return res.status(400).end();
}
const event = JSON.parse(req.body); // { id, type, data, ... }
if (event.type === 'payment_intent.succeeded') {
fulfilOrder(event.data.id); // event.data is the payment intent
}
res.json({ received: true }); // 2xx within 5s or we retry
});Idempotent handlers
MerasPay-Event-Id header (also in event.id) before acting. Any metadata you set on the session round-trips on event.data.metadata.4 · The return page
On success_url we append ?session_id=cs_…. Fetch the session to show the right state immediately (the webhook may still be in flight).
curl https://api.merashub.com/v1/checkout/sessions/cs_01HZ8AYJX3D8M7QK0E5V4WJ9K \
-H "Authorization: Bearer sk_test_..."
# → { "status": "complete", "amount_minor": 50, "currency": "DJF", ... }Test it in sandbox
| Name | Type | Description |
|---|---|---|
Wallet OTP | CAC / SabaPay | On the sandbox OTP screen, enter 123456 to succeed. Any other code is declined. |
Phone | any | Use any Djibouti number, e.g. +25377000000. No real SMS is sent in sandbox. |
Card | CNP | Use 4111 1111 1111 1111, any future expiry, CVV 123. |
Waafi | HPP | Sandbox auto-approves the hosted Waafi page after a short delay. |
Go-live checklist
- ✓KYC approved in the merchant portal (Settings → Business).
- ✓At least one live provider connected (Settings → Providers).
- ✓Webhook endpoint registered + signature verification working.
- ✓Swapped sk_test_ for sk_live_ in your server env.
- ✓success_url / cancel_url point at production routes.