Embed payments in your own app or site.
When you own the customer experience but you do not want to own five different rail integrations. Drop in @meraspay/sdk-js for the browser, call the JSON API from your backend — one PaymentIntent per charge across every live Djiboutian rail.
Two surfaces, one model
Pick the surface that fits your stack — both produce the same PaymentIntent under the hood.
Browser — @meraspay/sdk-js
Load js.meraspay.io/v1/meras.js, mount iframe-hosted Elements for OTP/MSISDN/card, and confirm with the client_secret. PCI SAQ-A scope, no PAN at your origin.
Server — REST
POST /v1/payment_intents from anywhere. Idempotency-Key header, OTP/3DS confirm step, every event mirrored to your webhook. Server SDKs in Node, Go, Python, PHP.
What teams build with Direct Payment
Every backend that needs to charge a customer in Djibouti, without redirecting them off your surface.
In-app checkout
Your mobile app collects the wallet number, your backend creates the PaymentIntent — no MerasPay-hosted page needed. The app handles the confirm step inline.
Recurring billing
Subscriptions trigger renewal charges from your billing service. Pair Direct Payment with the Customers and Subscriptions resources for full lifecycle automation.
Invoice payments
Send an invoice link by SMS or email; when the customer taps, your backend creates the intent for their preferred rail and walks them through confirm.
Marketplace splits
Combine Direct Payment with application_fee + transfer_data to settle vendor and platform amounts atomically inside one charge.
Why merchants choose it over rolling their own
The boring fundamentals — idempotency, retries, ledger, audit — are taken care of so your team can spend its time on product, not on rebuilding payment plumbing.
No hosted page required
You own the customer experience top-to-bottom. The MerasPay surface is purely a JSON API.
Idempotent by contract
Every POST requires an Idempotency-Key. The same key returns the same response for 24 hours — retries can never double-charge.
Confirm-when-ready
Intents stay in requires_action until you call /confirm. Useful for SCA-style flows, OTP collection, or human-in-the-loop approvals.
Per-merchant rail choice
Pick the cheapest rail per customer segment, or fail over rail-by-rail by re-creating the intent. Routing is a property of your business logic, not ours.
Real-time webhooks
payment_intent.created, payment_intent.requires_action, payment_intent.succeeded, payment_intent.payment_failed — signed, retried, observable.
Audit-grade ledger
Every intent posts double-entry rows before it touches a provider. Your reconciler hits one schema, not five.
The four-step flow
- 1
Create the PaymentIntent
POST /v1/payment_intents with amount, currency, payment_method.provider, and the customer identifier (MSISDN, account number, etc.).
- 2
Receive a next_action
The response tells you exactly what to do: collect an OTP, redirect to a 3DS challenge, poll for asynchronous confirmation, or nothing at all if the rail completes synchronously.
- 3
Confirm
POST /v1/payment_intents/:id/confirm with the OTP or returned token. The orchestrator finalises with the provider and writes ledger entries.
- 4
Listen on the webhook
You can already mark the order paid synchronously from the confirm response, but webhooks remain the source of truth for retries, refunds, and disputes.
Real code, real shapes
A typed Node example using the official SDK. The pattern is the same in Go, Python, Java and PHP — the wire format is JSON over HTTPS, and every SDK is generated from the same OpenAPI 3.1 spec.
// Node — single charge against the SabaPay rail.
import MerasPay from '@meraspay/node';
const meras = new MerasPay(process.env.MERASPAY_SECRET_KEY);
const intent = await meras.paymentIntents.create(
{
amount: 25_000, // 25,000 DJF (minor units)
currency: 'DJF',
description: 'Order #4082',
payment_method: {
provider: 'sabapay',
account: '253-77-123456',
},
metadata: { order_id: '4082' },
},
{
idempotencyKey: `order_4082_charge_1`,
apiVersion: '2026-05-01',
},
);
if (intent.status === 'requires_action') {
// Collect the OTP from the customer, then:
const confirmed = await meras.paymentIntents.confirm(intent.id, {
confirmation: { otp: req.body.otp },
});
return confirmed; // status === 'succeeded' if all went well
}Idempotency makes retries free.
The same Idempotency-Key inside 24 hours returns the original response. Your retry loops, your queue redeliveries, and your operator double-clicks are all safe.
# Same idempotency key → exact same response, even on a retry.
$ curl https://api.meraspay.io/v1/payment_intents \
-u sk_live_xxx: \
-H "Idempotency-Key: order_4082_charge_1" \
-d amount=25000 -d currency=DJF \
-d "payment_method[provider]=sabapay" \
-d "payment_method[account]=253-77-123456"
{ "id": "pi_01HZF8AYJX3D8M7QK0E5V4WJ9K", ... } # first call
{ "id": "pi_01HZF8AYJX3D8M7QK0E5V4WJ9K", ... } # retry — byte-identicalFrequently asked
What is the difference between Direct Payment and Hosted Checkout?▾
Direct Payment is a JSON API your backend calls. Hosted Checkout is a MerasPay-hosted page your customer is redirected to. Both end up creating a PaymentIntent — the difference is who owns the UI.
Do you ever attempt to retry a payment automatically?▾
No. We retry our own connection failures (e.g. provider timeout, idempotent retry) but never re-attempt a payment that the customer or rail declined. You retry by creating a new PaymentIntent with a new idempotency key.
Can I tokenise a wallet number for repeated billing?▾
Yes — create a Customer, attach a PaymentMethod, and reference customer + payment_method on subsequent intents. The wallet number is encrypted at rest under the customer's data key.
What about FX conversion?▾
Direct Payment is denominated in a single currency. For cross-currency flows (most often DJF→ETB via SantimPay) you would use Remittance Origination, which locks an FX rate at order creation.
Ready to integrate?
Get a sandbox account in minutes. Production goes live after a brief KYB review.