Version de travail · contenus et exemples à affiner Voir les 95 pages ↗
Parlons de votre projet ↗
Plateformes / P027

Escrow (séquestre opérationnel)

Adaptation éditoriale
Référence de travail. Les routes, schémas, limites et procédures hérités du référentiel sont à aligner avec l’API d’Éric. Ils ne prouvent pas une fonctionnalité livrée. Les hôtes .invalid sont des exemples inertes.

Fonction avancée à cadrer

Les réserves opérationnelles de cette référence ne sont ni le cantonnement réglementaire ni un service de séquestre juridique garanti. Leur activation dépend du dispositif partenaire et du périmètre plateforme.

Point d’attention.

L'escrow n'est pas un encaissement instantané. Seule la clé plateforme (clé API marchand émise par le plateforme) peut créer un pot, lancer son encaissement, et plus tard le libérer ou le rembourser. Le marchand ne peut pas se débloquer lui-même.

Tout le cycle de vie passe par /partner_api/escrows : vous créez le pot, vous lancez son encaissement avec POST /partner_api/escrows/{id}/collect, puis vous le libérez ou le remboursez. Le pot montre lui-même où en est l'encaissement, les montants et les frais. Ce n'est pas un séquestre réglementé : les fonds retenus n'apparaissent ni dans le solde du marchand ni dans GET /transactions jusqu'à la libération.

Point d’attention.

POST /payment_requests et POST /payment_links avec escrowId répondent 400 escrow_id_not_supported, et rien n'est créé. Les encaissements d'un pot ne sont plus lisibles sur GET /payment_requests/{id} ni GET /payment_links/{id} (404). Le pot ne contient plus paymentRequestId ni paymentLinkId. Lisez le bloc collect du pot à la place.

Clés

