Comment confirmer automatiquement le paiement d’une commande par webhook
Reliez une facture à une commande, vérifiez les signatures webhook et protégez la livraison contre les doublons et les annulations.
Mis à jour:
Pour que votre site marque automatiquement une commande comme payée, créez une facture dans Cryptanio et configurez un webhook : une notification envoyée à votre serveur lorsque l'état du paiement change. Le serveur vérifie l'authenticité du message, retrouve la commande et contrôle l'état de la facture. Il peut ensuite accorder l'accès acheté ou transmettre la commande pour exécution.
Prenons l'exemple d'une boutique qui crée une facture dont le prix est exprimé en dollars comme unité de compte, tandis que l'acheteur choisit une cryptomonnaie et un réseau disponibles sur la page de paiement. Les fonds arrivent sur les adresses de la boutique. Les commandes et la remise du produit restent gérées par son application ; Cryptanio relie la facture au transfert et signale son état. Cette intégration nécessite un développeur ou la possibilité de modifier le code serveur du site.
Préparez le projet et les adresses de paiement
Dans votre compte Cryptanio, sélectionnez un projet et ajoutez vos adresses de réception. Connectez-les au service Payments : une simple surveillance de l'adresse ne suffit pas pour émettre des factures. Vérifiez qu'elles sont actives et correspondent au réseau choisi. Les paiements réels nécessitent des adresses en mode de fonctionnement réel.
Prévoyez plusieurs adresses si les clients peuvent régler des commandes simultanément. Une adresse peut être occupée par une facture ouverte ou temporairement indisponible après sa clôture. L'intégration doit afficher une erreur claire lorsqu'aucune adresse n'est libre et permettre une nouvelle tentative ultérieure.
Créez une clé API et conservez-la sur le serveur de la boutique. Elle servira aux appels vers https://api.cryptanio.com/v1. Ne la placez pas dans le JavaScript de la page, une application mobile ou un dépôt public.
Configurez l'adresse de réception des notifications
Ouvrez les « Notifications » du projet choisi. Si « Utiliser les paramètres de notification du compte » est activé, configurez Webhook via « Paramètres du compte », ou désactivez l’héritage et définissez l’adresse propre au projet.
Dans la carte Webhook, indiquez l’adresse HTTPS de votre gestionnaire dans « Point final URL » et cliquez sur « Enregistrer ». Le gestionnaire doit accepter directement les requêtes POST : la redirection vers une autre URL n’est pas utilisée. Récupérez le « Secret de signature » dans la même carte et conservez-le sur le serveur, séparément de la clé API.
Vérifiez que Webhook n’est pas en pause. Traitez payment.confirmed et payment.rolled_back dans votre gestionnaire, et utilisez payment.pending pour un statut intermédiaire si nécessaire. Avant d’exécuter la commande, vérifiez le mode réel et les autres conditions ci-dessous. Il n’est pas nécessaire d’activer un canal Webhook séparé pour l’adresse de la facture.
Créez une facture pour une commande précise
Enregistrez d'abord la commande dans la base de la boutique. Le serveur crée ensuite la facture avec POST /v1/invoices. Transmettez votre identifiant de commande dans reference. Dans cet exemple, le prix de la commande est de 12.50 USD : invoice_currency et invoice_amount définissent le prix, tandis que l'acheteur choisit un réseau et un actif disponibles sur la page de paiement. N'envoyez donc pas chain, asset, amount, address ni tolerance dans cette requête. Indiquer USD ne signifie ni accepter des fonds en monnaie fiduciaire ni convertir les cryptomonnaies reçues en dollars.
#!/usr/bin/env bash
set -euo pipefail
: "${CRYPTANIO_API_KEY:?Set CRYPTANIO_API_KEY}"
: "${CRYPTANIO_OPERATION_ID:?Set CRYPTANIO_OPERATION_ID}"
: "${CRYPTANIO_PROJECT_ID:?Set CRYPTANIO_PROJECT_ID}"
json_string() {
local text="$1" char code index output='"'
for ((index=0; index<${#text}; index++)); do
char="${text:index:1}"
case "$char" in
'"'|'\') output+="\\$char" ;;
*) printf -v code '%d' "'$char"
if ((code < 32)); then printf -v char '\\u%04x' "$code"; fi
output+="$char" ;;
esac
done
printf '%s"' "$output"
}
response_file="$(mktemp)"
trap 'rm -f "$response_file"' EXIT
transfer_status=0
http_status="$(curl \
--silent --show-error --max-time 30 --request POST \
--header "Authorization: Bearer $CRYPTANIO_API_KEY" \
--header 'Accept: application/json' \
--header 'Content-Type: application/json' \
--header "Idempotency-Key: $CRYPTANIO_OPERATION_ID" \
--data-binary '{"project":'"$(json_string "$CRYPTANIO_PROJECT_ID")"',"invoice_currency":"USD","invoice_amount":"12.50","reference":"order-42"}' \
"https://api.cryptanio.com/v1/invoices" \
--output "$response_file" --write-out '%{http_code}')" || transfer_status=$?
if [[ "$http_status" == 2?? ]]; then
cat "$response_file"
exit "$transfer_status"
fi
printf 'HTTP %s\n' "$http_status" >&2
cat "$response_file" >&2
exit 1
Avant d'exécuter l'exemple, définissez CRYPTANIO_API_KEY, CRYPTANIO_PROJECT_ID et CRYPTANIO_OPERATION_ID sur votre serveur. Utilisez une clé API de production autorisée à créer des factures ; CRYPTANIO_PROJECT_ID doit désigner le projet Payments préparé. CRYPTANIO_OPERATION_ID est l'identifiant enregistré de l'opération de création pour cette commande : de 16 à 80 lettres ASCII, chiffres, tirets ou traits de soulignement. L'exemple le transmet dans l'en-tête Idempotency-Key, et non dans un champ JSON. Générez une autre valeur pour une nouvelle commande ; conservez la même valeur pour réessayer la même requête.
Si la création réussit, l'API renvoie HTTP 201. Enregistrez l'id de la facture et le checkout_url reçu avec la commande, puis dirigez l'acheteur vers cette URL. Tant qu'aucun moyen de paiement n'est choisi, le réseau, l'adresse et le montant en cryptomonnaie ne sont pas encore attribués. Sur la page de paiement, l'acheteur choisit un moyen disponible et obtient les coordonnées et l'échéance en vigueur. Si votre propre interface affiche les coordonnées de paiement, utilisez les données de la tentative concernée, le montant exact amount et son échéance expires_at ; n'arrondissez pas le montant et ne le remplacez pas par asked. Les liens renvoyés par les requêtes GET ultérieures et la liste des factures sont en lecture seule ; conservez séparément le lien interactif reçu lors de la création et ne le communiquez qu'à l'acheteur concerné.
Si la connexion est interrompue après l'envoi, renvoyez le même corps avec le même Idempotency-Key. Une nouvelle tentative réussie renvoie HTTP 201 et le même id. Modifier les paramètres avec une clé déjà utilisée entraîne HTTP 409. reference relie la facture à la commande, mais ne remplace pas la clé d'idempotence. Ne changez pas de clé simplement parce que la requête a expiré.
Vérifiez l'authenticité du webhook
Cryptanio transmet X-Timestamp, X-Signature, X-Event-Id, X-Event-Type et X-Attempt. La signature se calcule ainsi :
hex(HMAC-SHA256(secret, timestamp + "." + raw_body))
raw_body désigne les octets originaux de la requête. Récupérez-les avant l'analyse du JSON : une nouvelle sérialisation peut changer les espaces ou l'ordre des champs et faire échouer la vérification. Limitez la taille du corps accepté et refusez les requêtes dépourvues de signature valide.
L'exemple ci-dessous utilise une fenêtre de fraîcheur de cinq minutes. Il s'agit d'un réglage de votre gestionnaire ; l'horloge du serveur doit être synchronisée pour qu'il fonctionne correctement.
import hashlib
import hmac
import re
import time
def verify_signature(secret, timestamp, raw_body, signature):
if not secret or not timestamp or not re.fullmatch(r"[0-9]{1,12}", timestamp):
return False
if not signature or not re.fullmatch(r"[0-9a-f]{64}", signature):
return False
if abs(time.time() - int(timestamp)) > 300:
return False
signed = timestamp.encode("ascii") + b"." + raw_body
expected = hmac.new(secret.encode("utf-8"), signed,
hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature)
La fonction vérifie la signature et l'heure de la requête. Après validation, analysez le JSON et contrôlez schema_version="v1", le project_id attendu, mode="live" et sandbox=false.
Pour prendre une décision, utilisez event_id et event_type du corps signé. Vous pouvez comparer les en-têtes correspondants à ces valeurs ; ces en-têtes d'événement ne sont pas eux-mêmes protégés par la signature. Ne lancez la logique de paiement que pour l'objet payment attendu, de direction in.
Distinguez confirmation du transfert et règlement de la commande
Les principaux événements pour la boutique sont :
payment.pending: le transfert est détecté et attend sa confirmation ;payment.confirmed: le transfert a atteint le nombre de confirmations requis ;payment.rolled_back: un transfert précédemment détecté a été annulé à la suite d'un changement de la chaîne de blocs.
Un événement payment.confirmed ne prouve pas encore qu'une commande précise est intégralement réglée. Vérifiez l'objet invoice et comparez invoice.id et invoice.reference à la commande enregistrée. Un message sans facture associée ne doit pas être rattaché automatiquement à un achat sur la seule ressemblance du montant.
Avant toute remise, demandez l'état actuel avec GET /v1/invoices/{id}. Vérifiez id, project_id, reference, le prix de la commande dans invoice_currency et invoice_amount, ainsi que sandbox=false. Contrôlez le réseau, l'actif, l'adresse et la tentative de paiement choisie ; en présence de plusieurs tentatives, tenez compte de settled_attempt_id et settled_attempt. Un paiement réussi correspond au statut paid de la facture ; pending, seen et partial n'autorisent pas la remise. Le retour de l'acheteur sur votre site ou l'ouverture du lien de paiement ne confirme pas le paiement.
Un montant insuffisant peut avoir le statut underpaid, un paiement partiel en attente d'un complément a le statut partial, et un paiement tardif a le statut late. En cas de trop-perçu, la politique du projet peut laisser la facture en paid ou lui attribuer overpaid avec needs_decision=true. Les états underpaid, overpaid et late exigent une décision distincte avant l'exécution de la commande. L'API transmet amount_raw et paid sous forme de chaînes. Pour les comparer, interprétez-les comme des entiers exacts dans les plus petites unités de l'actif ; decimals définit l'échelle. N'utilisez pas de calcul en virgule flottante binaire. Définissez à l'avance le traitement des écarts ; aucun remboursement automatique n'en découle.
Gérez les répétitions et les annulations
La livraison peut répéter des messages. Enregistrez l'événement vérifié dans un stockage durable et utilisez event_id comme clé unique. La répétition d'un événement déjà accepté doit recevoir une réponse positive sans déclencher une nouvelle remise du produit.
Le seul identifiant de transaction ne permet pas d'écarter tous les messages suivants : un transfert passe par plusieurs états. Pour la combinaison de chain, tx_id, index, address et de l'identité de l'actif, enregistrez la dernière payment.revision appliquée. N'appliquez une transition que si sa revision est supérieure à celle déjà traitée.
Enregistrez de façon cohérente le changement de commande et la tâche de remise, avec une protection contre les traitements simultanés. La tâche doit avoir une clé de commande unique. Juste avant son exécution, vérifiez que la remise reste autorisée. Si une annulation arrive, mettez à jour l'état et stoppez les remises non exécutées ; pour un accès déjà accordé, prévoyez un réexamen ou une suspension.
Vérifiez le parcours complet
Avant un paiement réel, testez l’analyse du corps, la signature et la protection contre les doublons avec des données préparées. Un événement de service webhook.test ne confirme aucun achat et ne remplace pas la vérification du parcours complet.
Utilisez ensuite votre propre commande de contrôle pour vérifier tout le processus : création de facture, transfert, confirmation et mise à jour de commande. Testez séparément les répétitions, le paiement insuffisant et l'annulation sur des données préparées. Renvoyez HTTP 2xx après l'enregistrement fiable du message accepté ; si cet enregistrement est temporairement impossible, renvoyez une erreur serveur pour permettre une nouvelle livraison.
Dans le compte, comparez l'événement et le journal de livraison à l'enregistrement de votre application. La vérification est terminée lorsque la commande confirmée est exécutée une seule fois et qu'un événement intermédiaire, étranger ou répété ne déclenche pas une remise incorrecte.
Références : factures de l'API v1, vérification de la signature webhook.