Retour à l'accueil

Documentation API · v1

Branchez votre application sur NDOSS.

Votre application envoie ses événements de paiement à NDOSS. Quand un paiement échoue, NDOSS propose à votre client de régler directement par Mobile Money, et vous prévient par webhook signé à chaque étape. Comptez une quinzaine de minutes pour une première intégration.

Démarrage rapide

Trois étapes. Tout se fait dans la rubrique Intégrations du menu de votre tableau de bord NDOSS, puis dans votre code.

1. Créez une clé d’API de test

Dans l’onglet Intégrations, créez une clé test. Elle n’est affichée qu’une fois : copiez-la dans les variables d’environnement de votre serveur, jamais dans du code côté navigateur.

2. Envoyez un premier événement

curl

curl -X POST https://ndoss.vercel.app/api/v1/events \
  -H "Authorization: Bearer ndoss_test_VOTRE_CLE" \
  -H "Content-Type: application/json" \
  -d '{ "type": "payment.failed", "idempotency_key": "evt_unique_de_votre_app", "user_ref": "id_utilisateur_dans_votre_app", "occurred_at": "2026-09-23T09:29:00Z", "data": { "amount": 5000, "currency": "XAF", "reference": "REF-4821", "reference_label": "Motif du transfert", "reason": "insufficient_balance", "product": { "id": "pack-120", "name": "Pack 120 crédits" }, "customer": { "first_name": "Kewsia", "email": "kewsia@exemple.com", "phone": "+24106000000" } }, "metadata": {} }'

Réponse attendue : 202 avec {"ok":true}. L’événement apparaît dans Intégrations → Historique → Événements reçus, et le paiement dans l’onglet Paiements, avec la mention « test ».

3. Branchez votre webhook

Saisissez l’URL HTTPS de votre serveur dans « Webhook sortant » : NDOSS génère un secret de signature. Rejouez ensuite le parcours du client avec la simulation (clé test uniquement) :

curl

# 1. le client "accepte" le paiement direct → vous recevez manual_payment.requested (livemode:false)
curl -X POST https://ndoss.vercel.app/api/v1/manual-payments/REF-4821/simulate \
  -H "Authorization: Bearer ndoss_test_VOTRE_CLE" \
  -d '{"action":"request"}'

# 2. le client "clique J'ai payé" → vous recevez customer.payment_claimed
curl -X POST https://ndoss.vercel.app/api/v1/manual-payments/REF-4821/simulate \
  -H "Authorization: Bearer ndoss_test_VOTRE_CLE" \
  -d '{"action":"claim"}'

Vous recevez alors les deux webhooks, signés, avec "livemode": false. Quand tout fonctionne, créez une clé live.

Authentification

NDOSS utilise trois mécanismes distincts, chacun pour un sens ou un usage précis.

MécanismeSensUsage
Clé d’APIVotre app → NDOSSEn-tête Authorization: Bearer ndoss_test_… ou ndoss_live_…
Signature de webhookNDOSS → votre appProuve que le webhook vient de NDOSS et n’a pas été modifié ni rejoué
Signature d’identitéVotre serveur → widgetProuve au widget que le visiteur est bien l’utilisateur user_ref

Clés test et live

Une clé ndoss_test_… travaille dans un espace isolé : ses paiements et événements n’atteignent jamais un vrai client, et le widget ne les affiche pas. Une clé ndoss_live_… agit sur les vrais paiements. Les deux espaces ne partagent ni les références ni les clés d’idempotence.

Gardez vos clés côté serveur

NDOSS ne stocke qu’une empreinte de la clé : impossible de la retrouver. En cas de fuite, révoquez-la dans l’onglet Intégrations, l’effet est immédiat.

Le secret de signature (webhooks et identité) est unique par organisation ; il apparaît dans l’onglet Intégrations dès que l’URL du webhook est enregistrée, et peut être régénéré.

Événements entrants

POST/api/v1/events
Toutes les URL de l’API sont préfixées par /api/v1. Un événement par requête, corps JSON de 64 Ko maximum.

L’enveloppe, identique pour tous les événements

Exemple : payment.failed

