Tableau de bord ↗ العربية

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.

Simuler une preuve
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.

Simuler la confirmation
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.

Simuler un rejet
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.

Simuler une expiration
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 :

Forcer un tick
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
}
ChampCompte
expiredIntentsIntents pending passés expired (échéance dépassée).
issuedInvoicesFactures de renouvellement émises.
markedOverdueFactures open dont l’échéance vient d’être dépassée.
markedUncollectibleFactures passées uncollectible (grâce épuisée).
closedSubscriptionsAbonnements terminés (annulation programmée arrivée à terme).
purgedIdempotencyKeysToujours 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

Produit, prix, 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

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

Preuve puis confirmation
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 incompleteactive, 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/:idlatestInvoice.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

CasRéponse
Clé lsk_live_…, ou session en mode Production400 ENVIRONMENT_NOT_ALLOWED
Aucune authentification401 AUTH_UNAUTHORIZED
Intent d’un autre marchand, intent live, ou identifiant inconnu404 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’horloge429 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 ?