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écanisme | Sens | Usage |
|---|---|---|
| Clé d’API | Votre app → NDOSS | En-tête Authorization: Bearer ndoss_test_… ou ndoss_live_… |
| Signature de webhook | NDOSS → votre app | Prouve que le webhook vient de NDOSS et n’a pas été modifié ni rejoué |
| Signature d’identité | Votre serveur → widget | Prouve 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
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
/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.
| Champ | Type | Description |
|---|---|---|
| type | string | Obligatoire. Un type du catalogue, ou custom.<nom>. |
| idempotency_key | string | Obligatoire. 1 à 128 caractères ASCII sans espace, unique par événement. Rejouable sans risque. |
| user_ref | string | Obligatoire. L’identifiant de l’utilisateur dans votre app (200 caractères max). |
| occurred_at | ISO 8601 | Facultatif. Refusé s’il est dans le futur (5 min de tolérance). |
| data | objet | Le contenu de l’événement (voir ci-dessous). |
| metadata | objet | Facultatif. Libre, conservé tel quel dans l’historique. |
Catalogue des types
| Type | Effet dans NDOSS |
|---|---|
| payment.failed | Ouvre un paiement direct au statut « proposé » pour cet utilisateur. |
| payment.succeeded | Si data.reference correspond à un paiement direct ouvert, le passe à « validé ». |
| checkout.abandoned | Mê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
| Champ | Type | Description |
|---|---|---|
| reference | string | Obligatoire. 1 à 64 caractères. Chaîne opaque : NDOSS n’impose aucun format ; elle sert à identifier le paiement dans vos webhooks et votre historique. |
| reference_label | string | Facultatif, 80 caractères max. Libellé de la référence, conservé et renvoyé dans vos webhooks. Il n’est pas affiché au client. |
| amount | entier | Obligatoire. De 1 à 100 000 000, en unité entière de la devise (pas de décimales). |
| currency | string | Obligatoire. Code ISO 4217 en majuscules (XAF). |
| reason | string | Facultatif. Un code de motif normalisé (voir plus bas). Toute autre valeur devient unknown. |
| product.id, product.name | string | Facultatifs, 200 caractères max. |
| customer.first_name, email, phone | string | Facultatifs. Le téléphone accepte + chiffres espaces ( ) -. |
Une référence par paiement
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.
| Code | Signification |
|---|---|
| insufficient_balance | Solde ou plafond insuffisant. |
| provider_error | Erreur côté opérateur ou processeur. |
| timeout | Le client n’a pas validé à temps, ou l’opérateur n’a pas répondu. |
| cancelled_by_user | Le client a annulé lui-même. |
| unknown | Motif 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 :
| Statut | Déclencheur | Qui l’envoie |
|---|---|---|
| proposed | Échec reçu (payment.failed) | Votre app → NDOSS |
| pending | Le client accepte et reçoit les instructions | NDOSS → votre app : manual_payment.requested |
| declared | Le client clique « J’ai payé » | NDOSS → votre app : customer.payment_claimed |
| validated | Vous avez vérifié le paiement (ou payment.succeeded reçu) | Votre app → NDOSS |
| refused | Vous refusez le paiement | Votre app → NDOSS |
| expired | Aucun règlement dans les 7 jours, ou vous le clôturez | NDOSS (automatique) ou votre app |
Le clic « J’ai payé » ne prouve rien
Enregistrer votre décision
{ "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éponse | Sens |
|---|---|
| 200 { ok, status } | Décision enregistrée. Renvoyer la même décision renvoie aussi 200. |
| 404 not_found | Aucun paiement avec cette référence dans cet espace (test/live). |
| 409 invalid_state | Une décision finale différente est déjà enregistrée : elle ne peut plus être contredite. |
Simuler le client (clé test uniquement)
{ "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.
- 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.
- NDOSS appelle votre endpoint (ci-dessous) avec une
referenceunique. - Vous répondez avec un lien ; NDOSS l’envoie dans le chat (jamais écrit par l’IA elle-même).
- 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
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"
}| Champ | Type | Description |
|---|---|---|
| reference | string | Numéro de commande NDOSS (64 caractères max). Clé d’idempotence : à renvoyer telle quelle dans vos événements. |
| livemode | booléen | false = essai : ne créez ni vente, ni compte, ni paiement. |
| product.id | string | Identifiant de l’offre dans NDOSS (champ « identifiant externe » de l’offre). |
| amount, currency | entier, string | Prix attendu par NDOSS. Sert de contrôle : refusez si votre catalogue diffère (409). |
| customer.email, customer.phone | string | Validés côté NDOSS avant l’appel. Le téléphone est transmis tel que saisi par le client. |
| customer.first_name | string | Facultatif : absent quand le client ne s’est pas présenté. |
| customer_ip | string | Facultatif : IP du visiteur, à transmettre à votre processeur si utile. |
| discount_code | string | Facultatif : 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"
}| Champ | Type | Description |
|---|---|---|
| checkout_url | string | Obligatoire, en https. Le lien doit ouvrir la caisse de paiement, pas une page catalogue : le client a déjà donné ses informations. |
| reference | string | Facultatif, écho de la référence. |
| user_ref | string | Facultatif : identifiant du compte créé ou retrouvé chez vous. |
| amount | entier | Facultatif : prix final après remise, si un code a été appliqué. NDOSS le retient comme montant de la commande. |
Lien de reprise
| Statut | error | Cause |
|---|---|---|
| 400 | validation_error | Champ invalide ou manquant (indiquez field et detail). |
| 401 | invalid_signature | Signature ou timestamp invalide. |
| 404 | unknown_product | product.id inconnu chez vous. |
| 409 | price_mismatch | Votre catalogue annonce un autre prix que amount (le prix avant remise). |
| 422 | invalid_discount_code | Le 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.
amountreste le prix catalogue : c’est lui que vous comparez à votre catalogue (409price_mismatch).- Code accepté : renvoyez le prix final dans
amountde la réponse. À défaut, NDOSS le retient à la réception depayment.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.amountest 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énement | Effet sur la commande | Affichage |
|---|---|---|
| payment.succeeded | Passe à « 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.succeededavecdata.amountdiffé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.succeededne 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 :
- Le client clique « Payer directement » : NDOSS affiche vos coordonnées et le montant exact, et vous envoie le webhook
manual_payment.requested. - Il clique « J’ai payé » : vous recevez
customer.payment_claimed. - Vous vérifiez le paiement réel, puis appelez
PATCH /api/v1/manual-payments/{reference}avecvalidated(ourefused).
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
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…).
| Type | Quand | data |
|---|---|---|
| manual_payment.requested | Le client a accepté le paiement direct | reference, reference_label, user_ref, amount, currency |
| customer.payment_claimed | Le 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ête | Contenu |
|---|---|
| X-NDOSS-Signature | sha256=<HMAC-SHA256 hexadécimal de "<timestamp>.<corps brut>" avec votre secret> |
| X-NDOSS-Timestamp | Secondes Unix au moment de l’envoi. Recalculé à chaque nouvel essai. |
| X-NDOSS-Event-Id | Identique 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-idaffiche 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
user_ref : la vérification se fait toujours côté serveur NDOSS.Ce que voit le client
- 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.
- 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.
- 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.
| État | Quand | Bouton par défaut |
|---|---|---|
| Paiement échoué | payment.failed reçu | Payer directement |
| Paiement abandonné | checkout.abandoned reçu | Payer directement |
| Paiement direct indisponible | Échec ou abandon, sans moyen de paiement direct configuré | aucun |
| Instructions de paiement direct | Le client a accepté : ses coordonnées s’affichent sous le texte | J’ai payé |
| « J’ai payé » reçu | Le 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 commande | Envoyé dans le chat après une commande (variable {link}) | aucun |
| Lien de paiement indisponible | Commande enregistrée mais lien non obtenu | aucun |
| Commande en attente de paiement | Le visiteur revient sans avoir payé | Reprendre mon paiement |
| Commande payée | Paiement confirmé (visible 3 jours) | aucun |
Variables
| Variable | Remplacé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).
| Statut | error | Cause |
|---|---|---|
| 202 | — | Événement accepté, ou clé d’idempotence déjà reçue (duplicate: true). Jamais une erreur. |
| 400 | validation_error | Champ invalide : field indique lequel, detail pourquoi. Rien n’est enregistré. |
| 401 | invalid_api_key | Clé absente, inconnue ou révoquée. |
| 403 | test_key_required | Endpoint réservé aux clés test. |
| 404 | not_found | Référence inconnue dans cet espace. |
| 409 | invalid_state | Transition impossible depuis le statut actuel. |
| 413 | payload_too_large | Corps supérieur à 64 Ko. |
| 429 | rate_limited | Trop 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ée | Limite |
|---|---|
| Événements, par clé | 120 par minute |
| Décisions, par clé | 120 par minute |
| Simulations, par clé | 30 par minute |
| Authentifications échouées, par IP | 20 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.
- Échec. Le paiement d’un utilisateur échoue chez Atek. Atek crée sa référence et envoie
payment.failedà NDOSS (statut « proposé »). - 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. - 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
/attentepeut lister tous les paiements en attente, triés par date, pour rapprocher un SMS Airtel ou Moov par montant ou par référence. - Le client déclare avoir payé. NDOSS envoie
customer.payment_claimed: la ligne correspondante passe en priorité dans le bot, sans nouvelle notification. - Vérification. L’administrateur contrôle son relevé Mobile Money, puis appuie sur Valider ou Refuser.
- Décision. Atek active les crédits et appelle
PATCH /api/v1/manual-payments/{reference}avecvalidated(ourefused) ; il peut aussi envoyerpayment.succeededavec la même référence. - 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
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.