Intégrer la boutique à QryptoPay
Cet article fait suite à la configuration de la page de paiement. À ce stade, le module, le marchand, un portefeuille de réception et la page de paiement doivent déjà être prêts ; si ce n’est pas le cas, commencez par le démarrage rapide.
Il reste à assembler le cycle de paiement complet : connecter votre boutique au terminal, générer des liens de paiement et recevoir les notifications de paiement par webhook.
💡 Vous ne voulez pas vous occuper de l’intégration vous-même ?
Notre équipe peut connecter QryptoPay à votre boutique : il vous suffit de nous écrire. Le coût dépend de la complexité du projet et de la technologie utilisée : langage, framework ou CMS.
Jetons du terminal
Chaque terminal possède sa propre paire de jetons, un jeton public et un jeton privé : ils garantissent que les intentions de paiement et leurs notifications n’ont pas été falsifiées. Tant que la paire n’est pas générée, le terminal ne peut pas créer de liens de paiement.
Pour la générer, ouvrez le marchand, repérez la fiche du terminal concerné et cliquez sur l’icône en forme de clé dont l’infobulle indique « Générer des jetons », puis, dans la boîte de dialogue « Générer les jetons du terminal », sur « Générer ». Le panneau affiche le jeton privé : copiez-le avec le bouton situé à côté du champ, puis confirmez avec « Je l’ai copié ».
⚠️ Le jeton privé n’est affiché qu’une seule fois
QryptoPay ne le conserve pas de son côté, vous ne pourrez donc pas le consulter de nouveau : si vous fermez la boîte de dialogue sans avoir copié le jeton, vous devrez générer une nouvelle paire, et l’ancienne cessera d’être valide dès la génération.
Pour en savoir plus, notamment sur la différence entre le jeton public et le jeton privé et sur les cas où il vaut la peine de régénérer la paire, consultez l’article Générer les jetons du terminal dans QryptoPay.
Étape 1. Configurer l’intégration
L’élément central de l’intégration est le terminal. Chaque terminal possède les paramètres suivants :
- ID — un identifiant unique ;
- Paire de jetons — jeton public et jeton privé (ils servent à identifier les paiements) ;
- Webhook — l’URL de votre boutique à laquelle QryptoPay envoie les notifications sur l’état des paiements ;
- Clé du webhook — sert à authentifier les notifications envoyées au webhook.
⚠️ Important
Conservez le jeton privé dans un endroit sûr. S’il est compromis, des personnes malveillantes peuvent perturber le traitement de vos paiements. C’est pourquoi, une fois le jeton généré, QryptoPay ne le conserve pas de son côté.
En cas de perte ou de compromission, vous pouvez générer une nouvelle paire de jetons : n’oubliez pas de mettre à jour les valeurs dans votre boutique.
La clé du webhook est moins critique, mais il est également recommandé de ne pas la divulguer à des tiers, afin d’éviter la falsification des notifications envoyées au webhook.
Nous vous recommandons de commencer par le terminal de test : contrairement au terminal de production, il permet de vérifier l’intégration sans transfert réel de cryptomonnaie.
Commencez par générer une paire de jetons pour le terminal de test et conservez le jeton privé de votre côté.
Enregistrez les paramètres du terminal dans les variables d’environnement de votre boutique. Par exemple, le fichier .env peut se présenter ainsi :
TERMINAL_ID: <ID du terminal>
PRIVATE_TOKEN: <jeton privé généré>
WEBHOOK_KEY: <clé du webhook issue des paramètres>
PAYMENT_URL: <domaine de la page de paiement>Il faut ensuite implémenter une méthode qui génère le jeton de paiement, utilisé pour créer un lien de paiement côté QryptoPay.
Voici un exemple d’implémentation d’une telle méthode en pseudo-code :
function generate_payment_token(private_key_b64, terminal_uuid) -> string
# décode la clé privée depuis base64/base64url en seed (octets bruts)
seed := decode_base64_any(private_key_b64)
# génère un nonce unique (généralement un uuid4)
nonce := uuid_v4()
# assemble le payload (champs obligatoires + données du paiement)
payload := {
ts: current_unix_time_seconds(),
nonce: nonce,
terminal_uuid: terminal_uuid, // ID du terminal dans QryptoPay
amount_fiat: transaction.amount, // montant à payer en USD, au format 00.00, decimal, supérieur à 0
payment_mid: transaction.uuid, // ID de la transaction dans votre boutique, string
back_to_store_link: link, // lien vers votre site pour que le client puisse y revenir après le paiement (des paramètres peuvent être ajoutés si besoin), string
customer: {
id: customer.uuid, // ID du client dans votre système (obligatoire), string
email: customer.email or "" // e-mail du client dans votre système (facultatif), string
},
metadata: { // tout autre paramètre que vous souhaitez transmettre
key: value // au format metadata.key=value
},
assets_deny: [asset, asset, ...] // facultatif : symboles des cryptomonnaies indisponibles pour ce paiement
}
# sérialise le payload en une représentation d’octets déterministe (canonique)
payload_bytes := canonical_encode(payload)
# encode le payload dans un format adapté à la transmission
payload_part := base64url_no_padding(payload_bytes)
# calcule la partie cryptographique du jeton à partir de la clé privée et de payload_part
proof_bytes := sign(seed, bytes(payload_part, ASCII))
# encode la partie cryptographique du jeton
proof_part := base64url_no_padding(proof_bytes)
# format final du jeton : "<payload_b64u>.<proof_b64u>"
return payload_part + "." + proof_part
end⚠️ Important
Pour générer le jeton de paiement, vous devez transmettre, en plus des données de la transaction, les paramètres obligatoires : l’horodatage actuel, un nonce (nous recommandons uuid4) et l’ID du terminal.
L’horodatage sert à calculer la durée de vie du lien (un peu plus d’une heure). Le nonce renforce la sécurité et empêche de créer plusieurs intentions de paiement avec le même lien (protection contre le spam). Sans l’ID du terminal, QryptoPay ne peut pas traiter le paiement.
En plus de metadata, le payload comporte un autre champ facultatif, assets_deny : un tableau de symboles de cryptomonnaies qui ne peuvent pas être choisies pour ce paiement, même si elles sont configurées chez vous et autorisées par la licence — par exemple ["USDT", "TRX"]. Le filtre agit par cryptomonnaie et non par réseau, et s’applique à tous les réseaux à la fois : ["USDT"] masque cette cryptomonnaie aussi bien sur Ethereum que sur Tron (USDT TRC-20), tandis que ["TRX"] masque uniquement le TRX natif, sans toucher à l’USDT TRC-20. Un symbole inconnu n’est pas une erreur : le lien de paiement est créé normalement, simplement sans rien masquer en plus. Une cryptomonnaie exclue ne peut pas non plus être choisie en contournant la page de paiement. Le champ est facultatif : les intégrations qui ne le transmettent pas ne verront aucune différence.
Les clés de metadata peuvent aussi servir au routage des paiements entre portefeuilles : sur la page du marchand, dans l’onglet « Flux de paiement », vous définissez des conditions de la forme « clé = valeur » et vous y faites référence par leur nom. Par exemple, avec metadata: { location: "eu" }, une règle dont la condition est location = eu s’applique. Le nom de la clé est choisi par la boutique : QryptoPay ne le connaît pas à l’avance et ne le vérifie pas ; l’essentiel est que la clé du lien corresponde à celle de la règle. La valeur est comparée sans tenir compte de la casse : eu et EU sont équivalentes. Une clé absente de metadata ne satisfait pas la condition : la règle ne s’applique simplement pas, sans erreur. Pour la condition sur le montant de paiement, la boutique n’a rien à transmettre : QryptoPay le calcule à partir de amount_fiat. Pour configurer les règles elles-mêmes, consultez l’article Routage des paiements entre les portefeuilles dans QryptoPay.
Avec le jeton obtenu, envoyez une requête POST à QryptoPay pour obtenir un lien de paiement. Par exemple :
POST /public/api/payments/intents/create/
Host: qpay.yoursite.com
Content-Type: application/json
{
"key": "jeton de paiement"
}⚠️ Important
Au moment de la requête, la page de paiement doit déjà être créée sur le serveur où QryptoPay est installé.
Si la requête aboutit, vous recevez une réponse contenant le lien de paiement. Transmettez ce lien au client pour qu’il puisse passer au paiement.
{
"service_id": "be535ba0-7f84-4cd3-9454-b26c4a938479",
"url": "https://qpay.yoursite.com/?payment=be535ba0-7f84-4cd3-9454-b26c4a979225",
"expires_at": "2026-01-30T08:21:56.526112Z"
}En cas d’erreur, le serveur peut renvoyer l’un des codes suivants :
- 400 — requête non valide (erreur de format ou paramètres obligatoires manquants).
- 403 — signature de la requête incorrecte ou jeton de paiement expiré.
- 409 — réutilisation d’un jeton à usage unique : le
noncetransmis a déjà été utilisé pour ce terminal. - 444 — création d’intentions de paiement temporairement interdite (par exemple en raison des restrictions de la licence ou d’un dépassement de limites).
- 445 — création d’intentions de paiement interdite : licence épuisée ou accès restreint de façon permanente.
- 500 — erreur interne du serveur.
⚠️ Important
Si le client ouvre le lien de paiement, choisit une cryptomonnaie et lance le paiement (clic sur « Continuer »), il ne peut plus changer de cryptomonnaie. Il doit alors retourner sur votre site et générer un nouveau lien de paiement. Ce comportement est voulu pour des raisons de sécurité.
⚠️ Important
Si vous transmettez l’adresse e-mail du client dans le payload à QryptoPay et qu’elle change par la suite, lors du paiement suivant QryptoPay retrouvera le client par son ID et, si l’adresse ne correspond pas, la remplacera. L’adresse mise à jour sera également appliquée aux paiements créés précédemment.
Le processus général de génération d’un lien de paiement se présente ainsi :
- une intention de paiement (par exemple une transaction) est créée dans votre boutique ;
- le montant à payer est converti en USD (QryptoPay n’accepte que des montants en USD) ;
- un jeton de paiement avec les paramètres requis est généré et envoyé au serveur sur lequel QryptoPay est installé ;
- QryptoPay renvoie un lien de paiement, que vous transmettez au client pour qu’il passe au paiement.
Le client effectue ensuite le paiement sur la page de paiement et, si besoin, revient dans la boutique. La notification de paiement réussi ou échoué parvient à votre boutique avec un délai, car les transactions de la blockchain doivent attendre les confirmations du réseau. Comptez en général de 1 à 2 minutes (Tron, USDT TRC-20) jusqu’à 10 à 30 minutes (Bitcoin) ; pour Monero, environ 20 minutes.
Vous pouvez maintenant générer un lien de paiement, l’ouvrir, choisir une cryptomonnaie et lancer le paiement. Si tout est correctement configuré, le paiement apparaît dans votre marchand QryptoPay, sous le terminal de test.
Étape 2. Configurer le webhook
Une fois que le client a payé, QryptoPay analyse les blockchains pour retrouver la transaction correspondante. Si le paiement a été effectué strictement selon les instructions de la page de paiement, une notification de paiement réussi est envoyée au webhook.
La notification est envoyée par une requête HTTP avec les paramètres suivants :
POST /your/webhook/url
Content-Type: application/json
X-Term-UUID: 2671f44b-a025-44d3-b2f1-a0ea07b8acb7
X-Timestamp: 1738150000
X-Body-SHA256: 9c4d2b0a2f5b7d6c8a... # 64 caractères hexadécimaux
X-Signature: 7f1c3d5e9a... # 64 caractères hexadécimaux
{
... corps JSON de la notification ...
}Pour traiter les notifications, il vous faut une méthode qui valide la signature du webhook et confirme que la requête n’a pas été interceptée ni modifiée. Voici un exemple de cette vérification en pseudo-code :
function validate_webhook_response() -> bool
# assemble le message pour le HMAC : "<terminal_uuid>:<ts>:<body_sha256_hex>"
message_str := string(term_uuid_header) + ":" + string(ts) + ":" + computed_body_hash
message_bytes := utf8_bytes(message_str)
# calcule la signature attendue : HMAC-SHA256(secret, message), hex
expected_sig := hmac_sha256_hex(key = utf8_bytes(secret), msg = message_bytes)
# compare les signatures en temps constant
if not constant_time_equals(expected_sig, sig_header) then
return false
end
return true
end⚠️ Important
Nous recommandons de considérer la requête du webhook comme non valide si au moins une des conditions suivantes est remplie :
- l’un des en-têtes obligatoires est absent ;
X-Term-UUIDne correspond pas à l’ID du terminal utilisé pour générer le jeton de paiement de cette transaction ;X-Timestampn’est pas un nombre (int) ou plus de 300 secondes se sont écoulées depuis l’horodatage indiqué (valeur recommandée).
Si la vérification réussit, vous pouvez désérialiser le corps de la requête. Vous obtenez un objet qui ressemble à ceci :
{
"payment_result": "payment_result", // résultat du paiement : success, mismatch, unexpected
"amount_coins": "6.343593", // montant en cryptomonnaie, jusqu’à 9 décimales, zéros de fin omis
"expected_amount_coins": "6.343593", // montant attendu selon la facture, en cryptomonnaie, jusqu’à 9 décimales, zéros de fin omis
"is_underpaid": false, // true si le montant reçu est inférieur au montant attendu
"amount_fiat": "0.00", // montant en USD, format 00.00
"surcharge_fiat": "0.00", // frais retenus en USD, format 00.00, 0.00 si les frais sont désactivés
"amount_fiat_net": "0.00", // solde qui vous revient après déduction des frais, USD, format 00.00 ; amount_fiat = amount_fiat_net + surcharge_fiat
"fiat_code": "USD", // devise fiat
"coins_asset": "USDC", // cryptomonnaie : BTC, ETH, USDT, USDC, etc.
"coins_chain": "ETH", // réseau : BTC, ETH, TRX, XMR, etc.
"service_id": "qryptopay_pi_uuid", // identifiant interne du paiement dans QryptoPay
"payment_mid": "string", // ID de la transaction dans votre boutique, null si le résultat est unexpected
"customer": {
"id": "your_id", // ID du client dans votre système
"email": "string" // e-mail du client, null s’il n’a pas été transmis
},
"metadata": { // null s’il n’a pas été transmis à l’origine ou si le résultat est unexpected
"key1": "value1"
},
"transaction_ids": [ // transactions associées dans la blockchain (une ou plusieurs)
"686...fbe",
"8be...6ab"
]
}Les montants en cryptomonnaie (amount_coins, expected_amount_coins) arrivent sous forme de chaîne décimale : analysez-la comme un decimal. Le nombre de chiffres après la virgule dépend de la cryptomonnaie, les zéros de fin ne sont pas transmis, et un montant entier arrive sans point décimal — par exemple "100". Les champs fiat amount_fiat, surcharge_fiat et amount_fiat_net, au contraire, contiennent toujours exactement deux chiffres après le point décimal.
Les champs surcharge_fiat et amount_fiat_net sont toujours présents, même lorsque les frais de paiement en cryptomonnaie sont désactivés : surcharge_fiat vaut alors 0.00 et amount_fiat_net est égal à amount_fiat, de sorte que les intégrations existantes continuent de fonctionner. Pour le détail du calcul des frais, consultez l’article Configurer les frais par devise dans QryptoPay.
Les champs expected_amount_coins et is_underpaid sont eux aussi toujours présents, quel que soit le scénario et même si le sous-paiement toléré est désactivé — pour la même raison : les intégrations existantes continuent de fonctionner. Pour savoir en quoi consiste ce réglage, voir la section « Sous-paiement toléré » plus bas.
⚠️ Important
Si la validation réussit, répondez par 200 OK. Sinon, QryptoPay renverra la notification de paiement jusqu’à recevoir une réponse 200.
Vous pouvez utiliser les données reçues pour le traitement ultérieur du paiement. Certains paramètres sont renvoyés tels quels — par exemple, toutes les valeurs de metadata transmises via le lien de paiement.
⚠️ Important
Si l’adresse e-mail du client est déjà enregistrée dans QryptoPay mais n’a pas été transmise pour un paiement ultérieur, la notification envoyée au webhook contiendra l’adresse e-mail stockée dans la base de QryptoPay.
💡 Astuce
Nous recommandons de valider en plus les données du webhook en les comparant aux paramètres d’origine de la transaction (par exemple amount_fiat_net avec le montant de la commande, l’ID du paiement et l’ID du client).
Il faut ensuite implémenter, côté boutique, une méthode qui traite correctement la notification entrante. Les scénarios possibles sont les suivants :
success— le paiement a abouti, y compris lorsque le montant reçu est légèrement inférieur à celui de la facture mais reste dans les limites du sous-paiement toléré (s’il est activé — détails plus bas). Dans ce cas, avant de finaliser le paiement, nous recommandons de comparer la valeuramount_fiat_net(montant après déduction des frais de paiement en cryptomonnaie) reçue dans la notification avec le montant interne de votre commande :amount_fiatpeut dépasser le montant de la commande du montant de ces frais ;mismatch— le client a payé trop, ou a payé en moins au-delà de ce que permet le sous-paiement toléré ;unexpected— le client a envoyé des fonds au portefeuille sans qu’un lien de paiement ait été généré au préalable.
Vous êtes libre de définir la logique de traitement de chaque scénario. Par exemple, en cas de mismatch, vous pouvez comparer le montant de la notification au montant attendu et, si le paiement est incomplet, demander au client de compléter son paiement. En cas de unexpected, vous pouvez par exemple créditer automatiquement le solde du client, puis le prévenir du paiement.
Une fois le gestionnaire implémenté, renseignez le champ du webhook dans les paramètres du terminal de test et refaites le parcours de paiement : générez un nouveau lien et ouvrez-le. Avec le terminal de test, le paiement est traité immédiatement et la notification est envoyée aussitôt au webhook.
Si votre boutique reçoit la notification, l’intégration de test est terminée.
Si votre boutique n’a pas reçu la notification ou n’a pas pu la traiter, vous pouvez vérifier l’état de la livraison et renvoyer la notification manuellement, directement depuis la fiche du paiement, sans nouveau transfert de fonds. La marche à suivre est décrite dans l’article Gérer les paiements dans QryptoPay.
Sous-paiement toléré
Il arrive qu’un client envoie un peu moins que le montant de la facture : son portefeuille a arrondi le transfert ou prélevé une partie pour les frais de réseau, et au lieu de 102 USDT, vous recevez 101,99. Par défaut, une telle facture n’est pas clôturée : QryptoPay attend le montant complet. Si vous êtes prêt à accepter ces paiements, activez le sous-paiement toléré : le paiement arrive alors sur le webhook avec payment_result: "success" et is_underpaid: true, et les boutiques qui considèrent déjà success comme une confirmation de paiement clôturent la commande sans modification.
Pour activer l’option, ouvrez QryptoPay → « Paramètres » → « Cryptomonnaies », activez le commutateur du bloc « Sous-paiement toléré » et définissez de combien le client peut payer en moins : un pourcentage du montant de la facture et, si nécessaire, un montant maximal de sous-paiement en dollars.
Pour distinguer un tel paiement d’un paiement du montant exact, comparez amount_coins (ce qui est arrivé) avec expected_amount_coins (ce que la facture attendait) et vérifiez is_underpaid. Le manque est à la charge de la boutique : il est déduit de amount_fiat_net, tandis que les frais de paiement en cryptomonnaie, s’ils sont configurés, sont retenus en totalité (détails dans l’article Configurer les frais par devise dans QryptoPay). Un trop-perçu arrive avec le résultat mismatch.
Étape 3. Vérifier le cycle complet
Avant d’accepter de vrais paiements, parcourez tout le parcours client avec le terminal de test :
- Créez une commande dans votre boutique et générez un lien de paiement pour celle-ci.
- Ouvrez le lien : la page de paiement s’affiche sur votre domaine.
- Choisissez une cryptomonnaie et confirmez le paiement.
- Attendez la notification du webhook : avec le terminal de test, le paiement est traité immédiatement, aucun transfert réel de cryptomonnaie n’est nécessaire.
- Vérifiez que la commande de la boutique est passée à l’état payé.
Si tous les points sont validés, l’intégration fonctionne de bout en bout : les liens sont créés, la page accepte le paiement, la boutique est informée du paiement. Si le parcours s’interrompt en cours de route, cherchez la cause à cette même étape : si le paiement n’apparaît pas chez le marchand, le problème vient du jeton de paiement ; s’il apparaît mais que la notification n’arrive pas, il vient du gestionnaire du webhook.
Étape 4. Passer au terminal de production
Vous pouvez maintenant accepter de vrais paiements. Pour cela, procédez ainsi :
- générez une nouvelle paire de jetons pour le terminal de production ;
- modifiez les paramètres du terminal et indiquez l’adresse du webhook ;
- dans le fichier
.envde votre boutique, remplacez les valeursID,PRIVATE_TOKENetWEBHOOK_KEYpar celles du terminal de production.
Vous pouvez ensuite vérifier le fonctionnement du terminal en créant un paiement d’essai. Attention : à cette étape, vous devrez effectuer un vrai transfert dans la cryptomonnaie pour laquelle un portefeuille a été ajouté. Nous vous recommandons de commencer par des montants minimes.
Si tout est correctement configuré, le paiement sera crédité dans votre boutique.
Étapes suivantes
Votre boutique est connectée et accepte des paiements via le terminal de production. Voyons maintenant comment gérer le module :
- Gérer les paiements dans QryptoPay — recherche d’un paiement, revérification, recherche d’une transaction par son hash et renvoi du webhook.
- Accepter Monero (XMR) — si vous ajoutez Monero : portefeuille, nœud et paiement d’essai.
- Configurer les notifications e-mail de QryptoPay — soyez informé des paiements bloqués et de l’état de la licence.
- Gérer les portefeuilles dans QryptoPay — soldes, réserve pour les frais, désactivation d’un portefeuille.
- Configurer les frais par devise dans QryptoPay — faites supporter le coût du transfert au client.
- Supervision de l’état de QryptoPay — soyez informé d’une panne du serveur avant vos clients.