{
  "type": "payment.failed",
  "idempotency_key": "evt_unique_de_votre_app",
  "user_ref": "id_utilisateur_dans_votre_app",
  "occurred_at": "2026-09-23T09:29:00Z",
  "data": {
    "amount": 5000,
    "currency": "XAF",
    "reference": "REF-4821",
    "reference_label": "Motif du transfert",
    "reason": "insufficient_balance",
    "product": { "id": "pack-120", "name": "Pack 120 crédits" },
    "customer": { "first_name": "Kewsia", "email": "kewsia@exemple.com", "phone": "+24106000000" }
  },
  "metadata": {}
}

Ce qui varie selon votre application va dans data ou metadata, jamais dans les champs standards.

ChampTypeDescription
typestringObligatoire. Un type du catalogue, ou custom.<nom>.
idempotency_keystringObligatoire. 1 à 128 caractères ASCII sans espace, unique par événement. Rejouable sans risque.
user_refstringObligatoire. L’identifiant de l’utilisateur dans votre app (200 caractères max).
occurred_atISO 8601Facultatif. Refusé s’il est dans le futur (5 min de tolérance).
dataobjetLe contenu de l’événement (voir ci-dessous).
metadataobjetFacultatif. Libre, conservé tel quel dans l’historique.

Catalogue des types

TypeEffet dans NDOSS
payment.failedOuvre un paiement direct au statut « proposé » pour cet utilisateur.
payment.succeededSi data.reference correspond à un paiement direct ouvert, le passe à « validé ».
checkout.abandonedMême effet que payment.failed (mêmes champs data obligatoires), avec le message « paiement abandonné » côté client.
custom.<nom>Espace libre (ex : custom.subscription_expiring). Enregistré dans l’historique. Nom en minuscules, chiffres et _.

Champs de data pour payment.failed et checkout.abandoned

ChampTypeDescription
referencestringObligatoire. 1 à 64 caractères. Chaîne opaque : NDOSS n’impose aucun format ; elle sert à identifier le paiement dans vos webhooks et votre historique.
reference_labelstringFacultatif, 80 caractères max. Libellé de la référence, conservé et renvoyé dans vos webhooks. Il n’est pas affiché au client.
amountentierObligatoire. De 1 à 100 000 000, en unité entière de la devise (pas de décimales).
currencystringObligatoire. Code ISO 4217 en majuscules (XAF).
reasonstringFacultatif. Un code de motif normalisé (voir plus bas). Toute autre valeur devient unknown.
product.id, product.namestringFacultatifs, 200 caractères max.
customer.first_name, email, phonestringFacultatifs. Le téléphone accepte + chiffres espaces ( ) -.

Une référence par paiement

Dans un même espace (test ou live), une reference désigne un seul paiement. Renvoyer un payment.failed avec une référence déjà connue n’écrase rien.

Codes de motif

Chaque processeur de paiement a ses propres erreurs. NDOSS ne les connaît pas : c’est à votre application (ou à votre connecteur) de traduire l’erreur du processeur vers cette liste courte. NDOSS peut ainsi adapter son message sans dépendre d’un processeur.

CodeSignification
insufficient_balanceSolde ou plafond insuffisant.
provider_errorErreur côté opérateur ou processeur.
timeoutLe client n’a pas validé à temps, ou l’opérateur n’a pas répondu.
cancelled_by_userLe client a annulé lui-même.
unknownMotif inconnu ou non traduisible. Valeur par défaut.

Exemple : une erreur générique du type UNSPECIFIED_FAILURE chez Chariow se traduit par unknown.

Paiement direct

Quand un paiement échoue, le client peut choisir de payer directement par Mobile Money sur les coordonnées que vous avez enregistrées dans NDOSS (Moyens de paiement). Le paiement suit ce cycle :

StatutDéclencheurQui l’envoie
proposedÉchec reçu (payment.failed)Votre app → NDOSS
pendingLe client accepte et reçoit les instructionsNDOSS → votre app : manual_payment.requested
declaredLe client clique « J’ai payé »NDOSS → votre app : customer.payment_claimed
validatedVous avez vérifié le paiement (ou payment.succeeded reçu)Votre app → NDOSS
refusedVous refusez le paiementVotre app → NDOSS
expiredAucun règlement dans les 7 jours, ou vous le clôturezNDOSS (automatique) ou votre app

Le clic « J’ai payé » ne prouve rien

C’est un signal, pas une preuve. Vérifiez toujours le paiement réel (relevé Mobile Money) avant de valider. NDOSS ne confirme jamais un paiement à votre place.

Enregistrer votre décision

PATCH/api/v1/manual-payments/{reference}
Corps : { "decision": "validated" | "refused" | "expired" }

