Skip to content
Developers

Build payments once. Ship everywhere.

A single, predictable API for M-Pesa, mobile money, cards, bank transfers, stablecoins and payouts — with the reliability primitives you'd build yourself, already done.

Overview

Designed to be boring, in the best way.

The primitives that make payments reliable are part of the platform — not left as an exercise.

REST + JSON

Resource-oriented URLs, standard HTTP verbs and JSON bodies. Amounts are integers in minor units — no floating-point surprises.

Idempotency keys

Send an Idempotency-Key header with any POST. Retries return the original response, so a network blip never double-charges.

Signed webhooks

Every event is signed with HMAC-SHA256 in Imaripay-Signature, and retried with exponential backoff for about 45 hours.

Sandbox

Test and live modes are fully isolated with separate keys. Magic numbers let you simulate every outcome deterministically.

Reconciliation built in

Pending payments are continuously re-checked upstream; late confirmations are captured and delivered as normal events.

Consistent errors

Every error has an HTTP status, a stable machine-readable code and a human message you can show or log.

Quickstart

Your first payment in three steps.

Create an account to get sandbox keys instantly. Authenticate with your secret key as a Bearer token — never expose it in a browser or mobile app.

  1. 1

    Create a payment

    Send the amount in minor units, the currency, the method and the customer's phone. M-Pesa and mobile money payments trigger a PIN prompt on the customer's handset.

    cURL
    curl https://imaripay.com/api/v1/payments \
      -H "Authorization: Bearer $IMARIPAY_KEY" \
      -H "Idempotency-Key: order_1042" \
      -d amount=150000 \
      -d currency=KES \
      -d method=mpesa \
      -d phone=254712345678
    Node.js
    const res = await fetch("https://imaripay.com/api/v1/payments", {
      method: "POST",
      headers: {
        Authorization: `Bearer $${process.env.IMARIPAY_KEY}`,
        "Content-Type": "application/json",
        "Idempotency-Key": "order_1042",
      },
      body: JSON.stringify({
        amount: 150000,
        currency: "KES",
        method: "mpesa",
        phone: "254712345678",
      }),
    });
    const payment = await res.json();
    Python
    import os, requests
    
    r = requests.post(
        "https://imaripay.com/api/v1/payments",
        headers={
            "Authorization": f"Bearer {os.environ['IMARIPAY_KEY']}",
            "Idempotency-Key": "order_1042",
        },
        json={
            "amount": 150000,
            "currency": "KES",
            "method": "mpesa",
            "phone": "254712345678",
        },
    )
    payment = r.json()
  2. 2

    Listen for webhooks

    Payments complete asynchronously. Register an endpoint in the dashboard and we'll POST events such as payment.succeeded and payout.paid. Respond 2xx quickly — we retry anything else.

    Event payload
    {
      "id": "evt_0m4xa1c9…",
      "type": "payment.succeeded",
      "data": { "object": { "id": "pay_0m4x9kq2…", "status": "succeeded", … } }
    }
  3. 3

    Verify the signature

    Each request carries a Imaripay-Signature: t=…,v1=… header. Compute an HMAC-SHA256 of `${t}.${rawBody}` with your endpoint secret, compare in constant time, and reject stale timestamps.

    Node.js
    import { createHmac, timingSafeEqual } from "node:crypto";
    
    export 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 = createHmac("sha256", secret)
        .update(`$${t}.$${rawBody}`).digest("hex");
      return v1?.length === expected.length &&
        timingSafeEqual(Buffer.from(v1), Buffer.from(expected));
    }
    Python
    import hmac, hashlib, time
    
    def verify(raw_body: bytes, header: str, secret: str) -> bool:
        parts = dict(p.split("=", 1) for p in header.split(","))
        t, v1 = parts.get("t", "0"), parts.get("v1", "")
        if abs(time.time() - int(t)) > 300:
            return False
        msg = f"{t}.".encode() + raw_body
        expected = hmac.new(secret.encode(), msg, hashlib.sha256).hexdigest()
        return hmac.compare_digest(v1, expected)
Test mode

Simulate every outcome.

In test mode no real money moves. Use these magic values with your sk_test_ key to trigger specific results — and make sure your integration handles them gracefully.

Test valueResult
Phone ending 0000
M-Pesa / mobile money
Fails — insufficient funds
Phone ending 1111
M-Pesa / mobile money
Fails — customer cancelled
Phone ending 2222
M-Pesa / mobile money
Never completes, then expires
Any other phone
M-Pesa / mobile money
Succeeds
“Decline” on the sandbox 3-D Secure page
Card
Declined
Any other card number
Card
Approved

Payouts in test mode to a destination ending in 0000 fail with an invalid-account error.

Available to registered developers

The full API reference is one sign-up away.

Create a free account to access the complete reference — every endpoint, parameter, event type and error code — alongside your sandbox keys, webhook tester and request logs.