Skip to main content
Sommaire du guide

Sommaire du guide

Temps de lecture : 20 min
#monobank#acquiring#vibecoding#ai_tools#payments
NOUVEAUDébutant20 min

Intégrer l'acquiring Monobank sans développeur grâce au vibe coding

Guide étape par étape pour intégrer les paiements en ligne Monobank sur un site web : conformité pour les travailleurs indépendants (FOP), création du terminal, vérification des webhooks ECDSA, UX fiable et tests de sécurité.

Publié :
POUR AGENT IAChatGPTClaude

Le vibecoding a radicalement transformé le développement de produits numériques. Aujourd'hui, pour intégrer sur un site la réception de paiements via Apple Pay, Google Pay ou cartes bancaires, il n'est plus nécessaire d'engager un développeur backend ni de maîtriser soi-même les protocoles cryptographiques. Il suffit de formuler clairement la logique métier à votre agent IA (Codex, Antigravity, Cursor ou Claude Code) et de lui fournir le contexte technique approprié issu de la documentation officielle.

Ce guide détaille l'intégralité du cycle de vie de l'intégration de l'acquiring en ligne Monobank dans votre projet : depuis la conformité bancaire et la configuration du tableau de bord entrepreneur jusqu'à la génération d'une passerelle de paiement sécurisée avec vérification cryptographique des signatures ECDSA dans les webhooks, la gestion des conditions de course (race conditions) dans l'interface et la mise en place d'une suite de tests automatisés obligatoires.

Astuce

Version vidéo du guide : Si vous préférez un format visuel, consultez la vidéo pratique détaillée sur YouTube →, où tout le processus est démontré en direct, de la première requête au débit réel des fonds.

Note

Pack de compétences agentiques : Pour une précision maximale de la génération de code, téléchargez le pack de connaissances officiel pour votre assistant IA :
Télécharger l'archive complète monobank-acquiring.zip (38 Ko) →


1. Architecture Hosted Checkout et cycle de vie du paiement

Monobank Acquiring fonctionne selon le schéma Hosted Checkout (paiement sur une page sécurisée de la banque). Cela signifie que votre site n'a pas besoin d'une certification de sécurité PCI DSS complexe et coûteuse, car l'utilisateur saisit les données de carte directement sur le domaine sécurisé de la banque.

La documentation officielle de Monobank pour les outils IA est disponible à l'adresse monobank.ua/api-docs/acquiring/dev/ai-tools/docs--ai-prompts →. Enregistrez ce lien comme source canonique des exigences techniques de la banque.

1.1. Flux de paiement de base