curl

curl -X PATCH https://ndoss.vercel.app/api/v1/manual-payments/REF-4821 \
  -H "Authorization: Bearer ndoss_live_VOTRE_CLE" \
  -H "Content-Type: application/json" \
  -d '{"decision":"validated"}'
RéponseSens
200 { ok, status }Décision enregistrée. Renvoyer la même décision renvoie aussi 200.
404 not_foundAucun paiement avec cette référence dans cet espace (test/live).
409 invalid_stateUne décision finale différente est déjà enregistrée : elle ne peut plus être contredite.

Simuler le client (clé test uniquement)

POST/api/v1/manual-payments/{reference}/simulate
Corps : { "action": "request" | "claim" }. Répond 403 avec une clé live.

Elle joue à la place du client pour que vous receviez vos webhooks sans client réel :request équivaut à « Payer directement », claim à « J’ai payé ». Les transitions sont contrôlées : claim avant request renvoie 409.

Paiement depuis le chat

Quand un client commande dans le chat, NDOSS appelle un endpoint de votre application pour obtenir son lien de paiement personnel, puis l’envoie dans la conversation. Vous gardez toute votre logique (compte, processeur de paiement, crédits) : NDOSS ne fait que recueillir l’email et le téléphone, et transporter le lien.

  1. Le client choisit une offre. L’agent lui demande son email puis son téléphone, les répète pour confirmation et enregistre la commande.
  2. NDOSS appelle votre endpoint (ci-dessous) avec une reference unique.
  3. Vous répondez avec un lien ; NDOSS l’envoie dans le chat (jamais écrit par l’IA elle-même).
  4. Le client paie. Vous renvoyez le résultat à NDOSS avec la même référence, par POST /api/v1/events.

Configuration : Intégrations → Webhook & événements → Configuration → « Création de lien de paiement », avec un bouton Tester qui rejoue un appel en mode test et affiche votre réponse.

L’endpoint que vous exposez

POSThttps://votre-app.example/api/ndoss/checkout
HTTPS obligatoire, adresse publique. Signé comme les webhooks : X-NDOSS-Timestamp, X-NDOSS-Signature, X-NDOSS-Event-Id (= la référence). Vérifiez la signature comme décrit dans « Webhooks sortants ».

Requête envoyée par NDOSS

{
  "reference": "ND-MG0K2A-7QXP",
  "livemode": true,
  "product": { "id": "pack-120", "name": "Pack 120 crédits" },
  "amount": 5000,
  "currency": "XAF",
  "customer": {
    "first_name": "Kewsia",
    "email": "kewsia@exemple.com",
    "phone": "074000000"
  },
  "customer_ip": "203.0.113.42"
}
ChampTypeDescription
referencestringNuméro de commande NDOSS (64 caractères max). Clé d’idempotence : à renvoyer telle quelle dans vos événements.
livemodebooléenfalse = essai : ne créez ni vente, ni compte, ni paiement.
product.idstringIdentifiant de l’offre dans NDOSS (champ « identifiant externe » de l’offre).
amount, currencyentier, stringPrix attendu par NDOSS. Sert de contrôle : refusez si votre catalogue diffère (409).
customer.email, customer.phonestringValidés côté NDOSS avant l’appel. Le téléphone est transmis tel que saisi par le client.
customer.first_namestringFacultatif : absent quand le client ne s’est pas présenté.
customer_ipstringFacultatif : IP du visiteur, à transmettre à votre processeur si utile.
discount_codestringFacultatif : code promo que le client a donné dans le chat (majuscules, lettres, chiffres, - et _). À valider chez vous : NDOSS ne le juge jamais.

Votre réponse (200)

{
  "checkout_url": "https://votre-app.example/pay/resume?ref=ND-MG0K2A-7QXP",
  "reference": "ND-MG0K2A-7QXP",
  "user_ref": "id_du_compte_dans_votre_app"
}
ChampTypeDescription
checkout_urlstringObligatoire, en https. Le lien doit ouvrir la caisse de paiement, pas une page catalogue : le client a déjà donné ses informations.
referencestringFacultatif, écho de la référence.
user_refstringFacultatif : identifiant du compte créé ou retrouvé chez vous.
amountentierFacultatif : prix final après remise, si un code a été appliqué. NDOSS le retient comme montant de la commande.

Lien de reprise

