Aller au contenu

Déclencher des retraits

Un retrait envoie des fonds depuis le solde available de votre workspace vers une adresse externe. C’est la moitié du produit où vivent les contrôles de sécurité : ce guide couvre donc le mur que vous rencontrerez avant la première requête, pas seulement la requête elle-même.

Les retraits sont un flux de production : les clés Sandbox ne reçoivent volontairement pas les scopes de retrait (le pourquoi est dans Passage en production), et la liste d’autorisation ci-dessous s’applique sur le mainnet.

Avant le premier retrait : la liste d’autorisation

Section intitulée « Avant le premier retrait : la liste d’autorisation »

Un workspace en production démarre avec la liste d’autorisation des retraits ACTIVE. Un retrait vers une adresse qui n’est pas un bénéficiaire approuvé est refusé en 422, quelle que soit la qualité du reste de la requête.

Les bénéficiaires se gèrent sur la page Security du tableau de bord, pas par cette API, parce qu’ajouter une destination de paiement est une action protégée : elle exige un second facteur fort, et la nouvelle adresse ne devient utilisable qu’après un délai de sécurité (24 h par défaut). Vos serveurs et vos propriétaires en sont informés : les webhooks BENEFICIARY_ADDED et BENEFICIARY_REMOVED partent, et tout changement qui affaiblit la politique déclenche WITHDRAWAL_POLICY_CHANGED avec data.weakened: true.

Anticipez : ajoutez les bénéficiaires que vous paierez avant d’en avoir besoin, pour que le délai de sécurité soit écoulé au moment du premier vrai retrait.

POST /v2/fees/quote (scope fees:read) chiffre un retrait avec le moteur exact qui le réglera : envoyez direction, chain, asset et amount, et lisez les frais et le net. Utilisez mode: "gross_up" pour la question inverse : combien retirer pour que le destinataire touche un montant cible. Le devis est indicatif ; les frais engageants sont calculés au règlement.

POST /v2/withdrawals (scope withdrawals:write). Envoyez toujours un en-tête Idempotency-Key ; la section cohérence plus bas repose dessus.

Requête :

{
"chain": "TRON",
"asset": "USDT_TRON",
"amount": "50",
"destination_address": "TExternalRecipientAddress123456789"
}

Réponse (201) :

{
"id": "e5f6a7b8-0000-0000-0000-000000000005",
"chain": "TRON_MAINNET",
"asset": "USDT_TRON",
"amount": "50.000000",
"fee": "0.900000",
"net_amount": "49.100000",
"status": "PENDING",
"idempotency_key": "3e1f8a20-6a5c-4d5e-9b2a-8c7d6e5f4a3b",
"requested_at": "2026-08-09T11:00:00.000Z"
}

Le montant demandé est verrouillé immédiatement : il passe d’available à locked dès que la requête est acceptée, pour qu’il ne puisse pas être dépensé deux fois pendant que le retrait est en vol. fee et net_amount sont des estimations à ce stade ; les chiffres définitifs arrivent avec WITHDRAWAL_CONFIRMED.

Parcours d’un retrait : requête et verrouillage, retenues optionnelles, diffusion, puis confirmé ou échoué avec fonds restitués

Suivre le retrait : chaque statut, et le webhook qui vous l’annonce

Section intitulée « Suivre le retrait : chaque statut, et le webhook qui vous l’annonce »
Statut Signification Webhook Final ?
PENDING Accepté, fonds verrouillés, en file pour diffusion (la réponse de création elle-même) non
PENDING_APPROVAL Au-dessus du seuil d’approbation, en attente d’un second membre WITHDRAWAL_PENDING_APPROVAL non
UNDER_REVIEW Destination retenue pour revue de conformité WITHDRAWAL_HELD non
PROCESSING Signé et diffusé ; txid est désormais renseigné WITHDRAWAL_PROCESSING non
CONFIRMING Accumule les confirmations on-chain (aucun) non
CONFIRMED Confirmé on-chain ; frais définitifs, détail dans le payload WITHDRAWAL_CONFIRMED oui
FAILED Échec on-chain ; les fonds verrouillés reviennent en available WITHDRAWAL_FAILED oui
CANCELLED Annulé avant diffusion ; les fonds reviennent en available WITHDRAWAL_CANCELLED oui

