Documentation API
Base URL : https://api.paylet.fr — préfixe de version /v1 — format JSON (UTF-8).
1. Authentification
Deux mécanismes distincts, jamais mélangés :
- M2M (SDK / agents autonomes) : header
X-API-Key(clé API marchand, créée via le dashboard). - Dashboard / humain : Bearer JWT (
Authorization: Bearer) obtenu viaPOST /v1/auth/login. - Connexion Google : flux OAuth
GET /v1/auth/google(voir 4.1).
2. En-têtes standardisés
| Header | Sens | Obligatoire sur |
|---|---|---|
| X-API-Key | Clé API marchand (M2M) | endpoints M2M |
| Authorization | Bearer JWT (dashboard) | endpoints dashboard |
| Idempotency-Key | UUID v4, dédup. des opérations financières | POST /v1/payments, POST /v1/topups |
| X-Payment | Reçu de paiement signé (rejeu x402) | requête rejouée vers le vendeur |
| X-Payment-Challenge | Challenge x402 signé | réponse 402 du vendeur |
3. Codes d'erreur normalisés
Les erreurs renvoient un statut HTTP adéquat et un corps :
{
"error": {
"code": "insufficient_funds",
"message": "Solde insuffisant : requis 1500000 micro-crédits, disponible 420000.",
"details": { "required_minor": 1500000, "available_minor": 420000 }
}
}| code | HTTP | Signification |
|---|---|---|
| invalid_request | 400 | corps malformé, champ manquant, type invalide |
| authentication_required | 401 | clé API ou JWT absent/invalide |
| invalid_api_key | 401 | clé API révoquée/inexistante |
| insufficient_permissions | 403 | scope insuffisant sur la clé |
| merchant_suspended | 403 | compte marchand suspendu |
| not_found | 404 | ressource inexistante |
| method_not_allowed | 405 | méthode HTTP non supportée sur la route |
| idempotency_conflict | 409 | même Idempotency-Key avec un corps différent |
| insufficient_funds | 402 | solde insuffisant pour le paiement |
| challenge_expired | 402 | challenge x402 expiré |
| challenge_invalid | 402 | signature ou payload du challenge invalide |
| payment_required | 402 | ressource payante, challenge joint (flux x402) |
| rate_limited | 429 | limite de débit atteinte |
| internal_error | 500 | erreur serveur (jamais de détail sensible) |
Le code HTTP 402 sert volontairement dans le flux x402 pour signaler « paiement requis » ET pour les erreurs de solde ; le client distingue les cas via le champ code.
4. Endpoints
4.1 Authentification & profil (dashboard)
/v1/auth/registerCorps : { "email": str, "password": str, "password_confirm": str, "first_name": str, "last_name": str, "company_name": str? }
Réponse 201 : { "merchant": { …profil public }, "access_token": str }
Erreurs : 400 invalid_request (email déjà pris, champ email), 422 (mot de passe < 8 caractères, ou password_confirm ≠ password).
/v1/auth/loginCorps : { "email": str, "password": str }
Réponse 200 : { "access_token": str, "token_type": "bearer", "expires_in": 86400 }
Erreurs : 401 authentication_required.
/v1/auth/google(public) — Démarre le flux OAuth Google : construit l'URL d'autorisation (client_id, redirect_uri, scope openid email profile) et redirige le navigateur (302).
/v1/auth/google/callback(public) — Échange le code contre un token Google, lit le profil, puis identifie ou crée le marchand et redirige vers le dashboard avec le JWT en query param ?token=… (302).
Erreurs : 401 authentication_required (email non vérifié ou échange échoué).
/v1/me(Bearer) — Réponse 200 : { "merchant": { "id": uuid, "email": str, "first_name": str, "last_name": str, "company_name": str|null, "fee_mode": str, "status": str, "currency": str, "created_at": str } }
/v1/me(Bearer) — Corps partiel : { "first_name"?, "last_name"?, "company_name"?, "email"?, "fee_mode"? } — Réponse 200 : profil mis à jour.
fee_mode accepte deduct_from_amount | add_to_price (voir section 9).
/v1/me/password(Bearer) — Corps : { "current_password": str, "new_password": str } — Réponse 200 : { "updated": true }.
Erreurs : 401 authentication_required (current_password invalide), 422 (new_password faible).
/v1/me(Bearer) — RGPD droit d'effacement. Réponse 202 : { "erasure": "scheduled", "effective_at": "…" }. Erreurs : 409 merchant_suspended si solde non nul non remboursé.
4.2 Clés API
/v1/api-keys(Bearer) — Corps : { "name": str, "scopes"?: str[] } (scopes par défaut : read:balance, write:payment, read:transactions).
Réponse 201 : { "api_key": { id, name, key_prefix, scopes, status, created_at }, "secret": "ak_live_…" }
Le secret n'est renvoyé qu'une seule fois, à la création.
/v1/api-keys(Bearer) — Réponse 200 : { "api_keys": [ { id, name, key_prefix, scopes, status, last_used_at, expires_at, created_at } ] }
/v1/api-keys/{api_key_id}(Bearer) — Réponse 204 (révocation : status -> revoked, jamais de suppression physique).
4.3 Solde & grand livre
/v1/balance(Bearer ou X-API-Key, scope read:balance) — Réponse 200 :
{
"currency": "EUR",
"balance_minor": 420000,
"balance_eur": "0.42",
"reserved_minor": 0,
"available_minor": 420000
}/v1/ledger(Bearer ou X-API-Key, scope read:balance) — Query : limit (défaut 50, max 200), cursor, entry_type, from, to.
Réponse 200 : { "entries": [ … ], "has_more": bool, "next_cursor": str }
4.4 Rechargement (Stripe)
/v1/topups(Bearer, Idempotency-Key requis) — Corps : { "amount_eur": "10.00" }
Réponse 201 : { "topup_id": uuid, "checkout_url": "https://checkout.stripe.com/…", "amount_eur": "10.00", "status": "pending" }
Erreurs : 400 invalid_request (montant < 1 EUR ou > 5000 EUR), 409 idempotency_conflict.
/v1/topups/{topup_id}(Bearer) — Réponse 200 : { "topup_id", "amount_eur", "status", "created_at", "completed_at" }
4.5 Transactions
/v1/transactions(Bearer ou X-API-Key, scope read:transactions) — Query : limit, cursor, status, from, to, role (payer | payee).
Réponse 200 : { "transactions": [ … ], "has_more": bool, "next_cursor": str }
/v1/transactions/{transaction_id}(Bearer ou X-API-Key) — Réponse 200 : transaction complète (dont error_code/error_message si failed).
4.6 Paiements M2M (cœur x402)
/v1/payments(X-API-Key, scope write:payment, Idempotency-Key requis) — Règle le challenge x402 : débite le payeur, crédite le bénéficiaire, émet un reçu signé.
Corps :
{
"challenge": "<challenge x402 signé, 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", "amount_minor": 1500000,
"fee_minor": 22500, "net_minor": 1477500, "currency": "EUR",
"settled_at": "…" },
"receipt": "<reçu signé base64url>"
}Erreurs : 401 invalid_api_key, 402 insufficient_funds, 402 challenge_expired, 402 challenge_invalid, 403 insufficient_permissions, 409 idempotency_conflict.
/v1/payments/{payment_id}(Bearer ou X-API-Key, scope read:transactions) — Réponse 200 : { "payment": { id, status, amount_minor, fee_minor, net_minor, currency, settled_at, error_code } }
/v1/payments/{payment_id}/receipt(X-API-Key, scope read:transactions) — Réponse 200 : { "receipt": "<reçu signé base64url>" } (permet de récupérer une preuve perdue).
4.7 Webhooks
/v1/webhooks/stripe(entrant, signature Stripe requise) — Reçoit les événements Stripe. Aucune auth applicative : la signature Stripe-Signature est vérifiée. Répond 200 immédiatement. Erreurs : 400 invalid_request (signature invalide).
/v1/webhook-endpoints(Bearer) — Corps : { "url": str, "events": str[] } — Réponse 201 : { "webhook_endpoint": { id, url, events, secret, status, created_at } }. Le secret est renvoyé une seule fois.
/v1/webhook-endpoints(Bearer) — Réponse 200 : { "webhook_endpoints": [ { id, url, events, status, created_at } ] }
/v1/webhook-endpoints/{endpoint_id}(Bearer) — Réponse 204.
4.8 Santé
/v1/health(public) — Réponse 200 : { "status": "ok", "version": "1.0.0", "time": "…" }
5. Flux x402 / HTTP 402
Séquence nominale : l'agent acheteur (client) interroge un marchand qui expose un endpoint payant protégé par le SDK Paylet.
- Requête sans preuve — l'agent appelle la ressource sans header de paiement.
GET /data/42 HTTP/1.1 Host: vendor.example.com X-API-Key: *** du vendeur si requise>
- Challenge 402 — le SDK vendeur répond :
HTTP/1.1 402 Payment Required X-Payment-Challenge: <challenge signé> { "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://…" } } }Le challenge est un JSON signé HMAC-SHA256, encodé base64url. Il contient le prix net (
price_minor), le montant total (amount_minor), la ressource, lefee_mode, le nonce et une expiration (TTL 5 min). - Règlement — l'agent appelle Paylet pour débiter son solde :
POST /v1/payments HTTP/1.1 Host: api.paylet.fr X-API-Key: *** de l'acheteur> Idempotency-Key: <uuid v4> { "challenge": "…", "amount_minor": 1500000, "currency": "EUR", … }Paylet authentifie la clé, vérifie le challenge (signature + TTL + nonce non rejoué), contrôle l'idempotence, puis débite atomiquement et renvoie la transaction settled + le reçu signé.
- Rejeu avec preuve — l'agent rejoue la requête initiale avec le reçu signé :
GET /data/42 HTTP/1.1 Host: vendor.example.com X-Payment: <reçu signé>
- Vérification et livraison — le SDK vendeur vérifie la signature du reçu (ou via
GET /v1/payments/{id}/receipten mode online), contrôle que resource / amount_minor / payee_merchant_id correspondent, puis délivre la ressource :200 OK+ data.
6. Idempotence
- Le header
Idempotency-Keyest obligatoire surPOST /v1/paymentsetPOST /v1/topups. - Même clé + même corps : la réponse d'origine est renvoyée (pas de re-débit).
- Même clé + corps différent : 409
idempotency_conflict. - Le rejeu d'un challenge dont le nonce est déjà consommé est refusé (
challenge_invalid), même avec un Idempotency-Key neuf.
7. Statuts de transaction
pending -> settled (paiement réussi) pending -> failed (solde insuffisant, challenge invalide…) settled -> refunded (remboursement, écritures correctrices) settled -> reversed (chargeback / litige)
En pratique, un paiement est settled ou failed dès la réponse.
8. Webhooks sortants
Chaque envoi est signé avec le header X-Webhook-Signature : HMAC-SHA256(secret, body).
transaction.settled :
{
"event": "transaction.settled",
"data": { "transaction_id": uuid, "amount_minor": 1500000, "currency": "EUR",
"status": "settled", "settled_at": "…" }
}topup.completed :
{
"event": "topup.completed",
"data": { "topup_id": uuid, "amount_minor": 1000000, "currency": "EUR" }
}balance.updated : solde courant après tout mouvement (optionnel, sur abonnement).
9. fee_mode — répartition de la commission
Chaque marchand choisit comment la commission est répartie via le champ fee_mode (visible dans GET /v1/me, modifiable via PATCH /v1/me).
deduct_from_amount(défaut) — l'acheteur paie le prix net, le vendeur reçoit le prix net moins la commission.add_to_price— l'acheteur paie le prix net plus la commission, le vendeur reçoit le prix net en entier.
La commission est toujours calculée sur le prix net du vendeur :
fee_minor = max(round(price_minor * 0.015), 2000)
| fee_mode | prix net (price_minor) | total payé (amount_minor) | crédit vendeur (net_minor) |
|---|---|---|---|
| deduct_from_amount | P | P | P − fee |
| add_to_price | P | P + fee | P |
Exemple chiffré : P = 1,50 € → fee_minor = max(round(1500000 × 0,015), 2000) = 22500 micro-crédits. En deduct_from_amount l'acheteur paie 1,50 € et le vendeur reçoit 1,4775 € ; en add_to_price l'acheteur paie 1,5225 € et le vendeur reçoit 1,50 €.