Si le jeton de votre processeur de paiement périme vite, renvoyez un lien de reprise hébergé chez vous : à chaque clic, votre serveur rouvre une caisse fraîche et redirige vers elle. NDOSS rejoue ce même lien quand le visiteur revient (bouton « Reprendre mon paiement »).
StatuterrorCause
400validation_errorChamp invalide ou manquant (indiquez field et detail).
401invalid_signatureSignature ou timestamp invalide.
404unknown_productproduct.id inconnu chez vous.
409price_mismatchVotre catalogue annonce un autre prix que amount (le prix avant remise).
422invalid_discount_codeLe code promo est inconnu, expiré ou déjà utilisé. NDOSS refait l’appel sans code et prévient le client : le lien est au prix normal.
429 / 5xx—NDOSS n’envoie pas de lien ; le client reçoit un message l’invitant à réessayer.

Idempotence : rappelé avec la même reference, votre endpoint renvoie le même lien — ni deuxième paiement, ni deuxième compte. Délai d’appel côté NDOSS : 10 secondes.

Node.js (Express)

// Votre endpoint, appelé par NDOSS (verifyNdossWebhook : voir « Webhooks sortants »)
app.post('/api/ndoss/checkout', express.raw({ type: '*/*' }), async (req, res) => {
  if (!verifyNdossWebhook(req.body, req.headers, process.env.NDOSS_SECRET)) {
    return res.status(401).json({ error: 'invalid_signature' });
  }
  const b = JSON.parse(req.body);

  const product = catalog[b.product.id];
  if (!product) return res.status(404).json({ error: 'unknown_product', field: 'product.id' });
  if (product.price !== b.amount) return res.status(409).json({ error: 'price_mismatch', field: 'amount' });

  // Mode test : ni vente, ni compte, ni appel au processeur de paiement
  if (!b.livemode) {
    return res.json({ checkout_url: `${SITE}/test-checkout/${b.reference}`, reference: b.reference });
  }

  // Idempotent sur b.reference : le même appel renvoie le même paiement, jamais un doublon
  const payment = await findOrCreatePayment(b.reference, b);
  res.json({
    checkout_url: `${SITE}/pay/resume?ref=${b.reference}`, // doit MENER À LA CAISSE, pas à une page catalogue
    reference: b.reference,
    user_ref: payment.userRef,
  });
});

Codes promo

Le client peut donner un code promo dans le chat (par exemple un code gagné à une roue de la chance sur votre site). L’agent ne propose, n’invente ni ne révèle jamais de code : il recopie celui que le client écrit et le transmet dans discount_codeau moment de créer le lien. Votre endpoint le valide auprès de votre processeur.

  • amount reste le prix catalogue : c’est lui que vous comparez à votre catalogue (409 price_mismatch).
  • Code accepté : renvoyez le prix final dans amount de la réponse. À défaut, NDOSS le retient à la réception de payment.succeeded.
  • Code refusé : répondez 422 invalid_discount_code. NDOSS refait l’appel sans code, envoie le lien au prix normal et le dit au client (message configurable « Code promo non valide »).
  • Dans vos événements de retour, data.amount est le montant réellement payé. NDOSS accepte un montant inférieur seulement si un code a été appliqué à la commande, jamais un montant supérieur ; la commande et le total dépensé du client reflètent alors ce qui a été payé.

Le retour de paiement

Renvoyez à NDOSS l’issue du paiement avec la référence reçue, par POST /api/v1/events :

Votre événementEffet sur la commandeAffichage
payment.succeededPasse à « payée » (le client devient « Client », son total dépensé augmente)Toast « Paiement confirmé » et carte confirmée dans le widget
payment.failedÉtat « Paiement échoué », paiement direct proposéCarte : « Payer directement » (Mobile Money) et « Reprendre mon paiement »
checkout.abandonedÉtat « Abandonné », paiement direct proposéMême carte, plus une notification au-dessus du lanceur

curl

curl -X POST https://ndoss.vercel.app/api/v1/events \
  -H "Authorization: Bearer ndoss_live_VOTRE_CLE" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "payment.succeeded",
    "idempotency_key": "sale_9f2c1e",
    "user_ref": "id_du_compte_dans_votre_app",
    "data": { "reference": "ND-MG0K2A-7QXP", "amount": 5000, "currency": "XAF" }
  }'
  • payment.succeeded avec data.amount différent du montant de la commande est refusé et signalé, la commande reste en attente.
  • Un événement tardif ne défait jamais un paiement déjà encaissé, et rejouer payment.succeeded ne compte pas la vente deux fois.
  • Sans nouvelle de votre part, une commande dont le lien est resté sans paiement plus de 2 heures passe « Sans suite ».

