Payer (côté acheteur)

Dérouler un paiement Machine-to-Machine : recevoir le challenge 402, le régler auprès de Paylet, puis rejouer la requête avec la preuve de paiement.

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.

curlRequête initiale
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.

httpRéponse 402
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.

curlPOST /v1/payments
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 :

jsonRèglement effectué
{
  "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.

curlRejeu avec preuve
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.

httpRéponse 200
HTTP/1.1 200 OK
Content-Type: application/json

{ "id": 42, "data": { "title": "Jeu de données 42", "rows": 1200 } }

Erreurs courantes

codeHTTPSignification
insufficient_funds402Solde insuffisant : rechargez votre compte.
challenge_expired402Challenge reçu depuis plus de 5 minutes : refaites l'appel initial.
challenge_invalid402Signature invalide ou challenge déjà consommé (nonce rejoué).
idempotency_conflict409Même Idempotency-Key avec un corps différent.
insufficient_permissions403La clé n'a pas le scope write:payment.