SwiftPay — integrator guide

Collect and pay out MTN / Airtel mobile money (RWF) through one API.
Your app talks to SwiftPay — we talk to PalmKash and MoMo.

Docshttps://pay.swifttech.rw/docs
Dashboardhttps://pay.swifttech.rw
API basehttps://pay.swifttech.rw/api/v1
Interactive APIhttps://pay.swifttech.rw/api/docs
HealthGET https://pay.swifttech.rw/health

1. Get an API key (token)

You do not log in to the API with email/password. You use a secret API key.

  1. Register
  2. Log in and complete merchant onboarding (business details + a KYC document)
  3. Wait until an admin approves your merchant
  4. Open API Keys in the dashboard
  5. Create a key (fpk_test_… sandbox or fpk_live_… live)
  6. Copy it once and store it on your server

Send it on every request:

X-Api-Key: fpk_live_YOUR_KEY

Also accepted: Authorization: Bearer fpk_live_YOUR_KEY

Grant these scopes on the key (the dashboard selects them by default):

  • payments:read / payments:write — collect and check payment status
  • transactions:read — list all ledger entries
  • settlements:read / settlements:write — balance and payouts
  • webhooks:read / webhooks:write — optional webhook management

Never put a live key in a browser, mobile app, or Git repo. The Try it box on this page is for sandbox keys only.


2. Collect money

POST /payments creates the payment and sends the MoMo prompt. Currency is always RWF.

POST /api/v1/payments
X-Api-Key: fpk_live_...
Content-Type: application/json

{
  "amount": "10000",
  "method": "MOBILE_MONEY",
  "description": "Order #1042",
  "metadata": {
    "phoneNumber": "2507XXXXXXXX"
  }
}

metadata.phoneNumber is required (MTN or Airtel, Rwanda).

If status is still PENDING, send the prompt with:

POST /api/v1/payments/{id}/process

Do not call process again while status is PROCESSING — that would not send a second prompt; we only sync the existing one.

Code examples

Copy-paste server examples in JavaScript and Python. Keep your API key on the server only.

Collect a MoMo payment

POST /payments sends the phone prompt. Poll GET /payments/{id} or wait for the webhook.

const API_BASE = "https://pay.swifttech.rw/api/v1";
const API_KEY = process.env.SWIFTPAY_API_KEY; // fpk_live_...

async function collectMobileMoneyPayment() {
  const res = await fetch(`${API_BASE}/payments`, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "X-Api-Key": API_KEY,
    },
    body: JSON.stringify({
      amount: "10000.00",
      method: "MOBILE_MONEY",
      description: "Order #1042",
      metadata: { phoneNumber: "2507XXXXXXXX" },
    }),
  });

  const created = await res.json();
  if (!created.success) {
    throw new Error(created.error?.message || "Create payment failed");
  }

  // Prompt is sent on create. Poll or wait for webhook.
  // const status = await fetch(`${API_BASE}/payments/${created.data.id}`, {
  //   headers: { "X-Api-Key": API_KEY },
  // });

  return created.data;
}

collectMobileMoneyPayment().then(console.log).catch(console.error);

Status and balance

GET /payments/{id} and GET /balance.

const API_BASE = "https://pay.swifttech.rw/api/v1";
const API_KEY = process.env.SWIFTPAY_API_KEY;

async function checkStatusAndBalance(paymentId) {
  const paymentRes = await fetch(`${API_BASE}/payments/${paymentId}`, {
    headers: { "X-Api-Key": API_KEY },
  });
  const payment = await paymentRes.json();

  const balanceRes = await fetch(`${API_BASE}/balance`, {
    headers: { "X-Api-Key": API_KEY },
  });
  const balance = await balanceRes.json();

  return { payment: payment.data, balance: balance.data };
}

Send a payout

POST /payouts for one recipient, POST /bulk-payouts for many.

const API_BASE = "https://pay.swifttech.rw/api/v1";
const API_KEY = process.env.SWIFTPAY_API_KEY;

async function sendPayout() {
  const res = await fetch(`${API_BASE}/payouts`, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "X-Api-Key": API_KEY,
    },
    body: JSON.stringify({
      phoneNumber: "2507XXXXXXXX",
      amount: 5000,
      recipientName: "Jane Uwase",
    }),
  });
  const json = await res.json();
  if (!json.success) throw new Error(json.error?.message || "Payout failed");
  return json.data;
}