Récupérer la vente : le paiement direct

Si la commande n’aboutit pas (abandon, échec, ou aucun paiement au bout de 2 heures), le widget aide le client à payer quand même. Sa carte propose deux voies : reprendre le lien, ou payer directement par Mobile Money sur les coordonnées configurées dans NDOSS (Moyens de paiement). Les textes sont ceux des messages configurables (« Paiement abandonné », « Instructions de paiement direct »…). Le cycle est celui du paiement direct :

  1. Le client clique « Payer directement » : NDOSS affiche vos coordonnées et le montant exact, et vous envoie le webhook manual_payment.requested.
  2. Il clique « J’ai payé » : vous recevez customer.payment_claimed.
  3. Vous vérifiez le paiement réel, puis appelez PATCH /api/v1/manual-payments/{reference} avec validated (ou refused).

La reference de ces événements est celle de la commande (ND-…) et le user_ref celui que vous avez renvoyé avec le lien (à défaut, l’email du client). Valider le paiement direct règle la commande d’origine : elle passe à « payée » sans créer une seconde vente. Un payment.succeeded tardif sur la même référence la règle aussi et referme le paiement direct. Le client n’a rien à saisir.

Quand la commande est payée, la conversation du client passe dans l’onglet « Terminé » avec un dernier message de confirmation. Une conversation sans message depuis 24 heures y passe aussi.

Utilisez la clé live pour les vraies commandes

Les événements envoyés avec une clé test sont enregistrés dans l’historique mais n’agissent jamais sur une vraie commande : une commande réelle ne passe donc à « payée » ou « abandonnée » que si vous utilisez la clé live. Prévoyez deux configurations (test et production) côté application.

Où voir les commandes

Les commandes prises par l’agent apparaissent dans la page Ventes avec la pastille « Chat IA » et leur état de paiement (Lien envoyé, Abandonné, Paiement échoué, Sans suite, ou payée). Un paiement direct Mobile Money validé y apparaît avec la pastille « Paiement direct ».

Webhooks sortants

NDOSS envoie une requête POST à l’URL configurée quand le client agit. Chaque app peut relayer ces notifications vers son propre outil d’administration (bot, email, Slack…).

TypeQuanddata
manual_payment.requestedLe client a accepté le paiement directreference, reference_label, user_ref, amount, currency
customer.payment_claimedLe client a cliqué « J’ai payé »reference, user_ref, amount, currency

Corps d’un webhook

{
  "id": "evt_5c2f0a5e-6a0b-4c2a-9a55-2b6f6f9a1d10",
  "idempotency_key": "evt_5c2f0a5e-6a0b-4c2a-9a55-2b6f6f9a1d10",
  "type": "manual_payment.requested",
  "livemode": true,
  "occurred_at": "2026-09-23T09:41:12.000Z",
  "data": {
    "reference": "REF-4821",
    "reference_label": "Motif du transfert",
    "user_ref": "id_utilisateur_dans_votre_app",
    "amount": 5000,
    "currency": "XAF"
  }
}

En-têtes

En-têteContenu
X-NDOSS-Signaturesha256=<HMAC-SHA256 hexadécimal de "<timestamp>.<corps brut>" avec votre secret>
X-NDOSS-TimestampSecondes Unix au moment de l’envoi. Recalculé à chaque nouvel essai.
X-NDOSS-Event-IdIdentique au champ id du corps. Sert à dédupliquer.

Vérifier la signature

Calculez la signature sur le corps brut reçu, avant de le parser. Refusez la requête si la signature diffère ou si le timestamp s’écarte de plus de 5 minutes : cela bloque le rejeu d’une requête interceptée.

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

// rawBody : le corps EXACT reçu (Buffer/string), avant tout JSON.parse
export function verifyNdossWebhook(rawBody, headers, secret) {
  const ts = Number(headers['x-ndoss-timestamp']);
  if (!Number.isInteger(ts) || Math.abs(Date.now() / 1000 - ts) > 300) return false;

  const expected = 'sha256=' + createHmac('sha256', secret).update(`${ts}.${rawBody}`).digest('hex');
  const received = Buffer.from(headers['x-ndoss-signature'] ?? '');
  const wanted = Buffer.from(expected);
  return received.length === wanted.length && timingSafeEqual(received, wanted);
}

