Vous ne pouvez pas valider une intégration si chaque scénario exige un vrai acheteur, un vrai virement et un clic humain. La sandbox porte donc cinq endpoints de simulation qui forcent chaque étape du cycle de vie : preuve déposée, argent confirmé, rejet, expiration, et le passage de l’horloge de facturation.
Rien n’est maquillé : ces endpoints traversent la vraie machine à états, le vrai pipeline de confirmation, et émettent les vrais webhooks. Ce que vous observez en sandbox est exactement ce que la production fera.
Déposer une preuve
POST /payments/intents/:id/simulate/proof joue le geste de l’acheteur : il attache une preuve synthétique (un PDF « PREUVE SIMULEE - SANDBOX », visible dans le tableau de bord comme n’importe quelle preuve) et passe l’intent en proof_submitted. Les webhooks payment.proof_submitted, payment.proof_analyzed puis payment.proof_validated partent vers votre endpoint sandbox, l’analyse étant simulée avec un verdict match. L’intent finit en proof_validated : la preuve tient, le paiement reste à confirmer.
curl -X POST https://api.lahsab.com/payments/intents/INTENT_ID/simulate/proof \
-H "x-api-key: lsk_sandbox_CLE" Mêmes gardes que l’endpoint public de dépôt : un intent expiré répond 409 PAYMENT_INTENT_EXPIRED, un intent dans un état terminal 409 PAYMENT_INVALID_TRANSITION.
Confirmer « argent reçu »
POST /payments/intents/:id/simulate/confirm joue votre clic Confirmer : la confirmation passe par le pipeline de signal habituel, donc le payload du webhook payment.confirmed porte confirmationSource: "manual", comme en production. L’intent passe en confirmed, et s’il est rattaché à une facture, elle est soldée : invoice.paid est émis et la période de l’abonnement avance.
curl -X POST https://api.lahsab.com/payments/intents/INTENT_ID/simulate/confirm \
-H "x-api-key: lsk_sandbox_CLE" Valide depuis pending, proof_submitted ou proof_validated. Idempotent : rappeler l’endpoint sur un intent déjà confirmé renvoie l’intent confirmé, sans erreur.
Rejeter
POST /payments/intents/:id/simulate/reject rejette le paiement. Le corps { "reason": "…" } est optionnel. L’intent passe en rejected et payment.rejected est émis.
curl -X POST https://api.lahsab.com/payments/intents/INTENT_ID/simulate/reject \
-H "x-api-key: lsk_sandbox_CLE" \
-H "content-type: application/json" \
-d '{ "reason": "Fonds jamais reçus" }' Le scénario « preuve déposée mais fonds jamais reçus » (le cas que votre code doit savoir encaisser) s’obtient en enchaînant simulate/proof puis simulate/reject.
Expirer
POST /payments/intents/:id/simulate/expire force l’expiration sans attendre expiresAt. Valide uniquement depuis pending (sinon 409 PAYMENT_INVALID_TRANSITION). Émet payment.expired.
curl -X POST https://api.lahsab.com/payments/intents/INTENT_ID/simulate/expire \
-H "x-api-key: lsk_sandbox_CLE" Faire tourner l’horloge
Le renouvellement et les retards ne dépendent pas d’un appel API : c’est l’horloge de facturation qui les produit, une fois par heure. POST /billing/simulate/tick l’exécute maintenant, bornée à vos données sandbox, et renvoie son rapport :
curl -X POST https://api.lahsab.com/billing/simulate/tick \
-H "x-api-key: lsk_sandbox_CLE" {
"expiredIntents": 0,
"issuedInvoices": 1,
"markedOverdue": 0,
"markedUncollectible": 0,
"closedSubscriptions": 0,
"purgedIdempotencyKeys": 0
}
| Champ | Compte |
|---|---|
expiredIntents | Intents pending passés expired (échéance dépassée). |
issuedInvoices | Factures de renouvellement émises. |
markedOverdue | Factures open dont l’échéance vient d’être dépassée. |
markedUncollectible | Factures passées uncollectible (grâce épuisée). |
closedSubscriptions | Abonnements terminés (annulation programmée arrivée à terme). |
purgedIdempotencyKeys | Toujours 0 en mode borné. |
L’endpoint est limité à 6 appels par minute (429 RATE_LIMITED au-delà).
Le parcours abonnement, en accéléré
Un prix interval: "day" plus l’horloge simulée : plusieurs cycles complets d’abonnement en quelques minutes, là où la production en met un par mois.
1. Le catalogue et le client
curl -X POST https://api.lahsab.com/products \
-H "x-api-key: lsk_sandbox_CLE" \
-H "content-type: application/json" \
-d '{ "name": "Abonnement Test" }'
# Un prix à la journée : un cycle complet dure 24 h, pas un mois.
curl -X POST https://api.lahsab.com/prices \
-H "x-api-key: lsk_sandbox_CLE" \
-H "content-type: application/json" \
-d '{
"productId": "PRODUCT_ID",
"amount": 500,
"type": "recurring",
"interval": "day"
}'
curl -X POST https://api.lahsab.com/customers \
-H "x-api-key: lsk_sandbox_CLE" \
-H "content-type: application/json" \
-d '{ "externalId": "user_test", "name": "Client Test", "phone": "0770000000" }' 2. Souscrire
curl -X POST https://api.lahsab.com/subscriptions \
-H "x-api-key: lsk_sandbox_CLE" \
-H "content-type: application/json" \
-d '{ "customerId": "CUSTOMER_ID", "priceId": "PRICE_ID" }' L’abonnement naît incomplete, sa première facture est émise, et la réponse porte latestInvoice.checkoutUrl, un lien qui se termine par /pay/INTENT_ID. Ce segment final est l’identifiant sur lequel s’appliquent les endpoints de simulation.
3. Payer la première facture
curl -X POST https://api.lahsab.com/payments/intents/INTENT_ID/simulate/proof \
-H "x-api-key: lsk_sandbox_CLE"
curl -X POST https://api.lahsab.com/payments/intents/INTENT_ID/simulate/confirm \
-H "x-api-key: lsk_sandbox_CLE" La facture passe paid, l’abonnement passe incomplete → active, et vous recevez payment.proof_submitted, payment.proof_analyzed, payment.proof_validated, payment.confirmed puis invoice.paid sur votre endpoint sandbox.
4. Faire tourner l’horloge
POST /billing/simulate/tick : le rapport revient avec "issuedInvoices": 1 : la facture de renouvellement est émise, car la période d’un jour se termine dans la fenêtre d’émission (invoiceLeadDays).
5. Payer le renouvellement
Récupérez le lien de la facture de renouvellement (GET /subscriptions/:id → latestInvoice.checkoutUrl), puis rejouez simulate/proof et simulate/confirm sur son intent. invoice.paid arrive, et currentPeriodEnd avance d’exactement une période, depuis la fin de la précédente, jamais depuis « maintenant ».
Les erreurs
| Cas | Réponse |
|---|---|
Clé lsk_live_…, ou session en mode Production | 400 ENVIRONMENT_NOT_ALLOWED |
| Aucune authentification | 401 AUTH_UNAUTHORIZED |
| Intent d’un autre marchand, intent live, ou identifiant inconnu | 404 PAYMENT_INTENT_NOT_FOUND |
| Transition impossible (ex. expirer un intent confirmé) | 409 PAYMENT_INVALID_TRANSITION |
| Preuve sur un intent expiré | 409 PAYMENT_INTENT_EXPIRED |
| Plus de 6 appels par minute sur l’horloge | 429 RATE_LIMITED |
Un intent hors de votre portée (autre marchand, autre environnement) répond 404, jamais 403 : il est invisible, pas interdit.
Et ensuite ?
- Le moteur d’abonnements et sa règle d’or : Abonnements.
- Vérifier la signature des webhooks reçus : Webhooks.
- Les cinq endpoints en détail : Référence API.