terminal
[ Client sur le site ] │ ├─ 1. Clique sur « Payer » ▼ [ Votre Serveur ] ──────────────► [ API Monobank : /invoice/create ] ▲ │ │ reçoit pageUrl, invoiceId │ └──────────────────────────────┘ │ ├─ 2. Redirige le client vers pageUrl (Hosted Checkout) ▼ [ Page de paiement Monobank ] ───► Paiement (Apple Pay / Google Pay / Carte bancaire) │ ├─ 3. Webhook POST asynchrone avec signature x-sign ▼ [ Votre Endpoint Webhook ] ──────► Vérification ECDSA SHA-256 → Statut "success" │ ├─ 4. Le client revient sur /payment-result ▼ [ Page de succès ] ──────────────► Affichage de l'accès / reçu
  • 1. Création de la facture (invoice/create) : Le client sélectionne un produit ou un tarif sur votre site. Le handler serveur envoie une requête POST à l'API Monobank avec le montant, l'objet du paiement et l'URL de retour. La banque renvoie un invoiceId unique et un lien pageUrl.
  • 2. Redirection vers la page de paiement : Le client est redirigé vers le pageUrl, où il paie la commande via Apple Pay, Google Pay, l'application Monobank ou la saisie des données de sa carte bancaire.
  • 3. Réception du Webhook : Après la finalisation du paiement, Monobank envoie automatiquement une requête POST à votre webHookUrl préalablement configuré, contenant le statut de la transaction et une signature numérique cryptographique.
  • 4. Fixation du statut (Terminal States) : Votre serveur doit réagir aux deux statuts finaux : success (paiement réussi, on accorde l'accès ou on expédie la commande) et failure (erreur ou rejet du paiement par la banque).
Important

Le statut expired (expiration du lien) ne génère pas de webhook. Si le client ferme la page de paiement sans payer, l'état de la commande doit être vérifié via un polling API de secours.


2. Prérequis juridiques et checklist de conformité du site pour les entrepreneurs individuels (FOP)

Même si le code de paiement est irréprochable, le service de surveillance financière et de sécurité de Monobank n'activera pas la réception de paiements en production si le site ne respecte pas la législation ukrainienne et les règles des systèmes de paiement Visa/Mastercard.

2.1. Prérequis bancaires

  • Compte professionnel ouvert chez Monobank (FOP ou personne morale) : L'acquiring est connecté exclusivement aux comptes professionnels. Il est interdit par la loi d'accepter des paiements commerciaux sur une carte personnelle (noire ou blanche) d'un particulier.
  • Codes NACE (KVED) appropriés : Les données d'enregistrement du FOP doivent inclure des codes d'activité pour le commerce en ligne ou les services (par exemple, 47.91 — Vente au détail par internet, 62.01/62.02 — Programmation informatique et conseil, 85.59 — Autres formes d'enseignement).

2.2. Checklist obligatoire des pages avant soumission à la modération

Avant de demander l'activation du terminal de paiement en ligne, assurez-vous que votre site contient les sections suivantes (généralement placées dans le footer) :

  • Offre publique (Contrat public) : Description de l'objet du contrat, moment de la conclusion, droits et obligations des parties.
  • Politique de confidentialité (Privacy Policy) : Formulation claire indiquant quelles données clients sont collectées et comment elles sont protégées conformément à la Loi « Sur la protection des données personnelles ».
  • Conditions de remboursement et de livraison : Procédure de retour de marchandise ou de fonds dans les 14 jours selon la Loi « Sur la protection des droits des consommateurs », et pour les produits/services numériques ou abonnements — les règles de résiliation du service.
  • Coordonnées complètes de l'entrepreneur dans le footer : Nom du FOP ou de la SARL, numéro fiscal / code EDRPOU, adresse juridique, numéro de téléphone de contact et e-mail professionnel du support.
  • Prix transparents et descriptions : Chaque bouton de paiement doit afficher un montant fixe en hryvnias (UAH) et une description claire de ce que le client achète exactement.
Avertissement

Si le site ne comporte pas d'offre publique, de coordonnées du FOP, ou si les prix sont remplacés par des mentions « à convenir », le service de sécurité de Monobank rejettera l'enregistrement du terminal lors de la phase de vérification.


3. Création d'un terminal web dans le tableau de bord Monobank Business

Pour interagir avec l'API de paiement, vous avez besoin d'une clé d'accès personnelle — X-Token. Elle est générée gratuitement dans le tableau de bord personnel de l'entrepreneur.

3.1. Algorithme pas à pas pour ouvrir la caisse

  1. Autorisation : Rendez-vous sur web.monobank.ua → et connectez-vous via QR code avec l'application Monobank sur smartphone.
  2. Accès à la caisse : Dans le menu de navigation gauche, sélectionnez la section « Caisse ».
  3. Ajout d'un outil : Cliquez sur le bouton « + Ajouter un outil » et choisissez l'option « Paiement sur site (développement propre) ».
  4. Enregistrement du terminal : Entrez le nom du projet (par exemple, Mon site ou Terminal principal) et confirmez l'opération via le bouton « Connecter ».
  5. Génération du token API : Ouvrez le terminal créé, allez dans l'onglet « Intégrations / Clés API », cliquez sur « Créer un token » et copiez la clé générée.

📍 Navigation dans le tableau de bord : web.monobank.ua → Caisse → + Ajouter un outil → Paiement sur site (développement propre)

Création d'un outil de paiement dans le tableau de bord Monobank Kasa ЗбільшитиCréation d'un outil de paiement dans le tableau de bord Monobank KasaCréation d'un outil de paiement dans le tableau de bord Monobank Kasa

📍 Obtention de la clé API : Caisse → Votre terminal → Intégration → Créer X-Token

Fenêtre modale de création et copie du token d'accès X-Token ЗбільшитиFenêtre modale de création et copie du token d'accès X-TokenFenêtre modale de création et copie du token d'accès X-Token

3.2. Règles strictes de sécurité des tokens

  • Aucun hardcoding dans le code client : Le X-Token donne un accès direct à la gestion de vos finances et aux remboursements. Il est catégoriquement interdit de le stocker dans du JavaScript/HTML public ou de le publier dans des dépôts GitHub publics.
  • Utilisation de variables d'environnement : Stockez le token uniquement dans un fichier .env sur le serveur sous le nom MONOBANK_TOKEN ou dans la section Secrets de votre plateforme d'hébergement (Vercel, Render, Railway, Replit, Lovable).
  • Token de test pour le développement : Pour les premiers essais, la banque fournit un token de test distinct sur la page api.monobank.ua →, permettant de simuler des transactions sans débit réel de fonds.

4. Prompts IA officiels Monobank pour les vibecoders

L'équipe Monobank a développé un ensemble de prompts système officiels pour les agents IA. Leur principal avantage est une correspondance exacte avec les endpoints actuels de la banque.

Page de la documentation officielle Monobank pour les outils IA ЗбільшитиPage de la documentation officielle Monobank pour les outils IAPage de la documentation officielle Monobank pour les outils IA
Avertissement

Piège principal pour les débutants — le montant en kopecks : L'API Monobank accepte tous les montants exclusivement en unités minimales de devise (kopecks). $100\text{ UAH} = 10,000\text{ kopecks}$. Si vous transmettez amount: 100, le client ne paiera que 1 hryvnia.

4.1. Prompt de base pour la création de paiement

Copiez ce prompt et envoyez-le dans le chat de votre assistant IA :

markdown
Je souhaite ajouter sur mon site la possibilité d'accepter des paiements via Monobank. TÂCHE : Rédigez un code complet et prêt à l'emploi pour créer un paiement et rediriger l'utilisateur vers la page de paiement. CE QUI DOIT SE PRODUIRE : 1. Sur mon site, il y a un bouton « Procéder au paiement ». 2. L'utilisateur clique → un paiement est créé dans Monobank. 3. L'utilisateur est redirigé vers la page de paiement Monobank. 4. Après le paiement, il revient sur mon site (https://mysite.com/payment-result). 5. Je vérifie le statut du paiement et affiche le résultat. DONNÉES DE TEST : - Montant : 100 UAH (10000 kopecks) - Description du paiement : "Paiement de commande" - URL de retour utilisateur : https://mysite.com/payment-result MON TOKEN MONOBANK : Variable d'environnement MONOBANK_TOKEN issue du fichier .env DOCUMENTATION : - Création d'un paiement : https://monobank.ua/api-docs/acquiring/methods/ia/post--api--merchant--invoice--create - Vérification du statut : https://monobank.ua/api-docs/acquiring/methods/ia/get--api--merchant--invoice--status Rédigez tout le code nécessaire pour ce flux.

4.2. Prompt pour la configuration du handler Webhook

Sans webhook, le serveur ne connaîtra pas le fait du paiement réussi si le client ferme le navigateur immédiatement après le débit :

markdown
J'ai besoin que mon serveur détecte automatiquement quand un utilisateur a payé une commande. TÂCHE : Configurer la réception automatique des informations de paiement depuis Monobank (webhook). CE QUI DOIT SE PRODUIRE : 1. L'utilisateur effectue le paiement sur la page Monobank. 2. Monobank envoie automatiquement une requête POST vers mon serveur. 3. Le serveur reçoit les données : invoiceId, statut (success/failure), montant. 4. Le serveur vérifie la signature cryptographique (ECDSA SHA-256 via l'en-tête x-sign). 5. Il met à jour le statut de la commande dans la base de données et journalise le résultat. IMPORTANT : - Validez impérativement l'en-tête x-sign à l'aide de la clé publique de la banque. - Traitez le corps brut de la requête (raw body Buffer / raw string), et non le JSON parsé. - Gérez les statuts : success, failure, processing, hold, expired. - Assurez l'idempotence : si le même webhook est reçu deux fois, ne dupliquez pas la livraison du produit. Écrivez tout le code pour un gestionnaire de webhook robuste.

5. Pack de compétences monobank-acquiring : amélioration de l'agent

Si vous vous limitez à un court prompt, l'assistant IA écrira un code basique (environ 6,8 sur 10) : le bouton fonctionnera, mais le code ne contiendra pas la vérification des signatures cryptographiques, la protection contre la falsification de prix ni la gestion des erreurs réseau rares.

Pour atteindre un niveau production (9,8–10 points), un pack de compétences spécialisé monobank-acquiring est ajouté à la racine du projet.

Audit de l'intégration de paiement : comparaison de sécurité avant et après utilisation du skill ЗбільшитиAudit de l'intégration de paiement : comparaison de sécurité avant et après utilisation du skillAudit de l'intégration de paiement : comparaison de sécurité avant et après utilisation du skill
bash
# Розпакування пакета скілів у проєкт curl -L -o monobank-acquiring.zip https://gotburnout.io/downloads/monobank-acquiring.zip unzip monobank-acquiring.zip -d monobank-acquiring/

5.1. Anatomie et structure du pack de compétences

Fichier SkillContenu et responsabilités
SKILL.mdManifeste central : flux de base, autorisation X-Token, types de données et gestion des erreurs 400, 403, 429, 500.
quickstart.mdDémarrage rapide : tutoriel pas à pas pour créer une facture et un fallback-polling avec des commandes curl prêtes à l'emploi.
invoice.mdCycle de vie des factures : endpoints de création de facture, vérification de statut, annulation et invalidation des liens.
webhook.mdProtection cryptographique : vérification mathématiquement précise de la signature ECDSA SHA-256 issue du header x-sign.
payment.mdPaiements directs : débits via token de carte sauvegardé, transactions synchrones et vérification 3DS.
wallet.mdTokenisation de cartes (Wallet) : stockage sécurisé du moyen de paiement du client dans le coffre de la banque pour des paiements en un clic.
fiscal.mdReçus et pRRO : description de la structure du panier basketOrder, calcul des taxes, remises et export des reçus en PDF.
statement.mdExtraits et analytics : obtention du registre des opérations réussies pour une période choisie avec calcul des frais bancaires.
merchant.mdDonnées marchand : obtention de la clé publique de la banque, gestion des sous-marchands et caissiers.
examples/Serveurs prêts : exemples fonctionnels de serveurs dans 6 langages populaires (Node.js, Python, Go, PHP, C#, Java).

6. Implémentation pratique : flux de paiement dynamique

Chaque projet a sa propre structure : certains vendent des consultations numériques avec des tarifs fixes, d'autres ont une boutique en ligne avec un panier dynamique, d'autres encore un simple landing page avec un seul bouton de don ou d'abonnement.

L'erreur principale des débutants est de « coder en dur » le montant (par exemple, 1000 UAH) directement dans le code client du bouton ou dans le corps de la requête POST venant du navigateur. Cette approche crée une vulnérabilité de sécurité critique.

6.1. Principe de sécurité : prix dynamique plutôt que hardcoding

  • Ne faites jamais confiance au montant venant du navigateur : Si le JavaScript client envoie { price: 1000 } au serveur, un attaquant peut, via l'inspecteur de code (DevTools) ou Postman, modifier cette valeur en { price: 1 } et acheter l'accès pour 1 hryvnia.

  • Le serveur est la seule source de vérité (Single Source of Truth) : Le frontend transmet au backend uniquement l'identifiant du produit (productId), le tarif choisi (planId: "pro") ou un tableau d'identifiants du panier (items: [{ id: "book_1", qty: 2 }]).

  • Conversion automatique en kopecks : Le serveur récupère le prix actuel depuis un fichier de configuration ou la base de données et le multiplie lui-même par 100 :

    $$\text{amount} = \text{Math.round}(\text{realPrice} \times 100)$$ Si vous changez le prix dans la base ou annoncez une remise, la facture Monobank sera générée avec le nouveau montant correct sans intervention dans la passerelle de paiement.

6.2. Prompt IA universel pour l'adaptation à tout projet

Copiez ce prompt et envoyez-le dans le chat de votre agent IA (Codex, Antigravity, Cursor ou Claude Code). L'agent scannera lui-même les fichiers de votre site, trouvera les boutons et prix existants et construira une intégration robuste :

markdown
Un package officiel de connaissances Monobank Acquiring a été ajouté à la racine de notre projet, dans le dossier /monobank-acquiring. TÂCHE : 1. Analyse l'architecture de notre projet : identifie où sont décrits nos produits, services, tarifs ou boutons de commande/paiement. 2. Crée ou intègre un point d'accès serveur sécurisé pour la création de paiement via Monobank : - Le client transmet UNIQUEMENT l'identifiant du produit/tarif (par exemple, tariffId ou productId), mais PAS le montant. - Le serveur récupère le prix réel et actuel depuis notre configuration ou base de données et le convertit en centimes (prix * 100). - Le token est extrait de manière sécurisée depuis process.env.MONOBANK_TOKEN. - Une requête POST est envoyée vers https://api.monobank.ua/api/merchant/invoice/create. - Le lien pageUrl est retourné au client pour rediriger vers la page de paiement de la banque. 3. Mets à jour les boutons de paiement existants sur le frontend : - Ajoute un état de chargement (indicateur Loader et désactivation du bouton pour éviter les clics multiples). - En cas de réponse réussie, effectue une redirection fluide de l'utilisateur vers le lien Monobank reçu. - Ajoute une gestion des erreurs avec des notifications intuitives pour l'utilisateur. 4. Crée une page de résultat de paiement (/payment-result) qui vérifie le statut de la commande et informe l'utilisateur du succès. 5. Rédige des tests unitaires pour valider le calcul dynamique des montants et la création de factures. Toutes les exigences techniques, structures de requêtes et codes de devise doivent être tirés des fichiers situés dans le dossier /monobank-acquiring (notamment SKILL.md et invoice.md).
Travail de l'agent IA dans l'IDE avec analyse de la structure et création d'un endpoint dynamique ЗбільшитиTravail de l'agent IA dans l'IDE avec analyse de la structure et création d'un endpoint dynamiqueTravail de l'agent IA dans l'IDE avec analyse de la structure et création d'un endpoint dynamique

6.3. Template architectural backend pour la création de facture

Voici des implémentations serveur éprouvées pour les deux stacks web les plus populaires :

typescript
// app/api/checkout/create-invoice/route.ts import { NextResponse } from "next/server"; const PRODUCTS_CATALOG: Record<string, { title: string; priceUah: number }> = { plan_starter: { title: "Тариф Starter", priceUah: 490 }, plan_pro: { title: "Тариф Pro", priceUah: 990 }, plan_vip: { title: "Тариф VIP", priceUah: 2490 }, }; export async function POST(req: Request) { try { const { productId } = await req.json(); // 1. Валідація: ціна формується виключно на бекенді const product = PRODUCTS_CATALOG[productId]; if (!product) { return NextResponse.json({ error: "Обраний товар або тариф не знайдено" }, { status: 400 }); } const amountInKopecks = Math.round(product.priceUah * 100); const orderReference = `order_${productId}_${Date.now()}`; const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || "https://mysite.com"; // 2. Запит до Monobank API const response = await fetch("https://api.monobank.ua/api/merchant/invoice/create", { method: "POST", headers: { "X-Token": process.env.MONOBANK_TOKEN!, "Content-Type": "application/json", }, body: JSON.stringify({ amount: amountInKopecks, ccy: 980, // Гривня (ISO 4217) merchantPaymInfo: { reference: orderReference, destination: `Оплата: ${product.title}`, comment: `Замовлення ${orderReference}`, }, redirectUrl: `${siteUrl}/payment-result?ref=${orderReference}`, webHookUrl: `${siteUrl}/api/payment/webhook`, validity: 3600, // Рахунок діє 1 годину }), }); const data = await response.json(); if (!response.ok) { return NextResponse.json({ error: data.errText || "Помилка створення рахунку в банку" }, { status: response.status }); } return NextResponse.json({ checkoutUrl: data.pageUrl, invoiceId: data.invoiceId }); } catch (error) { return NextResponse.json({ error: "Внутрішня помилка ініціалізації платежу" }, { status: 500 }); } }

7. Traitement sécurisé des webhooks et signature cryptographique ECDSA

La partie la plus critique de toute intégration financière est la vérification des notifications de paiement. Un attaquant peut envoyer une requête HTTP falsifiée à votre URL /api/payment/webhook avec un faux message de succès.

Pour éviter cela, Monobank signe chaque webhook à l'aide de l'algorithme asymétrique ECDSA (courbe secp256r1 / SHA-256) et transmet la signature dans le header HTTP x-sign.

7.1. Pourquoi JSON.stringify casse la vérification de signature

Avertissement

Piège critique du rawBody : Pour vérifier la signature ECDSA, il faut strictement le flux d'octets original envoyé par le serveur Monobank. Si vous essayez de parser le JSON puis de rappeler JSON.stringify(req.body), l'ordre des clés, les espaces ou les sauts de ligne changeront. Cela produira un hash SHA-256 différent, et la vérification de signature retournera garanti une erreur !

7.2. Implémentation de la vérification du webhook

Ci-dessous, l'implémentation correcte pour récupérer le corps brut et vérifier la signature pour Next.js App Router et Express :

typescript
// app/api/payment/webhook/route.ts import { NextResponse } from "next/server"; import crypto from "crypto"; let cachedPubKey: string | null = null; async function getMonobankPubKey(token: string): Promise<string> { if (cachedPubKey) return cachedPubKey; const res = await fetch("https://api.monobank.ua/api/merchant/pubkey", { headers: { "X-Token": token }, next: { revalidate: 86400 }, // Кешуємо публічний ключ на 24 години }); const data = await res.json(); cachedPubKey = `-----BEGIN PUBLIC KEY-----\n${data.key}\n-----END PUBLIC KEY-----`; return cachedPubKey; } export async function POST(req: Request) { const signature = req.headers.get("x-sign"); if (!signature) { return new NextResponse("Missing x-sign header", { status: 400 }); } // 1. Отримуємо сире незмінене тіло запиту у вигляді тексту const rawBody = await req.text(); try { const pubKey = await getMonobankPubKey(process.env.MONOBANK_TOKEN!); // 2. Верифікуємо підпис ECDSA SHA-256 const verifier = crypto.createVerify("SHA256"); verifier.update(rawBody); const isValid = verifier.verify(pubKey, Buffer.from(signature, "base64")); if (!isValid) { console.error("Вебхук відхилено: недійсний підпис x-sign"); return new NextResponse("Invalid signature", { status: 400 }); } // 3. Парсимо дані тільки після успішної перевірки криптографії const payload = JSON.parse(rawBody); const { invoiceId, status, amount, reference } = payload; if (status === "success") { // Тут активуємо замовлення в базі даних з перевіркою на дублікати (ідемпотентність) console.log(`Замовлення ${reference} (${invoiceId}) оплачено на суму ${amount / 100} грн`); } return new NextResponse("OK", { status: 200 }); } catch (error) { console.error("Помилка обробки вебхука:", error); return new NextResponse("Internal verification error", { status: 500 }); } }

8. Test local des webhooks (Localhost & Cloudflare Tunnels)

Problème courant chez les vibecoders : lors du lancement du serveur sur http://localhost:3000, la banque ne peut pas livrer le webhook, car la machine locale n'a pas d'adresse IP publique sur Internet.

Monobank envoie les webhooks exclusivement vers des adresses publiques avec un protocole HTTPS valide. Pour tester le cycle complet sur votre ordinateur, ouvrez un tunnel sécurisé.

8.1. Démarrage rapide du tunnel (sans inscription et gratuit)

Utilisez Cloudflare Tunnel (cloudflared) ou untun :

bash
# Миттєвий HTTPS-тунель на 3000 порт без встановлення утиліт npx untun@latest tunnel --port 3000

Après le lancement, vous obtiendrez une URL HTTPS publique temporaire, par exemple : https://your-tunnel-name.trycloudflare.com

8.2. Configuration du webhook pour le test local

Dans le code de création de facture, pendant le développement, indiquez l'URL obtenue :

typescript
webHookUrl: "https://your-tunnel-name.trycloudflare.com/api/payment/webhook"

Maintenant, lors d'un paiement de test, la banque enverra un vrai webhook directement sur votre serveur local dans le terminal, et vous pourrez vérifier que la signature x-sign passe bien la vérification.


9. UX de la page de retour et résolution de la condition de course (Race Condition)

Quand le client paie la facture via l'application Monobank ou Apple Pay, la banque le redirige vers l'URL redirectUrl (/payment-result?ref=...) immédiatement.

Cependant, le webhook réseau depuis le serveur de la banque vers votre serveur peut prendre 1 à 2 secondes de retard à cause du routage réseau. Si la page de résultat vérifie immédiatement le statut dans la base de données, elle risque d'afficher : « Commande non payée », ce qui paniquera le client (les fonds sont débités de la carte, mais le site signale l'absence de paiement).

9.1. Pattern d'ingénierie pour résoudre la race condition

  1. État initial de chargement : La page s'ouvre avec un statut neutre : « Confirmation du paiement auprès de la banque... » et un indicateur animé.
  2. Polling court (Short Polling) : Le frontend effectue jusqu'à 5 requêtes rapides vers votre propre API toutes les 1,5 secondes (/api/orders/check-status?ref=...), attendant que le webhook change le statut de la commande en success dans la base.
  3. Fallback : Si après 8 secondes le webhook n'est pas arrivé, le client voit le message : « Paiement accepté en traitement. Dès que la banque confirmera la transaction, l'accès s'ouvrira automatiquement ».

9.2. Composant React prêt à l'emploi pour la page de résultat

tsx
// app/payment-result/page.tsx "use client"; import { useEffect, useState } from "react"; import { useSearchParams, useRouter } from "next/navigation"; export default function PaymentResultPage() { const searchParams = useSearchParams(); const router = useRouter(); const ref = searchParams.get("ref"); const [status, setStatus] = useState<"checking" | "success" | "pending" | "failed">("checking"); useEffect(() => { if (!ref) { setStatus("failed"); return; } let attempts = 0; const maxAttempts = 5; const interval = setInterval(async () => { attempts++; try { const res = await fetch(`/api/orders/status?ref=${encodeURIComponent(ref)}`); const data = await res.json(); if (data.status === "success") { clearInterval(interval); setStatus("success"); } else if (attempts >= maxAttempts) { clearInterval(interval); // Якщо вебхук затримався, переводимо в статус очікування, а не помилки setStatus(data.status === "processing" ? "pending" : "pending"); } } catch (err) { if (attempts >= maxAttempts) { clearInterval(interval); setStatus("pending"); } } }, 1500); return () => clearInterval(interval); }, [ref]); return ( <div className="max-w-md mx-auto my-16 p-8 rounded-2xl bg-neutral-900 border border-neutral-800 text-center text-white"> {status === "checking" && ( <div> <div className="w-12 h-12 border-4 border-amber-500 border-t-transparent rounded-full animate-spin mx-auto mb-4" /> <h2 className="text-xl font-semibold mb-2">Підтверджуємо оплату...</h2> <p className="text-sm text-neutral-400">Отримуємо статус транзакції від банку. Зачекайте кілька секунд.</p> </div> )} {status === "success" && ( <div> <div className="w-12 h-12 bg-emerald-500/20 text-emerald-400 rounded-full flex items-center justify-center mx-auto mb-4 text-2xl font-bold">✓</div> <h2 className="text-xl font-semibold mb-2">Оплата успішна!</h2> <p className="text-sm text-neutral-400 mb-6">Ваше замовлення #{ref} успішно підтверджено.</p> <button onClick={() => router.push("/dashboard")} className="px-6 py-2.5 rounded-xl bg-amber-500 hover:bg-amber-400 text-black font-semibold transition"> Перейти до кабінету </button> </div> )} {status === "pending" && ( <div> <div className="w-12 h-12 bg-amber-500/20 text-amber-400 rounded-full flex items-center justify-center mx-auto mb-4 text-2xl font-bold">⏳</div> <h2 className="text-xl font-semibold mb-2">Платіж в обробці</h2> <p className="text-sm text-neutral-400 mb-6">Кошти зарезервовано. Підтвердження надійде протягом 1–2 хвилин.</p> <button onClick={() => router.push("/")} className="px-6 py-2.5 rounded-xl bg-neutral-800 hover:bg-neutral-700 text-white font-medium transition"> На головну </button> </div> )} </div> ); }

10. Fonctionnalités avancées : pRRO intégré, hold et Wallet

Monobank Acquiring offre une gamme complète d'outils pour des modèles de business plus complexes :

Page finale de paiement Monobank Hosted Checkout avec Apple Pay et cartes ЗбільшитиPage finale de paiement Monobank Hosted Checkout avec Apple Pay et cartesPage finale de paiement Monobank Hosted Checkout avec Apple Pay et cartes

10.1. RRO logiciel : Checkbox intégré gratuit dans la Caisse Monobank

Pour la plupart des FOP ukrainiens de groupes 2 et 3, la fiscalisation des ventes en ligne est une obligation légale.

Le principal avantage de Monobank pour les entrepreneurs est l'intégration gratuite et native avec le service pRRO Checkbox :

  • Activation en un clic : Dans le tableau de bord web.monobank.ua, allez dans les paramètres du terminal créé et activez le switch « Fiscalisation via Checkbox ». Monobank crée gratuitement la caisse et signe les reçus avec votre KEPP (signature électronique qualifiée).
  • Fiscalisation automatique : Si vous avez un catalogue standard avec un taux identique, vous n'avez même pas besoin de modifier le code de création de paiement — le reçu est formé automatiquement selon le champ destination dans l'objet du paiement.
  • Fiscalisation avancée via API : Si vous vendez des produits avec des taux de TVA différents ou des articles soumis à accise avec codes UKTZED, transmettez le tableau basketOrder à l'intérieur de l'objet merchantPaymInfo :
json
"merchantPaymInfo": { "reference": "order_1001", "destination": "Оплата онлайн-курсу", "customerEmails": ["client@example.com"], "basketOrder": [ { "name": "Доступ до курсу з вайбкодингу", "qty": 1, "sum": 99000, "code": "SKU-COURSE-01", "unit": "шт.", "total": 99000 } ] }

10.2. Paiement en deux étapes (Hold)

Si vous vendez des biens physiques qui peuvent être en rupture de stock, utilisez le mode hold :

  • Blocage : Transmettez paymentType: "hold" lors de la création de la facture. Les fonds sont gelés sur la carte de l'acheteur pour une durée allant jusqu'à 9 jours.
  • Débit (Finalize) : Après vérification de la disponibilité du produit, appelez /api/merchant/invoice/finalize. Vous pouvez débiter soit le montant total, soit un montant inférieur (par exemple, si un article manque).
  • Annulation : Si le produit n'est pas disponible, appelez /api/merchant/invoice/cancel — le paiement est annulé sans aucun frais bancaire pour l'acheteur.

10.3. Sauvegarde de cartes et abonnements (Wallet)

Si vous lancez un service SaaS avec un abonnement mensuel récurrent, transmettez saveCardData: true lors de la création de la première facture. Après le paiement réussi, le webhook retournera un walletId. Utilisez ce token pour les prochains débits automatiques sans ressaisie des données de carte par le client.


11. Matrice de sécurité et tests automatisés (Vitest / Jest)

Le paiement est la zone de responsabilité financière maximale. Une erreur dans un bouton classique provoque de l'agacement, mais une erreur dans la passerelle de paiement entraîne des pertes matérielles directes ou le blocage de la caisse par la banque.

Avant de passer en production, lancez la matrice de tests de sécurité :

Test de sécuritéCe qui est vérifiéComportement attendu du système
1. Protection contre la modification de prix (Price Tampering)Le client envoie productId: "vip", mais tente d'imposer dans la requête amount: 100 (1 UAH)Le serveur ignore le champ amount du client, prend le prix réel de la config (2490 UAH = 249 000 kopecks). Si le productId est falsifié — retourne HTTP 400.
2. Blocage sans signatureUne requête POST arrive sur l'endpoint /api/payment/webhook sans le header x-signLa requête est bloquée immédiatement avec une réponse HTTP 400 Bad Request. Aucun traitement de commande n'a lieu.
3. Blocage des fausses signaturesUn attaquant a envoyé une fausse signature dans x-sign avec le statut status: "success"La vérification crypto.verify(SHA256, ...) retourne false. Le serveur retourne HTTP 400/401, l'accès ou le produit n'est pas délivré.
4. Idempotence du webhook (Duplicate Delivery)Monobank a envoyé le même webhook success deux ou trois fois consécutives à cause d'un lag réseauL'accès ou le produit est activé une seule fois. Le webhook répété ne provoque pas de double délivrance, retournant à la banque HTTP 200 OK.
5. Résistance à la race conditionLe webhook de la banque arrive avant que la requête HTTP initiale de création de facture ne termine son écriture en baseLe handler du webhook est robuste à l'absence d'enregistrement en BDD (utilise UPSERT ou crée la commande à la volée).
6. Respect des rate limits (Rate Limit Buffer)En cas d'échec du webhook, un polling de statut /api/merchant/invoice/status est lancéLes requêtes sont exécutées avec une pause d'au moins 15 secondes, protégeant le terminal du blocage HTTP 429 Too Many Requests.

11.1. Suite de tests automatisés prête pour votre projet

Demandez à votre agent IA d'ajouter la suite de tests suivante au projet (monobank-acquiring.test.ts) :

typescript
import { describe, it, expect } from "vitest"; import crypto from "crypto"; // Генерація тестової пари ECDSA secp256r1 для симуляції підписів Monobank const { publicKey, privateKey } = crypto.generateKeyPairSync("ec", { namedCurve: "prime256v1", publicKeyEncoding: { type: "spki", format: "pem" }, privateKeyEncoding: { type: "pkcs8", format: "pem" }, }); describe("Monobank Acquiring Security Tests", () => { it("Тест 1: Не дозволяє клієнту маніпулювати ціною", () => { const CATALOG: Record<string, { priceUah: number }> = { plan_pro: { priceUah: 990 } }; const clientPayload = { productId: "plan_pro", amount: 100 }; // Спроба підсунути 1 грн const safeAmount = Math.round(CATALOG[clientPayload.productId].priceUah * 100); expect(safeAmount).toBe(99000); // 990.00 грн, а не 1.00 грн }); it("Тест 2: Відхиляє вебхук без обов'язкового заголовка x-sign", () => { const headers: Record<string, string> = {}; const hasSignature = Boolean(headers["x-sign"]); expect(hasSignature).toBe(false); }); it("Тест 3: Успішно верифікує валідний цифровий підпис банку", () => { const rawPayload = JSON.stringify({ invoiceId: "inv_123", status: "success", amount: 99000 }); const signer = crypto.createSign("SHA256"); signer.update(rawPayload); const validSignatureBase64 = signer.sign(privateKey, "base64"); const verifier = crypto.createVerify("SHA256"); verifier.update(rawPayload); const isValid = verifier.verify(publicKey, Buffer.from(validSignatureBase64, "base64")); expect(isValid).toBe(true); }); it("Тест 4: Блокує підроблений підпис або модифіковане тіло запиту", () => { const originalPayload = JSON.stringify({ invoiceId: "inv_123", status: "success", amount: 99000 }); const tamperedPayload = JSON.stringify({ invoiceId: "inv_123", status: "success", amount: 1000 }); const signer = crypto.createSign("SHA256"); signer.update(originalPayload); const signature = signer.sign(privateKey, "base64"); const verifier = crypto.createVerify("SHA256"); verifier.update(tamperedPayload); const isValid = verifier.verify(publicKey, Buffer.from(signature, "base64")); expect(isValid).toBe(false); }); it("Тест 5: Ідемпотентність — повторний вебхук не видає товар двічі", () => { const processedOrders = new Set<string>(); function handleOrder(invoiceId: string): { processed: boolean } { if (processedOrders.has(invoiceId)) { return { processed: false }; // Дубль відхилено } processedOrders.add(invoiceId); return { processed: true }; } expect(handleOrder("inv_001").processed).toBe(true); // Перший вебхук expect(handleOrder("inv_001").processed).toBe(false); // Повторний прихід дубліката expect(processedOrders.size).toBe(1); }); });

12. Checklist d'ingénierie final avant lancement

Vérifiez votre module de paiement sur ces 10 points avant de basculer vers les paiements en production :

  • Conformité du site : Le footer contient des liens vers l'Offre Publique, la Politique de Confidentialité, les Conditions de Retour et les coordonnées complètes du FOP avec numéro fiscal.
  • Montants en kopecks : Toutes les valeurs amount sont multipliées par 100 ($1\text{ UAH} = 100\text{ kopecks}$) via Math.round.
  • Sécurité de la clé : Le token est extrait dans .env sous le nom MONOBANK_TOKEN et ajouté au .gitignore.
  • Prix depuis le backend (SSOT) : Le client transmet uniquement l'identifiant du produit, le montant est pris exclusivement depuis la base ou la config.
  • Traitement rawBody : Le webhook vérifie le buffer texte ou binaire original (req.text() ou req.rawBody), évitant le double parsing via JSON.stringify.
  • Protection cryptographique : Le webhook vérifie la signature x-sign via la clé publique de la banque avec l'algorithme SHA256 ECDSA.
  • URL publique pour le webhook : Le serveur est accessible de l'extérieur via un certificat SSL HTTPS valide (pour les tests locaux, Cloudflare Tunnel ou ngrok conviennent).
  • Idempotence : La réception répétée du même webhook ne provoque pas de double délivrance de produit ou d'abonnement.
  • UX de la page de résultat : Sur /payment-result, un loader d'attente et un polling court du statut sont configurés pour protéger l'utilisateur contre la race condition.
  • Paiement réel de 1–5 UAH : Une transaction de test réussie a été effectuée avec une vraie carte, le débit des fonds et leur affichage dans le tableau de bord bancaire ont été vérifiés.

Ressources utiles et fichiers à télécharger

Ce guide est entièrement gratuit. S'il vous a fait gagner une soirée, vous pouvez soutenir la croissance du projet.
Soutenir l'auteur