// Express : app.post('/webhooks/ndoss', express.raw({ type: '*/*' }), (req, res) => { ... })
// Dédupliquez ensuite sur le champ "id" : un même événement peut arriver plusieurs fois.

Livraison et nouvelles tentatives

  • Répondez 2xx rapidement (délai maximal 5 secondes). Toute autre réponse, ou un délai dépassé, compte comme un échec.
  • Jusqu’à 6 essais au total, espacés de 1 min, 5 min, 30 min, 3 h puis 12 h.
  • Les essais suivants partent lors de la prochaine activité de votre organisation dans NDOSS (événement reçu, décision, visite du widget, historique consulté) : ils ne sont pas garantis à la minute près.
  • Un même événement peut donc vous parvenir plusieurs fois : dédupliquez sur id.
  • L’URL doit être en HTTPS et publique. Les adresses locales ou privées sont refusées, et NDOSS ne suit pas les redirections.
  • Les livraisons en échec se rejouent à la main depuis Intégrations → Historique → Webhooks envoyés.

Widget

Le widget de chat NDOSS peut être installé sur votre site ou votre application web. Activez-le dans la page Widgets du tableau de bord, puis collez le script. Pour proposer le paiement direct à un utilisateur connecté, votre serveur doit l’identifier avec une signature.

Apparence et comportement

  • Mobile : le widget prend tout l’écran et suit la zone réellement visible ; quand le clavier s’ouvre, l’en-tête reste en place et la saisie remonte au-dessus du clavier. Le défilement de votre page est verrouillé pendant l’ouverture.
  • Ordinateur : fenêtre flottante en bas à droite ou à gauche (data-position="left").
  • Accueil : message d’accueil, délai de réponse et ressources utiles (jusqu’à 6 liens en https ou mailto), configurables dans la page Widgets du tableau de bord. Aucun emoji : des icônes.
  • Notifications : quand un client a une commande ou un paiement en attente, une notification glisse au-dessus du lanceur (fermeture automatique après 9 secondes, croix, non réaffichée après fermeture pendant la session ; un clic ouvre le chat).
  • Page produit : data-product-id affiche un bandeau « Besoin d’aide ? » et précharge le produit.

1. Signer l’identité côté serveur

À chaque rendu de page, signez identify:<user_ref>.<exp> avec le secret de l’organisation. exp est un timestamp Unix dans le futur, à 24 heures maximum : une signature volée cesse de fonctionner d’elle-même.

import { createHmac } from 'node:crypto';

// À exécuter sur VOTRE serveur, à chaque rendu de page pour l'utilisateur connecté
export function ndossIdentity(userRef, secret) {
  const exp = Math.floor(Date.now() / 1000) + 3600; // 24 h maximum
  const signature = 'sha256=' + createHmac('sha256', secret).update(`identify:${userRef}.${exp}`).digest('hex');
  return { userRef, exp, signature };
}

2. Passer l’identité au script

HTML

<script
  src="https://ndoss.vercel.app/widget.js"
  data-org="VOTRE_ID_ORGANISATION"
  data-user-ref="<?= $userRef ?>"
  data-user-exp="<?= $exp ?>"
  data-user-signature="<?= $signature ?>"
></script>

Le user_ref doit être exactement celui que vous envoyez dans vos événements. Il transite par postMessage vers le widget, jamais dans une URL.

Sans signature valide, aucun accès

Le widget ne montre les paiements d’un utilisateur que si la signature est valide. Un visiteur ne peut pas deviner ni choisir un user_ref : la vérification se fait toujours côté serveur NDOSS.

Ce que voit le client

  1. Une carte « Paiement en attente » avec le montant. Le bouton « Payer directement » n’apparaît que si vous avez enregistré un moyen Mobile Money ou virement actif.
  2. Après clic : vos coordonnées de paiement et le montant exact à envoyer. La référence reste interne : le client n’a rien à saisir, vous la recevez dans les webhooks.
  3. Après « J’ai payé » : un message de vérification en cours. Le paiement reste « déclaré » jusqu’à votre décision.

Messages au client

Tout ce que le client lit dans le widget est configurable par organisation, dans Intégrations → Messages : rien n’est figé dans l’application. Chaque état du paiement a son titre, son texte, le libellé de son bouton et une bulle d’appel sur le lanceur du widget. Des textes par défaut en français sont fournis ; vous les surchargez (ton, langue, marque) et pouvez revenir aux défauts à tout moment.

