API de paiement Konbit
Encaissez un paiement depuis le wallet Konbit de vos clients. Intégration en quelques lignes, sandbox gratuit.
Comment ça marche
Votre serveur crée une intention de paiement. Le client la règle dans son app Konbit avec son code PIN. Vous êtes notifié par webhook signé et vous livrez votre service numérique.
1. Créez une intention
Un appel serveur avec le montant. Vous recevez un lien de paiement et un identifiant.
2. Le client paie
Il ouvre le lien, confirme avec son PIN. Aucun frais pour lui : il paie le montant affiché.
3. Vous êtes notifié
Un webhook signé confirme le paiement. Vous livrez le service. L'argent arrive sur votre solde Konbit.
amountMinor est un entier dans la plus petite unité : 10000 = 100,00 DOP. Jamais de décimales.
Démarrage en 5 minutes
Trois étapes : votre serveur crée une intention, le client la règle dans Konbit, votre serveur confirme le paiement avant de livrer. Voici l'appel complet et la réponse réelle, annotée.
curl -X POST https://app.getkonbit.com/api/v1/payment_intents \
-H "Authorization: Bearer sk_test_..." \
-H "Idempotency-Key: order-123" \
-H "Content-Type: application/json" \
-d '{"amountMinor":10000,"description":"Order 123","metadata":{"orderId":"order-123"},"returnUrl":"https://mon-site.com/merci"}'
{
"id": "5Qx0u5gexpnqsw6qQhks", // konbitRef — gardez-le
"status": "REQUIRES_PAYMENT",
"amountMinor": 10000, // 100,00 DOP
"currency": "DOP",
"feeMinor": 200, // notre commission
"code": "562183", // saisie manuelle / QR
"checkoutUrl": "https://app.getkonbit.com/pay/5Qx0u5gexpnqsw6qQhks",
"expiresAtMs": 1783710307539 // millisecondes
}
checkoutUrl : la page où le client paie. code : saisie manuelle / QR. feeMinor : notre commission, déjà déduite de votre solde (le client paie amountMinor). expiresAtMs : après quoi l'intention passe EXPIRED.
La redirection vers returnUrl a lieu dans le navigateur du client et peut être falsifiée (il suffit d'ouvrir l'URL). Ne livrez JAMAIS votre service sur la seule foi du retour. Confirmez toujours côté serveur : webhook signé ou GET du statut.
Obtenir vos accès
Deux niveaux, honnêtement présentés.
Clé de test — immédiate
Générez une clé sk_test_ ci-dessus : valable 7 jours, sandbox, SANS webhook. Parfaite pour explorer l'API par polling. Aucun argent réel ne bouge.
Clé marchande — sous 24 h
Écrivez-nous avec votre société et votre cas d'usage. On vous crée un marchand dédié : clé persistante + webhook signé. Réponse généralement sous 24 h ouvrées. (Le libre-service marchand arrivera ; aujourd'hui c'est nous qui l'ouvrons, pour la conformité.)
Gardez le secret secret
La clé sk_ n'apparaît qu'UNE fois et ne vit que côté serveur : jamais dans un navigateur, une app mobile, un dépôt Git ou une URL. Stockez-la dans un gestionnaire de secrets. Compromise ? Demandez une rotation : l'ancienne est révoquée sur-le-champ.
Générez une clé de test
Essayez tout de suite, sans compte. Une clé de test valable 7 jours, en mode sandbox.
Valable 7 jours. Mode test uniquement.
Impossible de générer une clé pour le moment. Réessayez.
Payer une intention de test (sandbox)
Pas besoin de compte Konbit : appelle POST /v1/test/pay avec l'id de l'intention. Konbit simule un payeur sandbox, passe l'intention à SUCCEEDED, envoie le webhook signé (si tu as fourni une URL) et crédite ton solde marchand. Tu testes ainsi tout le cycle en autonomie.
curl -X POST https://app.getkonbit.com/api/v1/test/pay \
-H "Authorization: Bearer sk_test_..." \
-H "Content-Type: application/json" \
-d '{"intentId":"<id renvoyé par /payment_intents>"}'
# -> l'intention passe à SUCCEEDED, le webhook signé part (si configuré),
# et ton solde marchand augmente (testable par GET /balance + payouts).
La clé générée ici est éphémère (7 jours), en sandbox : aucun argent réel. Elle suffit pour boucler create → pay → webhook → payout en autonomie. Pour une clé PERSISTANTE et le passage en production réelle, contacte-nous — un marchand dédié + la conformité viennent au go-live. Pourquoi « test » dans la clé (sk_test_) ? C'est le préfixe sandbox : impossible de bouger de l'argent réel. En sandbox, la « carte de test » = POST /v1/test/pay (payeur synthétique) ; toute page de paiement affiche un bandeau « SANDBOX ». Les clés de production (sk_live_) sont délivrées par Konbit après validation.
Intentions de paiement
REQUIRES_PAYMENT : en attente · SUCCEEDED : payé · EXPIRED : expiré · CANCELED : annulé par vous · REFUNDED : remboursé.
Montants
amountMinor est un entier en unités mineures (10000 = 100,00 DOP). Bornes : 1 000 à 5 000 000, soit 10 à 50 000 DOP. DOP uniquement en V1 (pas encore de HTG côté API).
Idempotency-Key (obligatoire)
Requise à la création. Vérifié : la MÊME clé (par marchand) renvoie TOUJOURS la même intention, sans doublon ni double débit. Utilisez votre identifiant de commande — un rejeu réseau est ainsi sans danger.
Réconciliation par metadata
Attachez vos identifiants (orderId, userId…) dans metadata (20 clés max). Ils reviennent tels quels dans le webhook et le GET, pour rapprocher un paiement de votre commande. N'y mettez AUCUN secret : le payeur et votre webhook les voient.
Référence de l'API
Authentifiez chaque requête avec votre clé secrète dans l'en-tête Authorization.
/v1/payment_intentsCréer une intention de paiement
curl -X POST https://app.getkonbit.com/api/v1/payment_intents \
-H "Authorization: Bearer sk_test_..." \
-H "Idempotency-Key: order-123" \
-H "Content-Type: application/json" \
-d '{"amountMinor":10000,"description":"Order 123","returnUrl":"https://mon-site.com/merci"}'
{
"id": "5Qx0u5gexpnqsw6qQhks",
"status": "REQUIRES_PAYMENT",
"amountMinor": 10000,
"currency": "DOP",
"feeMinor": 200,
"code": "562183",
"checkoutUrl": "https://app.getkonbit.com/pay/5Qx0u5gexpnqsw6qQhks",
"expiresAtMs": 1783710307539
}
Redirigez votre client vers checkoutUrl (ou affichez le QR) pour qu'il paie.
/v1/payment_intents/{id}Consulter le statut d'un paiement
{ "id": "5Qx0...", "status": "SUCCEEDED", "amountMinor": 10000, "feeMinor": 200 }
/v1/refundsRembourser un paiement
curl -X POST https://app.getkonbit.com/api/v1/refunds \
-H "Authorization: Bearer sk_test_..." \
-H "Content-Type: application/json" \
-d '{"payment_intent":"5Qx0u5gexpnqsw6qQhks"}'
/v1/balanceConsulter votre solde
{ "object": "balance", "balanceMinor": 9800, "totalFeesMinor": 200, "currency": "DOP" }
/v1/payoutsVerser une part de votre solde vers le wallet d'un chauffeur / bénéficiaire, par son code Konbit (9 caractères). Idempotency-Key obligatoire.
Clé marchande requise (contrat Konbit) : les clés de test du portail ne peuvent pas verser vers un vrai wallet — leur solde sandbox est fictif.
curl -X POST https://app.getkonbit.com/api/v1/payouts \
-H "Authorization: Bearer sk_test_..." \
-H "Idempotency-Key: driver-42-2026-W27" \
-H "Content-Type: application/json" \
-d '{"konbitCode":"K7M2P9QRS","amountMinor":85000,"metadata":{"driverId":"drv_42"}}'
{ "id":"p_8Kd...", "object":"payout", "status":"SUCCEEDED", "amountMinor":85000, "feeMinor":0, "currency":"DOP", "konbitCode":"K7M2P9QRS" }
/v1/payoutsLister vos versements, pour la réconciliation.
Statuts d'un paiement
REQUIRES_PAYMENT : en attente · SUCCEEDED : payé · EXPIRED : expiré · CANCELED : annulé par vous · REFUNDED : remboursé.
Codes d'erreur
Toute erreur renvoie le même objet : un code stable (à tester) et un message lisible.
{ "error": { "code": "amount_out_of_bounds", "message": "..." } }
| Code | Quand |
|---|---|
401 unauthorized | Clé absente, invalide ou révoquée. |
400 idempotency_key_required | En-tête Idempotency-Key manquant sur la création. |
400 amount_out_of_bounds / invalid_amount | Montant hors des bornes configurées, ou non entier. |
400 invalid_return_url | returnUrl fourni mais pas en https. |
404 not_found | Intention inconnue ou appartenant à un autre marchand. |
404 code_not_found | Code Konbit du bénéficiaire introuvable (payout). |
409 not_cancelable / not_refundable | Statut incompatible (déjà payé, déjà remboursé, non annulable…). |
409 insufficient_balance | Solde marchand insuffisant pour le remboursement ou le payout. |
429 quota_exceeded | Trop de requêtes. |
503 service_unavailable | Konbit Pay temporairement désactivé. |
Webhooks
Chaque changement d'état déclenche un POST JSON signé vers votre webhookUrl (https obligatoire). En-tête Konbit-Signature: t=
POST (votre webhookUrl)
Konbit-Signature: t=1720000000000,v1=<hmac_sha256>
{
"id": "evt_9f2c...", // id de l'ÉVÉNEMENT — dédup
"type": "payment_intent.succeeded", // .refunded · .canceled · .expired · .payment_failed (data.failureCode)
"createdAtMs": 1720000000000, // millisecondes
"data": {
"id": "5Qx0...", // id de l'intention (= konbitRef)
"status": "SUCCEEDED",
"amountMinor": 10000,
"currency": "DOP",
"feeMinor": 200,
"metadata": { "orderId": "order-123" }
}
}
Le t de la signature ET createdAtMs sont en millisecondes depuis l'epoch (13 chiffres). Le piège n°1 : les traiter comme des secondes fait échouer la fenêtre de 5 minutes et vous rejetez des webhooks valides.
Vérifier la signature, pas à pas
1. Lisez le corps BRUT (non parsé). 2. Extrayez t et v1 de l'en-tête. 3. Rejetez si |maintenant − t| > 5 min. 4. Calculez HMAC-SHA256(secret_webhook, "
Vérification — extraits prêts à copier
const crypto = require("crypto");
// IMPORTANT: raw body, pas express.json() sur cette route
app.post("/konbit/webhook",
express.raw({ type: "application/json" }),
(req, res) => {
const raw = req.body; // Buffer brut
const sig = req.get("Konbit-Signature") || "";
const parts = Object.fromEntries(sig.split(",").map(p => p.split("=")));
const t = Number(parts.t); // MILLISECONDES
if (!t || Math.abs(Date.now() - t) > 5 * 60 * 1000) return res.sendStatus(401);
const expected = crypto.createHmac("sha256", process.env.KONBIT_WEBHOOK_SECRET)
.update(t + "." + raw.toString("utf8")).digest("hex");
const a = Buffer.from(expected), b = Buffer.from(parts.v1 || "");
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) return res.sendStatus(401);
const event = JSON.parse(raw.toString("utf8"));
if (alreadyProcessed(event.id)) return res.sendStatus(200); // dédup sur event.id
// re-vérifiez event.data.amountMinor + currency contre VOTRE commande
markProcessed(event.id);
fulfill(event.data.metadata.orderId);
res.sendStatus(200);
});
<?php
$raw = file_get_contents("php://input"); // corps brut
$sig = $_SERVER["HTTP_KONBIT_SIGNATURE"] ?? "";
parse_str(str_replace(",", "&", $sig), $p); // t=...&v1=...
$t = (int)($p["t"] ?? 0); // MILLISECONDES
if (!$t || abs(round(microtime(true)*1000) - $t) > 300000) { http_response_code(401); exit; }
$expected = hash_hmac("sha256", $t . "." . $raw, getenv("KONBIT_WEBHOOK_SECRET"));
if (!hash_equals($expected, $p["v1"] ?? "")) { http_response_code(401); exit; }
$event = json_decode($raw, true);
if (already_processed($event["id"])) { http_response_code(200); exit; } // dédup
// re-vérifiez amountMinor + currency contre votre commande
mark_processed($event["id"]);
fulfill($event["data"]["metadata"]["orderId"]);
http_response_code(200);
import hmac, hashlib, time, os
from flask import request, abort
@app.post("/konbit/webhook")
def konbit_webhook():
raw = request.get_data() # bytes bruts
parts = dict(p.split("=", 1) for p in request.headers.get("Konbit-Signature", "").split(","))
t = int(parts.get("t", 0)) # MILLISECONDES
if not t or abs(time.time() * 1000 - t) > 300000:
abort(401)
expected = hmac.new(os.environ["KONBIT_WEBHOOK_SECRET"].encode(),
f"{t}.".encode() + raw, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, parts.get("v1", "")):
abort(401)
event = request.get_json()
if already_processed(event["id"]): # dédup sur event.id
return "", 200
# re-vérifiez amountMinor + currency contre votre commande
mark_processed(event["id"])
fulfill(event["data"]["metadata"]["orderId"])
return "", 200
Politique de relance
Livraison immédiate à l'événement. En cas d'échec (réponse hors 2xx, ou délai de 10 s dépassé), Konbit réessaie 5 fois, avec un backoff de 1, 5, 30, 120 puis 720 minutes — soit 6 tentatives au total sur environ 14,5 heures avant l'abandon (statut « dead », visible pour diagnostic). Chaque tentative est RESIGNÉE avec un t courant : votre fenêtre de 5 min accepte donc les relances. L'id de l'événement reste identique d'une relance à l'autre — dédupliquez dessus.
Ou par interrogation (polling)
Deux façons de savoir qu'un paiement a réussi : recevoir le webhook, ou interroger l'intention jusqu'au statut SUCCEEDED.
curl https://app.getkonbit.com/api/v1/payment_intents/{id} \
-H "Authorization: Bearer sk_test_..."
# répétez jusqu'à "status": "SUCCEEDED"
Webhook ET polling de réconciliation, jamais l'un sans l'autre. Le webhook vous notifie vite ; un GET périodique des intentions récentes rattrape toute livraison manquée (endpoint momentanément indisponible, événement « dead »). Ne dépendez jamais d'une seule voie.
Payer une intention de test
En sandbox, le plus simple est POST /v1/test/pay (ci-dessus) : aucun compte Konbit requis. Sinon, ouvre le checkoutUrl connecté à un compte Konbit de test approvisionné. Une fois l'encaissement carte activé pour ton compte (voir plus bas), le même checkoutUrl propose aussi « Payer par carte » (sans compte Konbit) : l'intention est alors marquée paidViaCard, créditée à ton solde USD, et tu reçois card_payment.succeeded + payment_intent.succeeded (déduplique par id).
Checklist de mise en production
Onze points avant de basculer. Les trois en gras sont ceux que 90 % des intégrateurs oublient — et qui coûtent de l'argent ou ouvrent une faille.
- Clé sk_ marchande obtenue et stockée côté serveur (gestionnaire de secrets), jamais côté client.
- Idempotency-Key posée sur chaque création (votre orderId).
- Montants en unités mineures, dans les bornes ; devise DOP.
- Corps BRUT préservé pour la vérif HMAC (aucun parsing avant la signature).
- Endpoint webhook en https, répond 2xx en moins de 10 s, traitement lourd en asynchrone.
- Signature vérifiée en temps constant, t traité en millisecondes, requêtes de plus de 5 min rejetées.
- Polling de réconciliation en filet de sécurité, en plus du webhook.
- Statuts EXPIRED / CANCELED / REFUNDED et codes d'erreur gérés proprement.
- Déduplication sur l'id de l'ÉVÉNEMENT (livraison au-moins-une-fois).
- Ré-vérification du montant ET de la devise du webhook contre votre commande — ne faites pas confiance au seul type d'événement.
- Aucun chemin côté client ne marque « payé » : returnUrl n'est pas une preuve, seule la confirmation serveur l'est.
Rail USD : encaissez par carte, versez par Zelle
Pour les plateformes qui opèrent en dollars (investissement, paie, marketplaces) : vos clients paient par carte sur un checkout hébergé Konbit — sans compte Konbit — et vous distribuez en USD par Zelle, par lots, avec confirmation webhook ligne par ligne.
1. Encaissement carte
Votre serveur crée l'intention (montant figé), le payeur tape sa carte sur /cpay. Frais Konbit ajoutés au montant, votre solde USD reçoit le net.
2. Fonds sécurisés
Chaque paiement passe par une période de sécurisation anti-fraude avant d'être distribuable. Solde en trois états : en attente, disponible, réservé.
3. Versements Zelle
Vous soumettez la liste (jusqu'à 200 bénéficiaires par lot). Konbit exécute chaque virement et vous notifie par webhook signé : payé ou échoué, ligne par ligne.
L'encaissement par carte (rail USD) est une activation PAR MARCHAND, validée par Konbit (conformité, clause de rétrofacturation) — jamais sur une clé de test. Une fois activé, vos liens de paiement DOP offrent aussi l'option carte au payeur, sans compte Konbit. Écrivez-nous pour l'activer.