API paiement pour logiciels de caisse

Pour un restaurant, une boutique ou tout logiciel qui encaisse des commandes de montants différents. Chaque commerce a sa propre clé et reçoit l'argent sur son propre compte ZingoPay.

Démonstration : argent fictif, aucun fonds réel. Les clés commencent par zp_test_.

À lire avant de commencer

  • Démonstration. Tout l'argent est fictif. Aucun vrai franc n'est encaissé ni versé. Ne promettez pas de vrai paiement à vos clients.
  • Il n'y a pas de dépôt par l'API. L'API sert seulement à encaisser : créer un paiement et savoir s'il est payé. On ne peut pas déposer ni envoyer de l'argent par l'API. Le client dépose chez un agent ZingoPay ; le commerçant ne dépose jamais.
  • Où va l'argent encaissé. Sur le compte ZingoPay Business du commerçant (un compte par restaurant ou boutique). Une commission de 0,8 % est retenue au commerçant ; le client ne paie aucun frais.
  • Comment retirer. Depuis l'espace Business, bouton « Retirer mon argent » : soit on vire l'argent vers un compte client (le vôtre, créé avec une autre adresse e-mail), soit on retire chez un agent. Un retrait chez un agent coûte 1 % (payé en plus du montant, sans frais à partir du 11e retrait du jour).
  • Le statut fait foi. Une commande est payée uniquement quand status vaut paid sur ZingoPay. Un client qui dit « j'ai payé » ne prouve rien.
  • Mot de passe et code. À la première connexion, le commerçant remplace le mot de passe reçu par le sien et crée un code à 4 chiffres. ZingoPay ne connaît jamais le nouveau mot de passe. Ne partagez jamais votre clé API.
  • Avant de nous écrire. Vérifiez dans l'espace Business que la clé existe, que le montant est entre 100 et 1 000 000 FCFA, que la référence est nouvelle et que l'adresse de notification répond 200 (bouton « Envoyer un test »).

Comment ça marche

  1. Le commerçant crée sa clé secrète dans son espace (Liens de paiement, puis « Connecter mon logiciel de caisse »).
  2. À chaque commande, votre serveur crée un paiement avec le montant exact et le numéro de commande.
  3. Le client ouvre pay_url (ou scanne le QR) et paie avec son compte ZingoPay. Un paiement ne peut être payé qu'une seule fois.
  4. Votre serveur lit le statut sur ZingoPay (ou reçoit la notification) : seul paid prouve que l'argent est arrivé.

Authentification

Toutes les requêtes portent l'en-tête Authorization: Bearer <clé secrète>. La clé reste sur votre serveur : jamais dans une application mobile ni une page web. Un compte commerçant = une clé = un compte qui reçoit l'argent : pour plusieurs restaurants, créez une clé par restaurant.

1. Créer un paiement au montant exact

POST /api/v1/payments avec amount (entier, FCFA, de 100 à 1 000 000), reference (le numéro de commande, 64 caractères au plus, unique par commerçant), description (facultatif) et expires_in_minutes (facultatif, 60 par défaut, 10 080 au plus).

curl -X POST https://VOTRE-DOMAINE/api/v1/payments \
  -H "Authorization: Bearer zp_test_..." \
  -H "Content-Type: application/json" \
  -d '{ "amount": 12500, "reference": "CMD-2041", "description": "Table 7", "expires_in_minutes": 30 }'
{
  "id": "0b9c1c0e-5d5e-4a39-9a7c-3f1c2b7d9e11",
  "reference": "CMD-2041",
  "amount_xaf": 12500,
  "description": "Table 7",
  "status": "pending",
  "expires_at": "2026-11-06T15:30:00Z",
  "paid_at": null,
  "payment_reference": null,
  "pay_url": "https://VOTRE-DOMAINE/payer/3BBD3A015E"
}

Rejouer la même requête (même reference, même montant) renvoie le même paiement, sans en créer un second. Même référence avec un autre montant : erreur reference_conflict.

2. Vérifier côté serveur que c'est payé

GET /api/v1/payments/{id} ou GET /api/v1/payments?reference=CMD-2041. Le champ status vaut pending, paid, expired ou cancelled. Quand il vaut paid, paid_at et payment_reference (le numéro de reçu ZingoPay) sont remplis. N'affichez jamais « payé » sur la seule parole du téléphone du client.

Pour rapprocher la caisse : GET /api/v1/payments (sans paramètre) renvoie les 50 derniers paiements encaissés dans data, liens fixes compris (reference vaut null pour un lien fixe).

Annuler une commande non payée : POST /api/v1/payments/{id}/cancel.

3. Notification (webhook, facultatif)

Si le commerçant enregistre une adresse https, ZingoPay y envoie {"type":"payment.paid","data":{...}} dès le paiement (3 essais). Elle part aussi pour un lien fixe (reference vaut alors null). Le bouton « Envoyer un test » de l'espace commerçant envoie un événement payment.test pour vérifier votre adresse. Répondez avec un code 2xx. Vérifiez la signature ZingoPay-Signature avec le secret de signature, puis relisez le statut avec GET avant de valider la commande.

import { createHmac, timingSafeEqual } from "node:crypto";

// Corps brut de la requête (avant tout JSON.parse) et en-tête ZingoPay-Signature: t=1730900000,v1=<hex>
export function isValid(rawBody: string, header: string, secret: string) {
  const [t, v1] = header.split(",").map((p) => p.split("=")[1]);
  if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;
  const expected = createHmac("sha256", secret).update(t + "." + rawBody).digest("hex");
  return v1?.length === expected.length && timingSafeEqual(Buffer.from(v1), Buffer.from(expected));
}

Erreurs

Format : {"error":{"code":"invalid_amount","message":"..."}}. Codes : invalid_api_key (401), merchant_not_ready (403), invalid_amount, invalid_reference, invalid_expiry (400), not_found (404), reference_conflict (409), rate_limited (429).

Cette API fonctionne en démonstration, avec de l'argent fictif. Nous contacter