Traitez CONFIRMED, FAILED et CANCELLED comme finaux : ils ne changeront plus. Tout le reste est transitoire, et l’issue terminale arrive toujours sous l’une de ces trois formes.

Deux des états transitoires impliquent des humains, et votre intégration doit s’attendre aux deux :

  • PENDING_APPROVAL (double validation). Si la politique de votre workspace fixe un seuil d’approbation, un retrait au-dessus attend l’approbation d’un second membre dans le tableau de bord ; le membre qui l’a créé ne peut jamais être son approbateur. Approuvé, il repasse en PENDING et suit le flux normal. Rejeté, il termine CANCELLED avec les fonds restitués et WITHDRAWAL_CANCELLED part. Resté au-delà de la fenêtre d’approbation, il expire et les fonds verrouillés reviennent en available ; sondez le retrait si vous devez détecter ce cas rapidement.
  • UNDER_REVIEW (retenue de conformité). Une destination qui correspond à une liste de filtrage retient le retrait pour revue au lieu de le signer. Libéré, il poursuit ; rejeté, il termine CANCELLED avec les fonds restitués. Le payload de WITHDRAWAL_HELD porte le statut et les champs standard, rien sur le filtrage lui-même.

DELETE /v2/withdrawals/{id} annule un retrait qui n’a pas été diffusé (PENDING ou PENDING_APPROVAL). Les fonds verrouillés reviennent en available et WITHDRAWAL_CANCELLED part. Une fois PROCESSING, la transaction est sur le réseau et ne peut plus être annulée ; attendez l’état terminal.

Chaque refus porte un code ; branchez-vous dessus, jamais sur le message. Ceux que cet endpoint renvoie, avec la conduite à tenir, sont regroupés dans le catalogue des codes d’erreur ; les plus fréquents sont le solde insuffisant, une destination mal formée pour la blockchain, une destination qui est une adresse gérée par CowriePay (retirez vers des adresses externes uniquement), une blockchain pas encore ouverte, un gel de conformité du workspace, et les refus mainnet de liste d’autorisation et de limites décrits plus haut.

L’échec qui coûte de l’argent est l’échec ambigu : vous avez envoyé la requête, la connexion est morte, et vous ignorez si le retrait existe. L’en-tête Idempotency-Key existe exactement pour cela ; envoyez-le à chaque requête et l’ambiguïté disparaît :

key = stored_key_for(payout) # générée une fois par intention, persistée
response = POST /v2/withdrawals (Idempotency-Key: key)
si timeout ou erreur réseau :
réessayez le POST avec la MÊME clé
-> 201 : il n'existait pas ; créé maintenant
-> 200 + Idempotent-Replay: true : il existait ; voici l'original
-> 409 : encore en vol ; patientez, réessayez la MÊME clé
-> 422 IDEMPOTENCY_MISMATCH : votre corps de retentative diffère ; bug de votre côté, ne forcez pas

Ne générez jamais une clé neuve à la retentative : c’est ainsi qu’un paiement devient deux. Si une requête est partie sans clé et sans réponse, réconciliez avant de réessayer : listez les retraits récents (GET /v2/withdrawals) et cherchez votre montant et votre destination avant de créer quoi que ce soit.

  • Webhooks : la vérification de signature et le contrat de déduplication sur lesquels repose votre suivi de statuts.
  • Encaisser des dépôts : l’autre moitié du flux d’argent.
  • Passage en production : la liste de contrôle complète du premier jour, liste d’autorisation comprise.