Getting started
The Transactions API lets your company collect payments (M-Pesa STK push, hosted checkout with card/bank/M-Pesa, Pesalink), send payouts (M-Pesa B2C, Pochi la Biashara, Till, Paybill), and manage customers and their wallets — all through a single REST API, with signed webhooks for real-time updates.
Payment links
Create an invoice, then redirect or embed — get paid by any method.
Direct API calls
Push an STK prompt or a payout straight from your own UI.
Customers & wallets
Onboard a customer — their wallet is created automatically.
Webhooks
Get a signed HMAC event the moment a transaction settles.
Two ways to integrate
Almost everything you'll build fits one of two shapes:
| Flow 1 — Direct API call | Flow 2 — Payment link | |
|---|---|---|
| You already have | Your own payment UI | No payment UI, or want one you don't maintain |
| You call | POST /transactions/.../initiate directly | POST /checkout/invoices, then redirect or embed the hosted page |
| The customer sees | Whatever you build | The Taifa Pay hosted checkout (redirect, or embedded via iframe) |
| SDK support today | None — call the HTTP API directly | @taifa/payments-node (create/reconcile) + @taifa/payments-js (embed) |
Both flows end the same way: you get back a pending transaction immediately, and the final outcome (COMPLETED / FAILED) arrives later — pushed via webhook or fetched with GET /transactions/{id}. Never treat the initiate/invoice response itself as the final result.
Base URL
All API endpoints are versioned under the /v1 prefix:
| Environment | Base URL |
|---|---|
| Sandbox | https://sandbox.merchants.taifapay.africa/v1 |
| Production | https://merchants.taifapay.africa/v1 |
Build and test your integration against sandbox first. Every endpoint in these guides behaves identically in both environments — only the host and your credentials change when you go live. The examples below use the sandbox host.
Onboarding
Before you write any code, three one-time things happen:
- We create your merchant account. Our team provisions the company that owns all your transactions, customers, and credentials — you don't create this yourself.
- You register your webhook URL(s) and collect your webhook secret. From the portal, tell us where to
POSTtransaction updates and copy the generated HMAC secret; you'll use it to verify deliveries (see Webhooks). - You create client credentials. Generate a
client_id/client_secretpair in the portal — one pair per environment (sandbox and production are separate). The secret is shown once; store it securely.
Once you have credentials, everything below is self-serve.
Prerequisites
To call the API you need the client credentials from onboarding above — a client_id and client_secret for the environment you're targeting. Keep the secret server-side; never embed it in a mobile app or browser code.
0. Get an access token
Both flows start the same way: exchange your credentials for a short-lived Bearer token (see Authentication for the full lifecycle). If you use the Node SDK (Flow 2, option A below) it does this for you automatically — skip ahead.
- cURL
- Node.js
- Python
curl -X POST https://sandbox.merchants.taifapay.africa/v1/auth/token \
-u "your_client_id:your_client_secret" \
-H "Content-Type: application/json" \
-d '{"grant_type": "client_credentials"}'
const res = await fetch('https://sandbox.merchants.taifapay.africa/v1/auth/token', {
method: 'POST',
headers: {
Authorization: 'Basic ' + Buffer.from(`${clientId}:${clientSecret}`).toString('base64'),
'Content-Type': 'application/json',
},
body: JSON.stringify({ grant_type: 'client_credentials' }),
});
const { access_token } = await res.json();
import base64
import requests
credentials = base64.b64encode(f"{client_id}:{client_secret}".encode()).decode()
response = requests.post(
"https://sandbox.merchants.taifapay.africa/v1/auth/token",
headers={
"Authorization": f"Basic {credentials}",
"Content-Type": "application/json",
},
json={"grant_type": "client_credentials"},
)
access_token = response.json()["access_token"]
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6...",
"expires_in": "3599",
"token_type": "Bearer"
}
Flow 1: Call the API directly
Use this when you already have your own payment UI and just need Taifa Pay to move the money — you prompt the customer yourself and react to the result. There's no SDK for this path yet; call the HTTP API with your Bearer token.
Example — send an M-Pesa STK push (see Direct payments & payouts for B2C, Pochi, Till, Paybill and Pesalink):
- cURL
- Node.js
- Python
curl -X POST https://sandbox.merchants.taifapay.africa/v1/transactions/m-pesa/c2b/initiate \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"phoneNumber": "0712345678",
"amount": 100,
"accountReference": "Account123",
"transactionDesc": "Top-up",
"externalId": "your-transaction-id"
}'
const res = await fetch('https://sandbox.merchants.taifapay.africa/v1/transactions/m-pesa/c2b/initiate', {
method: 'POST',
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
phoneNumber: '0712345678',
amount: 100,
accountReference: 'Account123',
transactionDesc: 'Top-up',
externalId: 'your-transaction-id',
}),
});
const { transaction } = await res.json();
response = requests.post(
"https://sandbox.merchants.taifapay.africa/v1/transactions/m-pesa/c2b/initiate",
headers={
"Authorization": f"Bearer {access_token}",
"Content-Type": "application/json",
},
json={
"phoneNumber": "0712345678",
"amount": 100,
"accountReference": "Account123",
"transactionDesc": "Top-up",
"externalId": "your-transaction-id",
},
)
transaction = response.json()["transaction"]
The customer enters their PIN on the prompt; the transaction resolves shortly after (see Track the payment below). Onboarding a customer instead? POST /customers creates their wallet in the same call — see Customers & wallets.
Flow 2: Payment links
Use this when you want a ready-made payment page (method switcher, card form, bank instructions) without building it yourself. Two steps: create an invoice, then send the customer to pay — either by redirecting them, or by embedding the checkout directly in your own page.
Step 1: Create an invoice
- cURL
- Node.js (SDK)
- Node.js (raw HTTP)
- Python
curl -X POST https://sandbox.merchants.taifapay.africa/v1/checkout/invoices \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"amount": 1500,
"accountReference": "INV-1001",
"description": "Order #1001 - 2x widget",
"customerPhone": "254712345678"
}'
// npm install @taifa/payments-node
import { TaifaPayments } from "@taifa/payments-node";
const taifa = new TaifaPayments({
clientId: process.env.TAIFA_CLIENT_ID!,
clientSecret: process.env.TAIFA_CLIENT_SECRET!,
environment: "sandbox", // or "production"
});
const invoice = await taifa.invoices.create({
amount: 1500,
accountReference: "INV-1001",
description: "Order #1001 - 2x widget",
customerPhone: "254712345678",
});
// invoice.invoiceNo -> hand to the browser SDK, or build your own redirect
// invoice.checkoutUrl -> or just redirect the customer here directly
// invoice.transactionId -> poll / webhook against this
The SDK holds your credentials, exchanges and caches the access token for you (no manual /auth/token call), and retries once on a mid-flight 401.
const res = await fetch('https://sandbox.merchants.taifapay.africa/v1/checkout/invoices', {
method: 'POST',
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
amount: 1500,
accountReference: 'INV-1001',
description: 'Order #1001 - 2x widget',
customerPhone: '254712345678',
}),
});
const { invoice } = await res.json();
response = requests.post(
"https://sandbox.merchants.taifapay.africa/v1/checkout/invoices",
headers={
"Authorization": f"Bearer {access_token}",
"Content-Type": "application/json",
},
json={
"amount": 1500,
"accountReference": "INV-1001",
"description": "Order #1001 - 2x widget",
"customerPhone": "254712345678",
},
)
invoice = response.json()["invoice"]
The response includes a checkoutUrl for the customer, an invoiceNo (for embedding), and a transactionId for you:
{
"message": "Invoice created",
"invoice": {
"transactionId": "a3f1c2d4-5e6b-7a8c-9d0e-1f2a3b4c5d6e",
"checkoutUrl": "https://pay.taifapay.africa/pay/ABCINV-1001",
"invoiceNo": "ABCINV-1001",
"amount": 1500,
"currency": "KES",
"status": "open",
"methods": ["mpesa_stk", "card"],
"expiresAt": "2026-07-17T12:30:00.000Z"
}
}
:::tip The transactionId is stable
transactionId settles whichever method the customer eventually picks (STK, card, bank, manual M-Pesa). Store it — it's the one id you track regardless of payment method.
:::
:::caution Creating an invoice always needs your secret
This step must happen server-side with your client_secret — the amount is fixed here, so it can never be set by the browser. Only the resulting public invoiceNo is safe to hand to the frontend.
:::
Step 2: Send the customer to pay
Pick one:
- Option A: Redirect
- Option B: Embed with the browser SDK
The simplest option — no extra dependency. Send the customer (SMS, email, WhatsApp, or an HTTP redirect from your site) to the checkoutUrl from step 1:
res.redirect(invoice.checkoutUrl);
// or just text/email invoice.checkoutUrl to the customer
The page is public and needs no authentication. It handles method selection, STK/card/bank collection, and shows its own status — you don't build any of that UI.
Keep the customer on your own page by rendering checkout in an iframe with @taifa/payments-js, using the invoiceNo from step 1 (never the secret, never the full URL):
npm install @taifa/payments-js
import { mountCheckout } from "@taifa/payments-js";
const handle = mountCheckout("#pay", {
invoiceNo: invoice.invoiceNo, // from your server, e.g. "ABCINV-1001"
host: "https://sandbox-pay.taifapay.africa", // sandbox; omit for production default
onSuccess: ({ receiptNumber }) => showThanks(receiptNumber),
onFailure: ({ message }) => showError(message),
onExpired: () => showExpired(),
});
// later, e.g. on unmount / route change:
handle.unmount();
Or with no bundler at all:
<script src="https://unpkg.com/@taifa/payments-js"></script>
<div id="pay"></div>
<script>
TaifaPay.mountCheckout("#pay", { invoiceNo: "ABCINV-1001", onSuccess: console.log });
</script>
Card data, PII, and your credentials never touch your page — everything is collected on the Taifa origin inside the iframe. onSuccess is a UX signal only; keep treating your webhook on transactionId as the authoritative confirmation before fulfilling an order.
Track the payment
Every money movement — invoice, direct STK, payout, wallet top-up — is a transaction with a stable id. Poll it any time:
- cURL
- Node.js (SDK)
curl https://sandbox.merchants.taifapay.africa/v1/transactions/a3f1c2d4-5e6b-7a8c-9d0e-1f2a3b4c5d6e \
-H "Authorization: Bearer $ACCESS_TOKEN"
const txn = await taifa.transactions.get(invoice.transactionId);
// or: await taifa.invoices.get(invoice.invoiceNo)
A transaction moves through PENDING → COMPLETED (or FAILED).
:::tip Prefer webhooks
Polling works, but a webhook pushes you the result the moment it's known — no busy-loop, no latency. The Node SDK ships constructWebhookEvent() / verifyWebhookSignature() to verify deliveries for you. Most integrations use webhooks as the source of truth and keep polling only as a reconciliation fallback (e.g. POST /webhooks/test to verify your endpoint, then rely on delivery).
:::
Going live
Once your flow works end-to-end against sandbox:
- Request production credentials (a separate
client_id/client_secret— sandbox credentials don't work against production). - Point your base URL (or
environment: "production"in the SDK) athttps://merchants.taifapay.africa/v1. - If you're embedding checkout, switch the browser SDK's
hostto the production default (https://pay.taifapay.africa— just omit the option) and get your origin allow-listed for framing. - Re-register your production webhook URL and rotate in its production webhook secret.
- Send a
POST /webhooks/testagainst production to confirm delivery and signature verification before taking real traffic.
Where to go next
- Authentication — token lifetimes and header options
- Hosted checkout & payment links — the full invoice → payment flow
- Inside the checkout page — a screen-by-screen tour of what your customer sees
- Customers & wallets — customer onboarding and wallet balances (creating a customer creates its wallet)
- Direct payments & payouts — STK push, B2C, Till, Paybill, Pochi
- Webhooks — signed event notifications
- API Reference — every endpoint, generated from the live OpenAPI spec