async function sendBulkPayout() {
  const res = await fetch(`${API_BASE}/bulk-payouts`, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "X-Api-Key": API_KEY,
    },
    body: JSON.stringify({
      rows: [
        { phoneNumber: "2507XXXXXXXX", recipientName: "Jane", amount: 5000 },
        { phoneNumber: "25078YYYYYYY", recipientName: "Eric", amount: 2500 },
      ],
    }),
  });
  return res.json();
}

Verify a webhook

Always verify X-SwiftPay-Signature on the raw request body.

import crypto from "crypto";
import express from "express";

const app = express();
const WEBHOOK_SECRET = process.env.SWIFTPAY_WEBHOOK_SECRET;

function verifySwiftPaySignature(rawBody, signatureHeader) {
  const expected = crypto
    .createHmac("sha256", WEBHOOK_SECRET)
    .update(rawBody)
    .digest("hex");

  try {
    return crypto.timingSafeEqual(
      Buffer.from(expected, "utf8"),
      Buffer.from(signatureHeader || "", "utf8"),
    );
  } catch {
    return false;
  }
}

app.post(
  "/hooks/swiftpay",
  express.raw({ type: "application/json" }),
  (req, res) => {
    const rawBody = req.body.toString("utf8");
    const signature = req.header("X-SwiftPay-Signature");

    if (!verifySwiftPaySignature(rawBody, signature)) {
      return res.status(401).send("Invalid signature");
    }

    const event = JSON.parse(rawBody);
    console.log("SwiftPay event", event);
    return res.status(200).json({ received: true });
  },
);

Try it here

Paste a key from Dashboard → API Keys, then collect, check status, check balance, or send a payout. Keys stay in your browser — we don’t store them.

Results appear here.

3. Check payment status

Prefer webhooks. To poll:

GET /api/v1/payments/{id}
X-Api-Key: fpk_live_...

Ask PalmKash for a live update:

POST /api/v1/payments/{id}/verify
StatusMeaning
PENDINGCreated, prompt not sent yet
PROCESSINGWaiting on the customer / network
SUCCESSFULPaid — net added to available balance
FAILEDDeclined, timed out, or error

List everything (collections, payouts, fees):

GET /api/v1/transactions

4. Check balance

GET /api/v1/balance
X-Api-Key: fpk_live_...
{
  "success": true,
  "data": {
    "merchantId": "…",
    "availableBalance": "9900.00",
    "pendingBalance": "0.00",
    "currencyCode": "RWF"
  }
}

You can only pay out from availableBalance.


5. Payout — single

Sends money to one phone number from your available balance. Recipient gets the amount; you are charged PalmKash’s withdraw deduction + 100 RWF.

POST /api/v1/payouts
X-Api-Key: fpk_live_...
Content-Type: application/json

{
  "phoneNumber": "2507XXXXXXXX",
  "amount": 5000,
  "recipientName": "Jane Uwase"
}

6. Payout — bulk

Same money movement, many recipients (max 500). CSV from the dashboard also works.

POST /api/v1/bulk-payouts
X-Api-Key: fpk_live_...
Content-Type: application/json

{
  "rows": [
    { "phoneNumber": "2507XXXXXXXX", "recipientName": "Jane", "amount": 5000 },
    { "phoneNumber": "25078YYYYYYY", "recipientName": "Eric", "amount": 2500 }
  ]
}

7. Fees (RWF)

We use PalmKash’s actual deduction, then add SwiftPay’s markup for that merchant.

Platform defaults (change in Settings → Fees; override on a merchant profile):

MovementDefault markup
Collection1% of gross
Payout100 RWF

Example — 10,000 RWF collection at the default 1% (PalmKash reports 0 on sandbox until they send a charge):

Amount
Gross10,000 RWF
SwiftPay100 RWF (1%)
Merchant net9,900 RWF − PalmKash

8. Webhooks

In the dashboard, open Webhooks, add your HTTPS URL, and save the signing secret.

Verify X-SwiftPay-Signature = HMAC-SHA256 of the raw body with that secret.

Important events: payment.successful, payment.failed, settlement.completed.


Share a hosted MoMo checkout without calling collect from your server:

https://pay.swifttech.rw/checkout/{reference}?secret=…

Create links in the dashboard (Payments) or POST /api/v1/payments/links.


Go-live checklist

  • Merchant approved
  • Live API key created (fpk_live_…)
  • Webhook HTTPS + signature verification tested
  • Collect → customer approves → status SUCCESSFUL → balance increases
  • Single and bulk payout against available balance

Security

  1. Keep API keys on the server only
  2. Always verify webhook signatures
  3. You do not need PalmKash credentials — SwiftPay owns that connection