Payer (côté acheteur)
Prérequis
- Un compte Paylet avec un solde rechargé (voir Démarrage rapide).
- Une clé API M2M avec le scope
write:payment. - L'URL d'une ressource payante exposée par un vendeur Paylet.
Le flux en un coup d'œil
Avec les SDK, tout le flux x402 est géré par un seul appel : la bibliothèque intercepte le 402, règle le challenge (avec un Idempotency-Key automatique) et rejoue la requête avec le reçu.
from paylet import Paylet
client = Paylet("ak_live_<votre-cle>")
# Un seul appel : le flux 402 est géré automatiquement.
# Séquence : appel sans preuve -> 402 -> règlement -> rejeu -> livraison.
data = client.fetch_paid("GET", "https://vendor.example.com/data/42")
print(data)Pour comprendre (ou intégrer sans SDK), voici les mêmes étapes de façon explicite, en HTTP brut.
Étape 1 — Appeler la ressource sans preuve
L'agent appelle l'endpoint payant normalement, sans header de paiement. Le SDK vendeur détecte l'absence de X-Payment.
curl -i https://vendor.example.com/data/42 \ -H "X-API-Key: ak_live_<cle-du-vendeur>"
Étape 2 — Recevoir le challenge 402
Le vendeur répond 402 Payment Required avec le challenge signé dans l'en-tête X-Payment-Challenge, et un corps d'erreur lisible par un humain.
HTTP/1.1 402 Payment Required
X-Payment-Challenge: <challenge signe base64url>
Content-Type: application/json
{
"error": {
"code": "payment_required",
"message": "Paiement requis pour accéder à cette ressource.",
"details": {
"price_minor": 1500000,
"amount_minor": 1500000,
"currency": "EUR",
"fee_mode": "deduct_from_amount",
"payee_merchant_id": "<uuid>",
"resource": "https://vendor.example.com/data/42"
}
}
}Le challenge lui-même est un objet JSON signé HMAC-SHA256 et encodé base64url. Il contient notamment price_minor (prix net du vendeur), amount_minor (total dû par l'acheteur), la resource ciblée, un nonce et une expiration (exp, 5 minutes). Ces deux montants peuvent différer selon le fee_mode du vendeur.
Étape 3 — Régler le challenge
L'agent appelle POST /v1/payments pour débiter son solde. Paylet vérifie la clé, la signature du challenge, sa fraîcheur (nonce non rejoué, TTL) et l'idempotence, puis débite atomiquement.
curl -X POST https://api.paylet.fr/v1/payments \
-H "X-API-Key: ak_live_<votre-cle>" \
-H "Idempotency-Key: 3c9a4b2e-7f1d-4a5b-9c6e-1d8f0b2a4c6e" \
-H "Content-Type: application/json" \
-d '{
"challenge": "<challenge signe base64url>",
"amount_minor": 1500000,
"currency": "EUR",
"resource": "https://vendor.example.com/data/42",
"payload_hash": "sha256:<hex>"
}'Réponse 200 :
{
"payment": {
"id": "<uuid>",
"status": "settled",
"price_minor": 1500000,
"amount_minor": 1500000,
"fee_minor": 22500,
"net_minor": 1477500,
"currency": "EUR",
"fee_mode": "deduct_from_amount",
"settled_at": "2026-08-21T12:00:00Z"
},
"receipt": "<reçu signe base64url>"
}Le montant fait foi depuis le challenge
price_minor, amount_minor et fee_mode sont lus depuis le challenge signé. Le amount_minor envoyé dans le corps doit correspondre au total du challenge, sinon Paylet répond 402 challenge_invalid.
Voici le même règlement via les SDK (équivalent du curl ci-dessus) :
from paylet import Paylet
client = Paylet("ak_live_<votre-cle>")
# challenge : objet base64url reçu dans l'en-tête X-Payment-Challenge.
payment = client.settle(challenge, 1500000)
print("Statut :", payment.status) # -> "settled"
print("Reçu signé :", payment.receipt) # à rejouer via X-PaymentÉtape 4 — Rejouer avec la preuve
L'agent rejoue la requête initiale en joignant le reçu signé dans le header X-Payment.
curl https://vendor.example.com/data/42 \ -H "X-API-Key: ak_live_<cle-du-vendeur>" \ -H "X-Payment: <reçu signe base64url>"
Étape 5 — Livraison de la ressource
Le SDK vendeur vérifie la signature du reçu (localement, sans appel réseau), contrôle que la resource, le amount_minor et le payee_merchant_id correspondent, puis délivre la ressource.
HTTP/1.1 200 OK
Content-Type: application/json
{ "id": 42, "data": { "title": "Jeu de données 42", "rows": 1200 } }Erreurs courantes
| code | HTTP | Signification |
|---|---|---|
| insufficient_funds | 402 | Solde insuffisant : rechargez votre compte. |
| challenge_expired | 402 | Challenge reçu depuis plus de 5 minutes : refaites l'appel initial. |
| challenge_invalid | 402 | Signature invalide ou challenge déjà consommé (nonce rejoué). |
| idempotency_conflict | 409 | Même Idempotency-Key avec un corps différent. |
| insufficient_permissions | 403 | La clé n'a pas le scope write:payment. |