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 via POST /v1/auth/login.
  • Connexion Google : flux OAuth GET /v1/auth/google (voir 4.1).

2. En-têtes standardisés

HeaderSensObligatoire sur
X-API-KeyClé API marchand (M2M)endpoints M2M
AuthorizationBearer JWT (dashboard)endpoints dashboard
Idempotency-KeyUUID v4, dédup. des opérations financièresPOST /v1/payments, POST /v1/topups
X-PaymentReçu de paiement signé (rejeu x402)requête rejouée vers le vendeur
X-Payment-ChallengeChallenge 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 }
  }
}
codeHTTPSignification
invalid_request400corps malformé, champ manquant, type invalide
authentication_required401clé API ou JWT absent/invalide
invalid_api_key401clé API révoquée/inexistante
insufficient_permissions403scope insuffisant sur la clé
merchant_suspended403compte marchand suspendu
not_found404ressource inexistante
method_not_allowed405méthode HTTP non supportée sur la route
idempotency_conflict409même Idempotency-Key avec un corps différent
insufficient_funds402solde insuffisant pour le paiement
challenge_expired402challenge x402 expiré
challenge_invalid402signature ou payload du challenge invalide
payment_required402ressource payante, challenge joint (flux x402)
rate_limited429limite de débit atteinte
internal_error500erreur 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)

POST/v1/auth/register

Corps : { "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).

POST/v1/auth/login

Corps : { "email": str, "password": str }

Réponse 200 : { "access_token": str, "token_type": "bearer", "expires_in": 86400 }

Erreurs : 401 authentication_required.

GET/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).

GET/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é).

GET/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 } }

PATCH/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).

POST/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).

DELETE/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

POST/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.

GET/v1/api-keys

(Bearer) — Réponse 200 : { "api_keys": [ { id, name, key_prefix, scopes, status, last_used_at, expires_at, created_at } ] }

DELETE/v1/api-keys/{api_key_id}

(Bearer) — Réponse 204 (révocation : status -> revoked, jamais de suppression physique).

4.3 Solde & grand livre

GET/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
}
GET/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)

POST/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.

GET/v1/topups/{topup_id}

(Bearer) — Réponse 200 : { "topup_id", "amount_eur", "status", "created_at", "completed_at" }

4.5 Transactions

GET/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 }

GET/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)

POST/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.

GET/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 } }

GET/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

POST/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).

POST/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.

GET/v1/webhook-endpoints

(Bearer) — Réponse 200 : { "webhook_endpoints": [ { id, url, events, status, created_at } ] }

DELETE/v1/webhook-endpoints/{endpoint_id}

(Bearer) — Réponse 204.

4.8 Santé

GET/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.

  1. 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>
  2. 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, le fee_mode, le nonce et une expiration (TTL 5 min).

  3. 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é.

  4. 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é>
  5. Vérification et livraison — le SDK vendeur vérifie la signature du reçu (ou via GET /v1/payments/&lbrace;id&rbrace;/receipt en 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-Key est obligatoire sur POST /v1/payments et POST /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_modeprix net (price_minor)total payé (amount_minor)crédit vendeur (net_minor)
deduct_from_amountPPP − fee
add_to_pricePP + feeP

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 €.