Payinstant Docs

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…

Base URL (dev) http://localhost:4040
Base URL (prod) https://api.payinstant.cd
Auth API Authorization: Bearer sk_test_… ou sk_live_…
Idempotence Idempotency-Key: <uuid> sur les POST argent
Rôle de Payinstant

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.

  1. Créez un compte sur le dashboard → récupérez sk_test_ + webhook_secret.
  2. POST /v1/escrows — créer le séquestre (payer + payee).
  3. POST /v1/escrows/:id/collect — déclencher le PayIn Mobile Money.
  4. Attendez le webhook escrow.held (ou poll GET /v1/escrows/:id).
  5. POST …/release ou …/refund.
1. Createintention de hold
2. CollectPayIn MM
3. Heldfonds cantonnés
4. Release / Refundsortie d’argent

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éUsageOù
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
Exposer une 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.

StatutSignification
createdSéquestre créé, pas encore de PayIn
collectingPayIn en cours chez l’opérateur
heldFonds cantonnés — vous pouvez release ou refund
frozenGelé (litige) — pas de sortie
releasedPayee crédité (net)
refundedPayer 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.

POST /v1/escrows

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.

POST /v1/escrows/:id/collect

Débite le numéro client. Statuts acceptés : created, failed (retry).

ChampTypeNotes
customer_numberstringEx. 0972148867
methodstringairtel, orange, mpesa…
firstname, lastnamestringRequis
emailstringRequis
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.

POST /v1/escrows/:id/release

Crédit net au payee. Séquestre doit être held.

POST /v1/escrows/:id/refund

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éthodePathDescription
GET/v1/escrows/:idDétail
GET/v1/escrows/by-ref/:merchantRefLookup commande
POST/v1/escrows/:id/syncRafraîchir via la vérification opérateur (verify)
POST/v1/escrows/:id/freezeGeler (litige)
POST/v1/escrows/:id/unfreezeDégeler
POST/v1/escrows/:id/cancelAnnuler 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).

HTTPCas typique
400Validation / montant > max séquestre
401Clé absente ou invalide
403Compte suspendu / live non activé
404Séquestre introuvable
409Transition d’état invalide / conflit
429Plafond 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).

Helper minimal : encapsulez 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.

  1. Variables : KANGA_BASE_URL, PAYINSTANT_SECRET_KEY, KANGA_WEBHOOK_SECRET (server-only).
  2. Route Handlers app/api/… pour create / collect / release.
  3. Route webhook app/api/hooks/payinstant avec raw body + HMAC.
  4. 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.

  1. Plugin ou functions.php : constantes / env pour les clés.
  2. À la commande : POST /v1/escrows + meta _kanga_escrow_id.
  3. Checkout : collect via AJAX vers un endpoint admin-ajax ou REST.
  4. Webhook REST /wp-json/kanga/v1/webhook → maj statut commande.
  5. Sur woocommerce_order_status_completed → release.
Désactivez le cache sur l’URL webhook. Vérifiez toujours la signature HMAC.

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)"
Besoin de clés ?

Créez un compte marchand — sandbox immédiat, live après KYC admin.

Ouvrir le dashboard