ÉtatQuandBouton par défaut
Paiement échouépayment.failed reçuPayer directement
Paiement abandonnécheckout.abandoned reçuPayer directement
Paiement direct indisponibleÉchec ou abandon, sans moyen de paiement direct configuréaucun
Instructions de paiement directLe client a accepté : ses coordonnées s’affichent sous le texteJ’ai payé
« J’ai payé » reçuLe client a déclaré avoir payéaucun
Paiement validéVous avez validé (visible 3 jours)aucun
Paiement refuséVous avez refusé (visible 3 jours)aucun
Paiement expiréDélai dépassé (visible 3 jours)aucun
Lien de paiement d’une commandeEnvoyé dans le chat après une commande (variable {link})aucun
Lien de paiement indisponibleCommande enregistrée mais lien non obtenuaucun
Commande en attente de paiementLe visiteur revient sans avoir payéReprendre mon paiement
Commande payéePaiement confirmé (visible 3 jours)aucun

Variables

VariableRemplacée par
{first_name}customer.first_name de l’événement (vide si absent : « Bonjour, » sans nom)
{amount}Montant formaté (5 000)
{currency}Devise ; XAF et XOF s’affichent FCFA
{product}product.name, ou « votre commande » si absent
{reason}La phrase configurée pour le code de motif (voir Codes de motif) ; vide pour unknown par défaut
{link}Le lien de paiement de la commande (uniquement pour « Lien de paiement d’une commande »)

Une variable inconnue est refusée à l’enregistrement, pour éviter une faute de frappe qui s’afficherait telle quelle chez le client. Un bouton laissé vide n’est pas affiché ; une bulle laissée vide non plus.

Ce que le client voit

Quand un paiement attend une action du client, une bulle s’affiche sur le lanceur du widget (« Un souci de paiement ? Réglez directement ») et une carte avec un bouton en évidence est épinglée en haut du chat et de l’accueil. Un clic sur le bouton fait avancer le paiement ; les coordonnées de paiement apparaissent alors dans la carte.

Erreurs et limites

Les erreurs de l’API ont toutes la forme { "error": "code", "field": "…", "detail": "…" } (field et detail pour les erreurs de validation).

StatuterrorCause
202—Événement accepté, ou clé d’idempotence déjà reçue (duplicate: true). Jamais une erreur.
400validation_errorChamp invalide : field indique lequel, detail pourquoi. Rien n’est enregistré.
401invalid_api_keyClé absente, inconnue ou révoquée.
403test_key_requiredEndpoint réservé aux clés test.
404not_foundRéférence inconnue dans cet espace.
409invalid_stateTransition impossible depuis le statut actuel.
413payload_too_largeCorps supérieur à 64 Ko.
429rate_limitedTrop de requêtes. Respectez l’en-tête Retry-After.
5xx—Erreur NDOSS : renvoyez la même requête avec le même idempotency_key.

Limites de débit

PortéeLimite
Événements, par clé120 par minute
Décisions, par clé120 par minute
Simulations, par clé30 par minute
Authentifications échouées, par IP20 par minute

Idempotence et nouvelles tentatives

Générez un idempotency_key stable pour chaque événement métier (par exemple pay_failed_<id du paiement>) et renvoyez-le tel quel après une erreur réseau ou un 5xx : le résultat est identique, sans doublon. Si NDOSS échoue en cours de traitement, la clé est libérée pour que votre nouvel envoi refasse le travail.

Conservation des données

Les événements reçus sont conservés 90 jours, les livraisons de webhooks 30 jours. Une clé d’idempotence n’est donc reconnue que pendant 90 jours. L’historique des changements de statut d’un paiement reste associé au paiement.

Exemples complets

Envoyer un échec de paiement

curl -X POST https://ndoss.vercel.app/api/v1/events \
  -H "Authorization: Bearer ndoss_test_VOTRE_CLE" \
  -H "Content-Type: application/json" \
  -d '{ "type": "payment.failed", "idempotency_key": "evt_unique_de_votre_app", "user_ref": "id_utilisateur_dans_votre_app", "occurred_at": "2026-09-23T09:29:00Z", "data": { "amount": 5000, "currency": "XAF", "reference": "REF-4821", "reference_label": "Motif du transfert", "reason": "insufficient_balance", "product": { "id": "pack-120", "name": "Pack 120 crédits" }, "customer": { "first_name": "Kewsia", "email": "kewsia@exemple.com", "phone": "+24106000000" } }, "metadata": {} }'

