Konbit
Konbit Pay · API

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.

Les montants sont en unités mineures

amountMinor est un entier dans la plus petite unité : 10000 = 100,00 DOP. Jamais de décimales.

1 · Démarrage

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"}'
Réponse
{
  "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.

⚠ returnUrl n'est PAS une preuve de paiement

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.

2 · Vos accès

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.

Sandbox

Générez une clé de test

Essayez tout de suite, sans compte. Une clé de test valable 7 jours, en mode sandbox.

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).
Ce que la clé de test permet (et ses limites)

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.

3 · Paiements

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.

5 · Codes d'erreur

Référence de l'API

Authentifiez chaque requête avec votre clé secrète dans l'en-tête Authorization.

POST/v1/payment_intents

Cré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"}'
Réponse
{
  "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.

GET/v1/payment_intents/{id}

Consulter le statut d'un paiement

Réponse
{ "id": "5Qx0...", "status": "SUCCEEDED", "amountMinor": 10000, "feeMinor": 200 }
POST/v1/refunds

Rembourser 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"}'
GET/v1/balance

Consulter votre solde

Réponse
{ "object": "balance", "balanceMinor": 9800, "totalFeesMinor": 200, "currency": "DOP" }
POST/v1/payouts

Verser 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"}}'
Réponse
{ "id":"p_8Kd...", "object":"payout", "status":"SUCCEEDED", "amountMinor":85000, "feeMinor":0, "currency":"DOP", "konbitCode":"K7M2P9QRS" }
GET/v1/payouts

Lister 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": "..." } }
CodeQuand
401 unauthorizedClé absente, invalide ou révoquée.
400 idempotency_key_requiredEn-tête Idempotency-Key manquant sur la création.
400 amount_out_of_bounds / invalid_amountMontant hors des bornes configurées, ou non entier.
400 invalid_return_urlreturnUrl fourni mais pas en https.
404 not_foundIntention inconnue ou appartenant à un autre marchand.
404 code_not_foundCode Konbit du bénéficiaire introuvable (payout).
409 not_cancelable / not_refundableStatut incompatible (déjà payé, déjà remboursé, non annulable…).
409 insufficient_balanceSolde marchand insuffisant pour le remboursement ou le payout.
429 quota_exceededTrop de requêtes.
503 service_unavailableKonbit Pay temporairement désactivé.
4 · Webhooks

Webhooks

Chaque changement d'état déclenche un POST JSON signé vers votre webhookUrl (https obligatoire). En-tête Konbit-Signature: t=,v1=. Voici l'enveloppe exacte :

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" }
  }
}
⚠ t est en MILLISECONDES

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, "."). 5. Comparez à v1 en temps constant. 6. Dédupliquez sur id (l'identifiant de l'ÉVÉNEMENT, pas data.id).

Vérification — extraits prêts à copier

Node.js / Express
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
<?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);
Python / Flask
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"
La règle d'or

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).

6 · Go-live

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.

Encaissement carte : activation par marchand

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.