Clé Encaissement instantané /escrows
Clé plateforme (émise par le SP pour ce marchand) Oui Oui
Clé Owner / Dashboard du marchand Oui 403
Clé entreprise du SP (/service_providers/*) Non (pas d'argent) 403

La clé plateforme est scoped au marchand. Elle ne peut pas toucher les escrows d'un autre marchand (404). Vous l'obtenez via Clés et magasin dédié.

Il n'y a pas d'activation par marchand. Dès que la clé plateforme existe, /partner_api/escrows est disponible pour ce marchand.

De la création à la libération

POST /partner_api/escrows                 { storeId, holdUntil? }        → pending
POST /partner_api/escrows/{id}/collect    { type, data }                 → pending, collect.status: pending
  → le client paie (redirect / in-app / soundbox / lien)
  → held (ESCROW_HELD) ; GET /transactions reste vide pour ce job
  ou l'encaissement échoue → failed (ESCROW_COLLECT_FAILED) → relancez /collect
POST /partner_api/escrows/{id}/release    → released (ESCROW_RELEASED)
  ou /refund                              → refunded (ESCROW_REFUNDED)
  ou holdUntil passe sans paiement        → expired  (ESCROW_EXPIRED)
Statut Signification Webhook
pending Pot créé, ou encaissement en cours aucun
failed L'encaissement (demande de paiement) a échoué ou expiré. Le pot peut être relancé ESCROW_COLLECT_FAILED
held Le client a payé, les fonds sont retenus ESCROW_HELD
released Fonds crédités au marchand (manuel ou à holdUntil) ESCROW_RELEASED
refunded Remboursement au payeur lancé ESCROW_REFUNDED
expired holdUntil est passé sans paiement ESCROW_EXPIRED

1. Créer le pot

Consulter la fiche de référence : Créer une réserve opérationnelle

curl -X POST "https://api.kelyko.invalid/partner_api/escrows" \
  -H "X-API-KEY: platform_key" \
  -H "X-API-KEY-ID: platform_key_id" \
  -H "Content-Type: application/json" \
  -d '{
    "storeId": "59ae202a-f583-4a15-970f-9e99bd1e0baa"
  }'

Corps : uniquement storeId et, optionnellement, holdUntil (ISO 8601 UTC).

  • holdUntil omis → 7 jours. Doit être dans le futur, au plus 30 jours.
  • holdUntil court dès la création du pot, pas dès le paiement. Il ne peut être ni prolongé ni désactivé.
  • Pot held à holdUntil → Kelyko le libère (pas un remboursement au payeur).
  • Pot encore pending ou failed à holdUntil → il passe expired, et son encaissement en cours est arrêté (demande expirée, lien désactivé).

Réponse (pending) :

{
  "id": "7c2d1a90-0b1e-4c3a-9f11-2e8c4d6a1b20",
  "storeId": "59ae202a-f583-4a15-970f-9e99bd1e0baa",
  "status": "pending",
  "holdUntil": "2026-09-14T20:00:00.000Z",
  "amount": null,
  "createdAt": "2026-09-07T20:00:00.000Z",
  "collect": null,
  "release": null,
  "refund": null
}

2. Lancer l'encaissement

Consulter la fiche de référence : Encaisser vers une réserve

type vaut payment_request ou payment_link. data reprend le corps habituel de Paiement en ligne ou de Liens de paiement, sans storeId (c'est celui du pot) :

  • payment_request : amountCents, currency, reference, paymentDetails.
  • payment_link : title, amountCents, currency. Le lien est toujours à paiement unique.
curl -X POST "https://api.kelyko.invalid/partner_api/escrows/7c2d1a90-0b1e-4c3a-9f11-2e8c4d6a1b20/collect" \
  -H "X-API-KEY: platform_key" \
  -H "X-API-KEY-ID: platform_key_id" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "payment_request",
    "data": {
      "amountCents": 50000,
      "currency": "XOF",
      "reference": "ORDER-1042",
      "paymentDetails": {
        "type": "redirect",
        "data": {
          "paymentMethod": "wave",
          "successUrl": "https://example.com/success",
          "errorUrl": "https://example.com/error"
        }
      }
    }
  }'

Réponse : le pot, avec le bloc collect.

{
  "id": "7c2d1a90-0b1e-4c3a-9f11-2e8c4d6a1b20",
  "status": "pending",
  "collect": {
    "type": "payment_request",
    "status": "pending",
    "errorReason": null,
    "reference": "ORDER-1042",
    "redirectUrl": "https://pay.wave.com/c/…",
    "link": null,
    "amount": { "amount": 50000, "currency": "XOF" },
    "fees": null,
    "paymentMethod": "wave",
    "counterpartIdentifier": null
  },
  "release": null,
  "refund": null
}
  • redirectUrl n'est renvoyé que dans cette réponse. Conservez-le : GET /escrows/{id} le renvoie à null.
  • Pour un lien, collect.link est l'URL à partager, reference et paymentMethod restent null jusqu'au paiement.
  • Tous les moyens de paiement mobile money sont acceptés : Wave, Orange, MTN, Moov, Djamo.
  • Un pot = un encaissement à la fois. /collect sur un pot pending qui a déjà un encaissement → 409 escrow_already_bound.
  • /collect sur un pot held, released, refunded, expired, ou après holdUntil → 409 escrow_not_collectable.
  • Si l'appel à l'opérateur échoue (400 third_party_payment_provider_error), le pot n'est pas consommé : vous pouvez rappeler /collect.

Suivre l'encaissement

Pollez GET /partner_api/escrows/{id}, ou écoutez les webhooks. collect.status :

collect.status Quand
pending En attente du paiement
success Payé : le pot est held
failed L'opérateur a refusé le paiement. errorReason donne sa raison
expired La demande a expiré sans paiement (lien : holdUntil passé)

Échec et nouvelle tentative

Une demande de paiement qui échoue ou expire fait passer le pot à failed, et ESCROW_COLLECT_FAILED part. Rappelez /collect sur le même pot : il repasse pending et le nouveau collect remplace l'ancien. Les tentatives précédentes n'apparaissent plus.

Tant que la tentative précédente peut encore être payée chez l'opérateur, /collect répond 409 escrow_not_collectable. Réessayez quand elle est terminée.

Un lien ne fait jamais passer le pot à failed : le payeur peut réessayer sur le même lien. Le pot reste pending jusqu'au paiement ou jusqu'à holdUntil.

3. Après le paiement (held)

  • GET /partner_api/escrows/{id} : status: "held", collect.status: "success", avec ce que le payeur a payé, les frais, son moyen de paiement et son numéro.
  • GET /partner_api/transactions : vide pour ce job. Le solde magasin (GET /stores/{id}/balance) ne change pas.
  • Webhook *ESCROW_HELD*, pas TRANSACTION_COMPLETED. Ne créditez pas le marchand comme si le paiement était dépensable. TRANSACTION_COMPLETED arrive plus tard, à la libération.
{
  "id": "7c2d1a90-0b1e-4c3a-9f11-2e8c4d6a1b20",
  "storeId": "59ae202a-f583-4a15-970f-9e99bd1e0baa",
  "status": "held",
  "holdUntil": "2026-09-14T20:00:00.000Z",
  "amount": { "amount": 50000, "currency": "XOF" },
  "createdAt": "2026-09-07T20:00:00.000Z",
  "collect": {
    "type": "payment_request",
    "status": "success",
    "errorReason": null,
    "reference": "ORDER-1042",
    "redirectUrl": null,
    "link": null,
    "amount": { "amount": 50000, "currency": "XOF" },
    "fees": { "amount": 800, "currency": "XOF" },
    "paymentMethod": "wave",
    "counterpartIdentifier": "+2250700000000"
  },
  "release": null,
  "refund": null
}

collect.amount est ce que le payeur a payé : le montant demandé si le marchand paie les frais, montant + frais si le payeur les paie (50800 ici). amount en haut du pot reste le montant demandé.

Un abonnement events: ["TRANSACTION_COMPLETED"] ne reçoit pas les webhooks escrow. events: null (défaut) les reçoit. Lisez d'abord l'en-tête Kelyko-Event. Qui reçoit l'appel est dans Événements. L'abonnement se crée avec la clé plateforme : Webhooks marchand.

Libération, remboursement, expiration

Un pot held se termine par une libération (manuelle ou automatique) ou un remboursement. Chacun porte sur tout le pot : pas de montant partiel.

Libération

POST /partner_api/escrows/{id}/release crédite le marchand. Résultat : status: "released", le bloc release, et une ligne type=escrow sur GET /transactions. Deux webhooks partent : ESCROW_RELEASED et TRANSACTION_COMPLETED, dont le corps est cette ligne.

"release": { "amount": { "amount": 49200, "currency": "XOF" }, "fees": { "amount": 800, "currency": "XOF" }, "autoRelease": false }

release.amount est le montant crédité au marchand, égal à celui de la ligne type=escrow.

Libération automatique à holdUntil

Si le pot est encore held à holdUntil, Kelyko le libère exactement comme un release : mêmes montants, même ligne type=escrow, mêmes webhooks, avec release.autoRelease: true.

Remboursement

POST /partner_api/escrows/{id}/refund rembourse le payeur. Le marchand ne reçoit rien et ne paie rien. Résultat : status: "refunded", le bloc refund, et ESCROW_REFUNDED, sans TRANSACTION_COMPLETED ni ligne sur GET /transactions.

"refund": {
  "status": "success",
  "amount": { "amount": 49000, "currency": "XOF" },
  "fees": { "amount": 200, "currency": "XOF" },
  "paymentMethod": "wave",
  "counterpartIdentifier": "+2250700000000"
}

ESCROW_REFUNDED part quand le remboursement est lancé. Avec Wave, refund.status vaut tout de suite success. Avec les autres opérateurs, il vaut pending jusqu'à la réponse de l'opérateur, puis success ou failed. Le pot reste refunded dans les deux cas : lisez refund.status sur GET /escrows/{id}. counterpartIdentifier peut être null quand le numéro du payeur n'est pas connu.

Expiration

Un pot pending ou failed dont holdUntil passe devient expired, et ESCROW_EXPIRED part. Son encaissement en cours est arrêté. Si un paiement de cet encaissement est malgré tout confirmé par l'opérateur après coup, le pot passe held (ESCROW_HELD), puis il est libéré automatiquement, car holdUntil est déjà passé.

Qui reçoit combien

Exemple : un paiement Wave de 500 XOF, avec 8 XOF de frais et 2 XOF de frais de remboursement.

Frais payés par le marchand Frais payés par le payeur
Le payeur paie (collect.amount) 500 XOF 508 XOF
À la libération, le marchand reçoit (release.amount) 492 XOF (frais 8) 500 XOF (frais 8)
Au remboursement, le payeur reçoit (refund.amount) 490 XOF (500 − 8 − 2) 498 XOF (508 − 8 − 2)

Votre commission de plateforme vous est versée à la libération ou au remboursement.

/escrows vs /transactions

Surface Rôle
GET /partner_api/escrows Cycle de vie du pot et de son encaissement, avec montants et frais (collect, release, refund)
GET /partner_api/transactions Portefeuille opérable seulement. Rien pendant le hold. Après libération : une ligne type=escrow avec le net crédité, les frais et le moyen de paiement du payeur
GET /stores/{id}/balance Disponible opérable seulement

Erreurs fréquentes

Code Quand
400 holdUntil invalide ; escrowId envoyé à /payment_requests ou /payment_links (escrow_id_not_supported)
422 data qui ne correspond pas à type
403 Clé Owner / Dashboard ou clé entreprise du SP
404 Magasin ou escrow introuvable, ou autre marchand
409 escrow_already_bound : un encaissement est déjà en cours. escrow_not_collectable : statut qui ne permet pas d'encaisser, holdUntil passé, ou tentative précédente encore payable. payment_request_exists_with_reference : reference déjà utilisée

Point d’attention.

Libérez ou remboursez ses pots held. Le détachement révoque les clés plateforme, et plus aucune clé ne peut agir sur ces pots.