Documentation
Documentation Développeur
Payinstant est une passerelle de séquestre Mobile Money pour le e-commerce en RDC. Chaque page ci-dessous explique une étape : auth, création, collect, webhooks, puis guides PHP / Node / Next / WordPress…
http://localhost:4040
https://api.payinstant.cd
Authorization: Bearer sk_test_… ou sk_live_…
Idempotency-Key: <uuid> sur les POST argent
Gateway only : hold / release / refund. Nous ne jugeons pas les litiges. Payinstant opère le rail Mobile Money — vos clients ne voient que votre marque.
Démarrage
Quickstart
Enchaînez les 5 appels essentiels pour créer un séquestre, collecter le Mobile Money, attendre held, puis release ou refund.
- Créez un compte sur le dashboard → récupérez
sk_test_+webhook_secret. POST /v1/escrows— créer le séquestre (payer + payee).POST /v1/escrows/:id/collect— déclencher le PayIn Mobile Money.- Attendez le webhook
escrow.held(ou pollGET /v1/escrows/:id). POST …/releaseou…/refund.
Sécurité
Authentification
Utilisez sk_ côté serveur pour l’argent, pk_ en front si besoin d’identifiant boutique, et webhook_secret pour vérifier les signatures.
| Clé | Usage | Où |
|---|---|---|
sk_test_* / sk_live_* |
Appels serveur (escrows, release…) | Backend uniquement — jamais en front |
pk_test_* / pk_live_* |
Identifiant public boutique | Frontend OK (pas d’appels secret) |
webhook_secret |
Vérifier X-Payinstant-Signature |
Backend webhook |
sk_ dans le navigateur = compromission. Toutes les opérations argent passent par votre serveur.
Cycle de vie
Flux & statuts
Chaque séquestre traverse des états précis. Cette page explique ce que signifie created, collecting, held, released, etc.
| Statut | Signification |
|---|---|
created | Séquestre créé, pas encore de PayIn |
collecting | PayIn en cours chez l’opérateur |
held | Fonds cantonnés — vous pouvez release ou refund |
frozen | Gelé (litige) — pas de sortie |
released | Payee crédité (net) |
refunded | Payer remboursé |
failed / cancelled | Échec ou annulation avant hold |
API
Créer un séquestre
Ouvrez une intention de hold avec montant, devise, parties payer/payee et référence marchande. Exemples multi-langages.
Crée une intention de hold. parties doit inclure au moins un payer et un payee.
curl -X POST http://localhost:4040/v1/escrows \
-H "Authorization: Bearer sk_test_YOUR_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"merchant_ref": "order_1042",
"amount": 25000,
"currency": "CDF",
"commission_amount": 2500,
"callback_url": "https://shop.cd/hooks/payinstant",
"parties": [
{ "role": "payer", "phone": "0972148867", "external_id": "buyer_1" },
{ "role": "payee", "phone": "0998001122", "display_name": "Seller" }
]
}'
<?php
$payload = [
'merchant_ref' => 'order_1042',
'amount' => 25000,
'currency' => 'CDF',
'commission_amount' => 2500,
'callback_url' => 'https://shop.cd/hooks/payinstant',
'parties' => [
['role' => 'payer', 'phone' => '0972148867'],
['role' => 'payee', 'phone' => '0998001122', 'display_name' => 'Seller'],
],
];
$ch = curl_init('http://localhost:4040/v1/escrows');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('PAYINSTANT_SECRET_KEY'),
'Idempotency-Key: ' . bin2hex(random_bytes(16)),
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode($payload),
]);
$res = json_decode(curl_exec($ch), true);
$escrowId = $res['data']['id'];
import { randomUUID } from "node:crypto";
const res = await fetch("http://localhost:4040/v1/escrows", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.PAYINSTANT_SECRET_KEY}`,
"Idempotency-Key": randomUUID(),
"Content-Type": "application/json",
},
body: JSON.stringify({
merchant_ref: "order_1042",
amount: 25000,
currency: "CDF",
commission_amount: 2500,
callback_url: "https://shop.cd/hooks/payinstant",
parties: [
{ role: "payer", phone: "0972148867" },
{ role: "payee", phone: "0998001122", display_name: "Seller" },
],
}),
});
const { data } = await res.json();
console.log(data.id, data.status);
// ⚠️ Ne jamais appeler /v1/escrows avec sk_ depuis le navigateur.
// Exposez plutôt un endpoint sur VOTRE backend, puis :
async function createEscrowFromCheckout(orderId) {
const res = await fetch("/api/payinstant/escrows", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ orderId }),
});
return res.json();
}
// app/api/payinstant/escrows/route.ts
import { randomUUID } from "crypto";
import { NextResponse } from "next/server";
export async function POST(req: Request) {
const body = await req.json();
const res = await fetch(`${process.env.KANGA_BASE_URL}/v1/escrows`, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.PAYINSTANT_SECRET_KEY}`,
"Idempotency-Key": randomUUID(),
"Content-Type": "application/json",
},
body: JSON.stringify({
merchant_ref: body.orderId,
amount: body.amount,
currency: "CDF",
parties: body.parties,
}),
});
const json = await res.json();
return NextResponse.json(json, { status: res.status });
}
import os, uuid, requests
r = requests.post(
"http://localhost:4040/v1/escrows",
headers={
"Authorization": f"Bearer {os.environ['PAYINSTANT_SECRET_KEY']}",
"Idempotency-Key": str(uuid.uuid4()),
"Content-Type": "application/json",
},
json={
"merchant_ref": "order_1042",
"amount": 25000,
"currency": "CDF",
"commission_amount": 2500,
"parties": [
{"role": "payer", "phone": "0972148867"},
{"role": "payee", "phone": "0998001122", "display_name": "Seller"},
],
},
)
print(r.json()["data"]["id"])
API
Collect — PayIn Mobile Money
Déclenchez le débit du numéro client. Le séquestre passe en collecting puis held (ou failed) via callback / sync.
Débite le numéro client. Statuts acceptés : created, failed (retry).
| Champ | Type | Notes |
|---|---|---|
customer_number | string | Ex. 0972148867 |
method | string | airtel, orange, mpesa… |
firstname, lastname | string | Requis |
email | string | Requis |
curl -X POST http://localhost:4040/v1/escrows/$ID/collect \
-H "Authorization: Bearer sk_test_YOUR_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"customer_number": "0972148867",
"method": "airtel",
"firstname": "Paul",
"lastname": "Client",
"email": "paul@example.com"
}'
$ch = curl_init("http://localhost:4040/v1/escrows/{$escrowId}/collect");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('PAYINSTANT_SECRET_KEY'),
'Idempotency-Key: ' . bin2hex(random_bytes(16)),
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'customer_number' => '0972148867',
'method' => 'airtel',
'firstname' => 'Paul',
'lastname' => 'Client',
'email' => 'paul@example.com',
]),
]);
$status = json_decode(curl_exec($ch), true)['data']['status']; // collecting
await fetch(`http://localhost:4040/v1/escrows/${id}/collect`, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.PAYINSTANT_SECRET_KEY}`,
"Idempotency-Key": crypto.randomUUID(),
"Content-Type": "application/json",
},
body: JSON.stringify({
customer_number: "0972148867",
method: "airtel",
firstname: "Paul",
lastname: "Client",
email: "paul@example.com",
}),
});
// app/api/payinstant/collect/route.ts
export async function POST(req: Request) {
const { id, ...body } = await req.json();
const res = await fetch(
`${process.env.KANGA_BASE_URL}/v1/escrows/${id}/collect`,
{
method: "POST",
headers: {
Authorization: `Bearer ${process.env.PAYINSTANT_SECRET_KEY}`,
"Idempotency-Key": crypto.randomUUID(),
"Content-Type": "application/json",
},
body: JSON.stringify(body),
},
);
return Response.json(await res.json(), { status: res.status });
}
API
Release & Refund
Une fois held : libérez le net au payee (release) ou remboursez le payer (refund). Corps vide, auth + Idempotency-Key.
Crédit net au payee. Séquestre doit être held.
Rembourse le payer. Séquestre doit être held.
# Libérer au vendeur
curl -X POST http://localhost:4040/v1/escrows/$ID/release \
-H "Authorization: Bearer sk_test_YOUR_KEY" \
-H "Idempotency-Key: $(uuidgen)"
# Ou rembourser l'acheteur
curl -X POST http://localhost:4040/v1/escrows/$ID/refund \
-H "Authorization: Bearer sk_test_YOUR_KEY" \
-H "Idempotency-Key: $(uuidgen)"
function kanga_post(string $path): array {
$ch = curl_init('http://localhost:4040' . $path);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('PAYINSTANT_SECRET_KEY'),
'Idempotency-Key: ' . bin2hex(random_bytes(16)),
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => '{}',
]);
return json_decode(curl_exec($ch), true);
}
kanga_post("/v1/escrows/{$id}/release");
// kanga_post("/v1/escrows/{$id}/refund");
async function release(id) {
const res = await fetch(`http://localhost:4040/v1/escrows/${id}/release`, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.PAYINSTANT_SECRET_KEY}`,
"Idempotency-Key": crypto.randomUUID(),
},
});
if (!res.ok) throw new Error((await res.json()).error);
return res.json();
}
<?php
/** functions.php ou plugin — release après livraison WooCommerce */
function kanga_release_escrow($escrow_id) {
$res = wp_remote_post(
trailingslashit(getenv('KANGA_BASE_URL')) . "v1/escrows/{$escrow_id}/release",
[
'headers' => [
'Authorization' => 'Bearer ' . getenv('PAYINSTANT_SECRET_KEY'),
'Idempotency-Key' => wp_generate_uuid4(),
'Content-Type' => 'application/json',
],
'body' => '{}',
'timeout' => 30,
]
);
return json_decode(wp_remote_retrieve_body($res), true);
}
add_action('woocommerce_order_status_completed', function ($order_id) {
$escrow_id = get_post_meta($order_id, '_kanga_escrow_id', true);
if ($escrow_id) kanga_release_escrow($escrow_id);
});
API
Statut & sync
Lisez un séquestre par id ou merchant_ref, forcez une vérification opérateur, gelez / dégelez ou annulez avant hold.
| Méthode | Path | Description |
|---|---|---|
| GET | /v1/escrows/:id | Détail |
| GET | /v1/escrows/by-ref/:merchantRef | Lookup commande |
| POST | /v1/escrows/:id/sync | Rafraîchir via la vérification opérateur (verify) |
| POST | /v1/escrows/:id/freeze | Geler (litige) |
| POST | /v1/escrows/:id/unfreeze | Dégeler |
| POST | /v1/escrows/:id/cancel | Annuler avant hold |
Événements
Webhooks (Payinstant → votre backend)
Recevez escrow.held, released, refunded… Vérifiez toujours X-Payinstant-Signature (HMAC-SHA256) avant de traiter.
Headers : X-Payinstant-Signature (HMAC-SHA256 hex du raw body avec votre webhook_secret),
X-Payinstant-Event.
Événements : escrow.created, collecting, held, frozen,
unfrozen, releasing, released, refunding,
refunded, failed, cancelled.
import express from "express";
import crypto from "crypto";
const app = express();
app.post("/hooks/payinstant", express.raw({ type: "application/json" }), (req, res) => {
const raw = req.body.toString("utf8");
const sig = req.get("x-kanga-signature") || "";
const expected = crypto
.createHmac("sha256", process.env.KANGA_WEBHOOK_SECRET)
.update(raw)
.digest("hex");
if (!crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) {
return res.status(401).send("invalid signature");
}
const event = JSON.parse(raw);
// event.event === "escrow.held" | "escrow.released" | …
res.json({ ok: true });
});
<?php
$raw = file_get_contents('php://input');
$sig = $_SERVER['HTTP_X_KANGA_SIGNATURE'] ?? '';
$expected = hash_hmac('sha256', $raw, getenv('KANGA_WEBHOOK_SECRET'));
if (!hash_equals($expected, $sig)) {
http_response_code(401);
exit('invalid signature');
}
$event = json_decode($raw, true);
// $event['event'], $event['data']
http_response_code(200);
echo json_encode(['ok' => true]);
// app/api/hooks/payinstant/route.ts
import { createHmac, timingSafeEqual } from "crypto";
export async function POST(req: Request) {
const raw = await req.text();
const sig = req.headers.get("x-kanga-signature") || "";
const expected = createHmac("sha256", process.env.KANGA_WEBHOOK_SECRET!)
.update(raw)
.digest("hex");
const a = Buffer.from(sig);
const b = Buffer.from(expected);
if (a.length !== b.length || !timingSafeEqual(a, b)) {
return new Response("invalid signature", { status: 401 });
}
const event = JSON.parse(raw);
// handle event.event
return Response.json({ ok: true });
}
import hmac, hashlib
from flask import Flask, request, abort
app = Flask(__name__)
@app.post("/hooks/payinstant")
def kanga_hook():
raw = request.get_data()
sig = request.headers.get("X-Payinstant-Signature", "")
expected = hmac.new(
os.environ["KANGA_WEBHOOK_SECRET"].encode(), raw, hashlib.sha256
).hexdigest()
if not hmac.compare_digest(sig, expected):
abort(401)
event = request.get_json(force=True)
return {"ok": True}
<?php
add_action('rest_api_init', function () {
register_rest_route('kanga/v1', '/webhook', [
'methods' => 'POST',
'permission_callback' => '__return_true',
'callback' => function (WP_REST_Request $req) {
$raw = $req->get_body();
$sig = $req->get_header('x-kanga-signature');
$expected = hash_hmac('sha256', $raw, getenv('KANGA_WEBHOOK_SECRET'));
if (!hash_equals($expected, (string) $sig)) {
return new WP_Error('invalid_sig', 'Invalid signature', ['status' => 401]);
}
$event = json_decode($raw, true);
if (($event['event'] ?? '') === 'escrow.held') {
// marquer commande payée…
}
return ['ok' => true];
},
]);
});
// URL callback : https://votresite.cd/wp-json/kanga/v1/webhook
Test
Sandbox
Avec sk_test_, simulez un PayIn Successful/Failed sans USSD réel pour valider votre intégration.
Avec sk_test_, utilisez POST /v1/sandbox/simulate-payin pour forcer un PayIn Successful/Failed sans USSD réel.
curl -X POST http://localhost:4040/v1/sandbox/simulate-payin \
-H "Authorization: Bearer sk_test_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{ "escrow_id": "clx…", "outcome": "Successful" }'
Référence
Erreurs
Codes HTTP et messages renvoyés par l’API (validation, limites, compte suspendu, transitions invalides).
| HTTP | Cas typique |
|---|---|
| 400 | Validation / montant > max séquestre |
| 401 | Clé absente ou invalide |
| 403 | Compte suspendu / live non activé |
| 404 | Séquestre introuvable |
| 409 | Transition d’état invalide / conflit |
| 429 | Plafond journalier dépassé / rate limit |
Corps : { "error": "message" } (+ details Zod si validation).
Intégration
Guide PHP
Appels cURL / Guzzle côté serveur + endpoint webhook avec hash_hmac. Idéal pour PHP pur ou Laravel.
Utilisez cURL ou Guzzle. Stockez PAYINSTANT_SECRET_KEY et KANGA_WEBHOOK_SECRET hors du webroot (.env / variables serveur).
kanga_request($method, $path, $body) avec headers Authorization + Idempotency-Key.
Voir exemples Create / Collect / Webhook ci-dessus.
Intégration
Guide Node.js
fetch natif ou Axios dans Express / Fastify / Nest. Gardez PAYINSTANT_SECRET_KEY hors du client.
Node 18+ (fetch natif) ou Axios. Placez les appels dans un service kanga.js côté serveur (Express, Fastify, Nest…).
Intégration
Guide JavaScript (navigateur)
Le front parle à VOTRE API, jamais directement à sk_. Utilisez pk_ seulement comme identifiant public si besoin.
Le front peut afficher l’UI checkout et appeler votre API
(/api/payinstant/*). Utilisez pk_ uniquement comme identifiant boutique si besoin —
jamais sk_ dans le bundle.
Intégration
Guide Next.js
Route Handlers App Router pour create/collect/release + webhook HMAC. Variables d’env server-only.
- Variables :
KANGA_BASE_URL,PAYINSTANT_SECRET_KEY,KANGA_WEBHOOK_SECRET(server-only). - Route Handlers
app/api/…pour create / collect / release. - Route webhook
app/api/hooks/payinstantavec raw body + HMAC. - UI client : formulaire téléphone → POST vers votre Route Handler.
Intégration
Guide WordPress / WooCommerce
Créez le séquestre à la commande, collect au checkout, webhook REST, release à la livraison.
- Plugin ou
functions.php: constantes / env pour les clés. - À la commande :
POST /v1/escrows+ meta_kanga_escrow_id. - Checkout : collect via AJAX vers un endpoint admin-ajax ou REST.
- Webhook REST
/wp-json/kanga/v1/webhook→ maj statut commande. - Sur
woocommerce_order_status_completed→release.
Intégration
Guide Python
requests / httpx + Flask/Django/FastAPI pour l’API et la vérif HMAC des webhooks.
Flask / Django / FastAPI — même pattern : requests/httpx côté serveur + HMAC sur le webhook (voir onglet Python).
Intégration
cURL — scénario complet
Script bash bout-en-bout : create → collect → simulate-payin → release, pour tester sans framework.
export BASE=http://localhost:4040
export SK=sk_test_YOUR_KEY
# 1) Create
ID=$(curl -s $BASE/v1/escrows \
-H "Authorization: Bearer $SK" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"merchant_ref":"demo_'$(date +%s)'",
"amount":15000,"currency":"CDF","commission_amount":1500,
"parties":[
{"role":"payer","phone":"0972148867"},
{"role":"payee","phone":"0998001122","display_name":"Seller"}
]
}' | python3 -c "import sys,json; print(json.load(sys.stdin)['data']['id'])")
# 2) Collect
curl -s $BASE/v1/escrows/$ID/collect \
-H "Authorization: Bearer $SK" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"customer_number":"0972148867","method":"airtel","firstname":"Paul","lastname":"Client","email":"paul@example.com"}'
# 3) Simulate (sandbox)
curl -s $BASE/v1/sandbox/simulate-payin \
-H "Authorization: Bearer $SK" \
-H "Content-Type: application/json" \
-d "{\"escrow_id\":\"$ID\",\"outcome\":\"Successful\"}"
# 4) Release
curl -s $BASE/v1/escrows/$ID/release \
-H "Authorization: Bearer $SK" \
-H "Idempotency-Key: $(uuidgen)"
Créez un compte marchand — sandbox immédiat, live après KYC admin.
Ouvrir le dashboard