Recevoir et vérifier un webhook

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

// rawBody : le corps EXACT reçu (Buffer/string), avant tout JSON.parse
export function verifyNdossWebhook(rawBody, headers, secret) {
  const ts = Number(headers['x-ndoss-timestamp']);
  if (!Number.isInteger(ts) || Math.abs(Date.now() / 1000 - ts) > 300) return false;

  const expected = 'sha256=' + createHmac('sha256', secret).update(`${ts}.${rawBody}`).digest('hex');
  const received = Buffer.from(headers['x-ndoss-signature'] ?? '');
  const wanted = Buffer.from(expected);
  return received.length === wanted.length && timingSafeEqual(received, wanted);
}

// Express : app.post('/webhooks/ndoss', express.raw({ type: '*/*' }), (req, res) => { ... })
// Dédupliquez ensuite sur le champ "id" : un même événement peut arriver plusieurs fois.

Identifier l’utilisateur du widget

import { createHmac } from 'node:crypto';

// À exécuter sur VOTRE serveur, à chaque rendu de page pour l'utilisateur connecté
export function ndossIdentity(userRef, secret) {
  const exp = Math.floor(Date.now() / 1000) + 3600; // 24 h maximum
  const signature = 'sha256=' + createHmac('sha256', secret).update(`identify:${userRef}.${exp}`).digest('hex');
  return { userRef, exp, signature };
}

Cas Atek de bout en bout

Atek est la première intégration. Elle dispose de son propre bot Telegram d’administration : c’est Atek qui le fait tourner, NDOSS n’en héberge aucune partie. NDOSS transporte seulement des faits ; le tri des paiements et la décision restent chez Atek.

  1. Échec. Le paiement d’un utilisateur échoue chez Atek. Atek crée sa référence et envoie payment.failed à NDOSS (statut « proposé »).
  2. Le client accepte. Dans le widget, il clique « Payer directement » : NDOSS affiche vos coordonnées Mobile Money et le montant exact à envoyer (sans rien demander de plus au client), et envoie manual_payment.requested à Atek.
  3. Le bot est prévenu. Atek enregistre ce paiement en attente et notifie le bot Telegram (nom, produit, montant, référence), avec les boutons Valider et Refuser. Une commande du type /attente peut lister tous les paiements en attente, triés par date, pour rapprocher un SMS Airtel ou Moov par montant ou par référence.
  4. Le client déclare avoir payé. NDOSS envoie customer.payment_claimed : la ligne correspondante passe en priorité dans le bot, sans nouvelle notification.
  5. Vérification. L’administrateur contrôle son relevé Mobile Money, puis appuie sur Valider ou Refuser.
  6. Décision. Atek active les crédits et appelle PATCH /api/v1/manual-payments/{reference} avec validated (ou refused) ; il peut aussi envoyer payment.succeeded avec la même référence.
  7. Expiration. Sans règlement dans le délai, Atek clôture la commande (expired). NDOSS expire de toute façon un paiement 7 jours après l’échec.

Deux précautions pour le bot

Limitez les boutons Valider et Refuser à l’identifiant Telegram de l’administrateur : si le bot est un jour ajouté à un groupe, personne d’autre ne doit pouvoir valider un paiement. Et gardez la vérification du relevé avant chaque validation : le clic du client ne prouve rien.

Côté Atek, la référence (ATK-… ou tout autre format) reste libre. Elle n’est jamais demandée au client : NDOSS la conserve et vous la renvoie dans manual_payment.requested et customer.payment_claimed, ce qui permet de rapprocher le virement par montant et par référence.

Limites connues

  • NDOSS n’envoie aucun message proactif au client (WhatsApp, SMS, email) après un échec : le paiement direct est proposé dans le widget, quand il l’ouvre.
  • Le mode test isole les données mais n’émet aucun vrai message : seuls vos webhooks de test partent.
  • Les limites de débit sont appliquées en mémoire par instance du serveur : elles freinent un flot simple, pas un abus distribué.
  • Les nouvelles tentatives de webhook dépendent de l’activité de l’organisation (pas de tâche planifiée) : prévoyez, côté app, un rapprochement de vos paiements en attente.

Une question ou un cas non couvert ? Contactez-nous.