Integration guide · ~15 minutes

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

You want the fastest path to live and you're fine redirecting the customer to a Meras page. If instead you need the payment UI to live entirely inside your own app, use the Direct integration guide.

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).

The round triptext
  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 order

Step by step

  1. 1

    Get your API keys

    Sign up at merchant.merashub.com, then Settings → Keys. You get a secret key per environment: sk_test_… (sandbox) and sk_live_… (live). The secret key is used server-side only — never ship it to the browser.
  2. 2

    Create a checkout session (server-side)

    Call POST /v1/checkout/sessions with the amount, currency, and the two URLs the customer returns to. You get back a hosted url.
  3. 3

    Redirect the customer to the url

    Send the browser to session.url. MerasPay renders the branded page; the customer picks a rail and pays.
  4. 4

    Confirm with the webhook (source of truth)

    Listen for payment_intent.succeeded. This is the only reliable signal a payment completed — never trust the redirect alone.
  5. 5

    Go live

    Swap sk_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.

POST /v1/checkout/sessionsbash
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"
  }'
201 Createdjson
{
  "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"
}
NameTypeDescription
amount
required
integerAmount to charge, in major units (DJF is whole francs).
currency
required
ISO 4217"DJF" today; "USD"/"ETB" on the corridor.
success_url
required
urlCustomer returns here on success. We append ?session_id=cs_…
cancel_url
required
urlCustomer returns here if they back out.
allowed_methodsarrayRestrict the rails shown, e.g. ["waafi","cac"]. Omit to offer all your live rails.
descriptionstringOrder summary shown on the payment page.
metadatamapFree-form key/value — round-trips on the webhook.

Always send an Idempotency-Key

Same key within 24h returns the same session instead of creating a duplicate — safe to retry on a dropped connection, never a double-charge.

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.

Node / Expressjavascript
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.

Verify + handle the webhookjavascript
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

We retry until you return 2xx, so the same event can arrive more than once. De-dupe on the 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).

GET /v1/checkout/sessions/{id}bash
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

NameTypeDescription
Wallet OTPCAC / SabaPayOn the sandbox OTP screen, enter 123456 to succeed. Any other code is declined.
PhoneanyUse any Djibouti number, e.g. +25377000000. No real SMS is sent in sandbox.
CardCNPUse 4111 1111 1111 1111, any future expiry, CVV 123.
WaafiHPPSandbox 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.
That's the whole integration