MariPay API Reference
One provider-agnostic REST API for collecting payments. Test mode uses a built-in simulator and never moves real money.
Base URL
https://api.maripay.com
Auth
Bearer sk_test_… / sk_live_…
Version
v1 · test mode included
Overview
MariPay sits between your system and payment providers. You call one API; MariPay talks to the underlying gateway. Test mode uses a built-in simulator, needs no gateway account and never moves real money.
Order
What is owed: amount, currency, your reference.
Payment
An attempt to collect an order through a provider.
Checkout session / payment link
A hosted page where the payer pays.
Webhook event
A signed notification of a payment or refund state change.
Authentication
Send your secret key as a bearer token. Keys are environment-scoped: sk_test_… can only touch test data and sk_live_… only live data. Create and rotate keys in the dashboard (Developers → API keys). Publishable keys (pk_…) are for browser contexts only.
curl https://api.maripay.com/api/v1/account \
-H "Authorization: Bearer sk_test_your_key"Scopes limit each key: orders:create, payments:create, refunds:create, webhooks:manage, reports:read, …
Idempotency
Every unsafe write (POST /orders, /payments, /refunds) requires an Idempotency-Key header: a unique string you generate per logical operation (8–255 chars; a UUID is ideal). Replaying the same key and body returns the stored response; the same key with a different body returns 409.
curl -X POST https://api.maripay.com/api/v1/payments \
-H "Authorization: Bearer sk_test_…" \
-H "Idempotency-Key: 7c1f0e2a-9c4b-4a1d-8f5e-2b6d1c3a4e5f" \
-H "Content-Type: application/json" \
-d '{ "amount": 500000, "currency": "UGX", "reference": "invoice-42" }'Orders
Create an order up front, or let POST /payments create one inline from amount and currency.
POST /api/v1/orders
{
"amount": 500000, // minor units — UGX has no subunit, so this is UGX 500,000
"currency": "UGX",
"description": "Term 1 fees",
"reference": "invoice-42", // your id — unique per merchant
"customer": { "email": "parent@example.com", "name": "A. Parent" },
"metadata": { "student_id": "stu_123", "term_id": "t1" }
}Payments
Create a payment for an order (or inline). The response tells you what happened:
POST /api/v1/payments
{ "order_id": "ord_…" } // or: amount + currency
→ 201
{
"id": "pay_…",
"object": "payment",
"amount": 500000, "currency": "UGX",
"status": "completed", // created | pending | processing | completed | failed | ...
"order_id": "ord_…",
"provider": "simulator",
"checkout_url": null // set when the payer must be redirected
}pending until you answer it. Call POST /api/v1/payments/{id}/simulate with { "outcome": "completed" } (or "failed"), or press Approve / Decline on the collection in the dashboard. An amount ending in 11 simulates a provider outage. Send the payer's country (for example "UG") to choose the provider handling the payment; it must match the phone number.Poll GET /api/v1/payments/{id} or, better, rely on webhooks.
Hosted checkout
Create a session and redirect the payer to its url. MariPay owns the payment page; the browser is never trusted for amount or merchant identity.
POST /api/v1/checkout_sessions
{ "amount": 500000, "currency": "UGX",
"success_url": "https://yoursite.com/thanks",
"cancel_url": "https://yoursite.com/cart" }
→ { "id": "cs_…", "url": "https://pay.maripay.com/checkout/<token>",
"qr_url": "https://api.maripay.com/qr/checkout/<token>.png",
"expires_at": "…" }On completion the payer is sent to success_url, and you also receive a payment.completed webhook.
Payment links & buttons
A reusable link that opens a hosted checkout. Omit amount to let the payer enter it.
POST /api/v1/payment_links
{ "currency": "UGX", "amount": 15000, "description": "Donation" }
→ { "id": "plink_…", "url": "https://pay.maripay.com/p/<token>",
"qr_url": "https://api.maripay.com/qr/link/<token>.png" }Drop-in button. The token is a public capability, not a credential:
<div data-maripay-link="<token>" data-maripay-label="Pay UGX 15,000"></div>
<script src="https://api.maripay.com/embed/button.js" async></script>Webhooks
Register an endpoint (dashboard → Webhooks). MariPay POSTs JSON events and signs each request:
MariPay-Signature: t=1717000000,v1=<hex hmac-sha256>
MariPay-Event-Id: evt_…
MariPay-Event-Type: payment.completedVerify by recomputing HMAC_SHA256(secret, "{t}.{raw body}"), comparing in constant time, and rejecting timestamps older than five minutes.
import crypto from "node:crypto"
function verify(rawBody, header, secret) {
const { t, v1 } = Object.fromEntries(header.split(",").map(p => p.split("=")))
if (Math.abs(Date.now()/1000 - Number(t)) > 300) return false
const expected = crypto.createHmac("sha256", secret).update(t + "." + rawBody).digest("hex")
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1))
}Events: payment.created · pending · processing · completed · failed · cancelled · expired · refunded, refund.created · completed · failed. Delivery retries with exponential backoff for about 24 hours; see the delivery log in the dashboard. Make your handler idempotent.
Refunds
POST /api/v1/refunds
{ "payment_id": "pay_…", "amount": 100000, "reason": "customer request" }
// omit "amount" for a full refundA refund can never exceed the captured amount (enforced in the API and the database). Partial refunds move the payment to partially_refunded; the last one to refunded.
Errors
Errors use a stable shape with a machine code and a request ID for support:
{ "error": { "code": "REFUND_EXCEEDS_PAYMENT",
"message": "Refund of 200000 exceeds the 100000 still refundable on this payment.",
"request_id": "req_…" } }Common codes: INVALID_API_KEY, INSUFFICIENT_SCOPE, IDEMPOTENCY_KEY_REQUIRED, IDEMPOTENCY_CONFLICT, VALIDATION_ERROR, INVALID_PAYMENT_STATE, RATE_LIMITED, PROVIDER_UNAVAILABLE.
Code examples
MariPay is a plain REST API, so any language with an HTTP client works. There is nothing to install. Here is the same payment request in JavaScript, Python and PHP:
const res = await fetch("https://api.maripay.com/api/v1/payments", {
method: "POST",
headers: {
Authorization: "Bearer " + process.env.MARIPAY_SECRET,
"Idempotency-Key": crypto.randomUUID(),
"Content-Type": "application/json",
},
body: JSON.stringify({ amount: 500000, currency: "UGX", reference: "invoice-42" }),
})
const payment = await res.json()import os, uuid, requests
res = requests.post(
"https://api.maripay.com/api/v1/payments",
headers={
"Authorization": "Bearer " + os.environ["MARIPAY_SECRET"],
"Idempotency-Key": str(uuid.uuid4()),
},
json={"amount": 500000, "currency": "UGX", "reference": "invoice-42"},
)
payment = res.json()<?php
$ch = curl_init("https://api.maripay.com/api/v1/payments");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer " . getenv("MARIPAY_SECRET"),
"Idempotency-Key: " . bin2hex(random_bytes(16)),
"Content-Type: application/json",
],
CURLOPT_POSTFIELDS => json_encode(["amount" => 500000, "currency" => "UGX", "reference" => "invoice-42"]),
]);
$payment = json_decode(curl_exec($ch), true);Integrating another system
MariPay stays generic. Your system keeps owning its own records (invoices, balances, allocations); MariPay owns the payment, provider reference, status, webhooks, refunds and reconciliation. Connect the two with reference and metadata:
- 1Your system computes what a customer owes and creates a payment:
POST /paymentswithreference: "invoice-42"andmetadata: { invoice_id, customer_id }. - 2Return the
checkout_url(or a payment link) to the customer. - 3MariPay verifies the provider result and sends a signed
payment.completedwebhook. - 4Your webhook handler verifies the signature, reads
data.object.metadata.invoice_id, marks the invoice paid and updates the balance, idempotently, keyed on the payment ID. - 5Missed a webhook? A reconciliation sweep re-delivers it, or query
GET /payments?reference=invoice-42.