Developer docs

MaripaPay API Reference

One provider-agnostic REST API for collecting payments. Base URL https://api.maripaypay.com. Version v1.

Overview

MaripaPay sits between your system and payment providers. You call one API; MaripaPay talks to the underlying gateway (Pesapal and others). Test mode uses a built-in simulator — no gateway account required — 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/refund state change.

Authentication

Send your secret key as a bearer token. Keys are environment-scoped: sk_test_… can only touch test data, 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.maripaypay.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 + body returns the stored response; the same key with a different body returns 409.

curl -X POST https://api.maripaypay.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 + 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" }
}

Amounts are always exact integers in minor units, and currency is always explicit. Never send a float.

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
}

Test amounts (last two digits, minor units) drive the simulator: …00 completes, …01 is declined, …02 stays pending, …03 insufficient funds, …11 a provider outage.

Poll GET /api/v1/payments/{id} or — better — rely on webhooks.

Hosted checkout

Create a session and redirect the payer to its url. MaripaPay 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.maripaypay.com/checkout/<token>",
    "qr_url": "https://api.maripaypay.com/qr/checkout/<token>.png",
    "expires_at": "…" }

On completion the payer is sent to success_url; you also receive a payment.completed webhook.

Webhooks

Register an endpoint (dashboard → Webhooks). MaripaPay POSTs JSON events and signs each request:

MaripaPay-Signature: t=1717000000,v1=<hex hmac-sha256>
MaripaPay-Event-Id: evt_…
MaripaPay-Event-Type: payment.completed

Verify 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 ~24h; 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 refund

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

Integrating another system

MaripaPay stays generic. Your system keeps owning its own records (invoices, balances, allocations); MaripaPay owns the payment, provider reference, status, webhooks, refunds and reconciliation. Connect the two with reference and metadata:

  1. Your system computes what a customer owes and creates a payment: POST /payments with reference: "invoice-42" and metadata: { invoice_id, customer_id }.
  2. Return the checkout_url (or a payment link) to the customer.
  3. MaripaPay verifies the provider result and sends a signed payment.completed webhook.
  4. Your 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.
  5. Missed webhook? A reconciliation sweep re-delivers it, or query GET /payments?reference=invoice-42.

A school fees system, an events platform, or a shop all integrate the same way — only the metadata differs.