# API de retrait

> Envoyez des retraits depuis votre solde marchand vers n'importe quelle adresse blockchain.

L'API de retrait vous permet de retirer par programmation des fonds depuis votre solde marchand vers n'importe quelle adresse blockchain.

> **WARNING:** Pour tous les endpoints de retrait, vous devez utiliser une **Payout API key** distincte pour générer la signature `sign`. Cette clé est différente de votre API key classique et doit être générée dans les paramètres de votre projet.

## Créer un retrait

Crée une demande de retrait depuis votre solde marchand.

`POST /v1/payout`

### Paramètres de la requête

| Champ | Type | Requis | Description |
|-------|------|--------|-------------|
| `currency` | string | oui | Devise du retrait (voir [References](/docs/references)) |
| `network` | string | oui | Code de réseau (voir [References](/docs/references)) |
| `amount` | string | oui | Montant du retrait |
| `to_address` | string | oui | Adresse blockchain du destinataire |
| `order_id` | string | non | **Clé d'idempotence** — unique au sein d'un projet. Un `POST` répété avec le même `order_id` ne crée pas de nouveau retrait — le retrait existant est renvoyé à la place |
| `url_callback` | string | non | URL pour les webhooks de retrait. Omettez ce champ pour désactiver les webhooks pour ce retrait |
| `memo` | string \| null | non | Tag de destination / mémo. Actuellement utilisé uniquement par les réseaux **TON** et **SOL** ; max. 255 caractères |
| `from_currency` | string | non | Solde source à débiter et à convertir automatiquement en `currency` au moment du retrait. Vous permet de payer en actifs volatils (`BTC`, `ETH`, …) tout en gardant votre solde dans un stablecoin comme `USDT` — vous n'avez pas à détenir vous-même la crypto volatile. Passez `"USDT"` pour débiter le solde USDT |
| `fee_option` | string | non | Mode de facturation des frais. `deduct` (par défaut) — frais de réseau + frais de plateforme déduits de `amount`, le destinataire reçoit `amount - fees`. `add` — frais ajoutés en supplément, le marchand est débité de `amount + fees`, le destinataire reçoit exactement `amount` |

> **INFO:** **Idempotence.** Au sein d'un projet, un retrait est unique par `order_id`. Renvoyer le même `POST` avec le même `order_id` est **sûr** — l'API renvoie le retrait existant au lieu d'en créer un doublon. Passez toujours un `order_id` pour les retraits en production.

### Exemples de requête

<CodeTabs langs="curl,php,js,python,go">

```bash
curl -X POST https://api.2328.io/api/v1/payout \
  -H "Content-Type: application/json" \
  -H "User-Agent: MyShop/1.0 (+https://myshop.example)" \
  -H "project: YOUR_PROJECT_UUID" \
  -H "sign: YOUR_HMAC_SIGNATURE" \
  -d '{"currency":"TRX","network":"TRX-TRC20","amount":"1.00","to_address":"TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t","order_id":"9ed25264-8be4-439f-acf5-2a8732538d27","url_callback":"https://your-site.com/webhook/payout","memo":null,"fee_option":"deduct"}'
```

```php
<?php
function apiSign(string $body, string $apiKey): string {
    return hash_hmac('sha256', base64_encode($body), $apiKey);
}

$project = 'YOUR_PROJECT_UUID';
$apiKey  = 'YOUR_PAYOUT_API_KEY';

$data = [
    'currency'     => 'TRX',
    'network'      => 'TRX-TRC20',
    'amount'       => '1.00',
    'to_address'   => 'TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t',
    'order_id'     => '9ed25264-8be4-439f-acf5-2a8732538d27',
    'url_callback' => 'https://your-site.com/webhook/payout',
    'memo'         => null,
    'fee_option'   => 'deduct',
];

$body = json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
$sign = apiSign($body, $apiKey);

$ch = curl_init('https://api.2328.io/api/v1/payout');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST           => true,
    CURLOPT_POSTFIELDS     => $body,
    CURLOPT_HTTPHEADER     => [
        'Content-Type: application/json',
        'User-Agent: MyShop/1.0 (+https://myshop.example)',
        "project: $project",
        "sign: $sign",
    ],
]);
$response = json_decode(curl_exec($ch), true);
```

```javascript
import { createHmac } from "crypto";

function apiSign(body, apiKey) {
  const base64 = Buffer.from(body, "utf8").toString("base64");
  return createHmac("sha256", apiKey).update(base64).digest("hex");
}

const PROJECT_UUID    = "YOUR_PROJECT_UUID";
const PAYOUT_API_KEY  = process.env.PAYOUT_API_KEY;

const data = {
  currency:     "TRX",
  network:      "TRX-TRC20",
  amount:       "1.00",
  to_address:   "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t",
  order_id:     "9ed25264-8be4-439f-acf5-2a8732538d27",
  url_callback: "https://your-site.com/webhook/payout",
  memo:         null,
  fee_option:   "deduct",
};

const body = JSON.stringify(data);
const sign = apiSign(body, PAYOUT_API_KEY);

const res = await fetch("https://api.2328.io/api/v1/payout", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "User-Agent":   "MyShop/1.0 (+https://myshop.example)",
    project:        PROJECT_UUID,
    sign,
  },
  body,
});
const json = await res.json();
```

```python
import json
import hmac
import hashlib
import base64
import httpx


def api_sign(body: str, api_key: str) -> str:
    b64 = base64.b64encode(body.encode("utf-8")).decode()
    return hmac.new(api_key.encode(), b64.encode(), hashlib.sha256).hexdigest()


PROJECT_UUID    = "YOUR_PROJECT_UUID"
PAYOUT_API_KEY  = "YOUR_PAYOUT_API_KEY"

data = {
    "currency":     "TRX",
    "network":      "TRX-TRC20",
    "amount":       "1.00",
    "to_address":   "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t",
    "order_id":     "9ed25264-8be4-439f-acf5-2a8732538d27",
    "url_callback": "https://your-site.com/webhook/payout",
    "memo":         None,
    "fee_option":   "deduct",
}

body = json.dumps(data, separators=(",", ":"), ensure_ascii=False)
sign = api_sign(body, PAYOUT_API_KEY)

r = httpx.post(
    "https://api.2328.io/api/v1/payout",
    headers={
        "Content-Type": "application/json",
        "User-Agent":   "MyShop/1.0 (+https://myshop.example)",
        "project":      PROJECT_UUID,
        "sign":         sign,
    },
    content=body.encode("utf-8"),
)
response = r.json()
```

```go
package main

import (
    "bytes"
    "crypto/hmac"
    "crypto/sha256"
    "encoding/base64"
    "encoding/hex"
    "encoding/json"
    "net/http"
)

func apiSign(body []byte, apiKey string) string {
    b64 := base64.StdEncoding.EncodeToString(body)
    h := hmac.New(sha256.New, []byte(apiKey))
    h.Write([]byte(b64))
    return hex.EncodeToString(h.Sum(nil))
}

func marshalCanonical(v any) ([]byte, error) {
    var buf bytes.Buffer
    enc := json.NewEncoder(&buf)
    enc.SetEscapeHTML(false)
    if err := enc.Encode(v); err != nil {
        return nil, err
    }
    return bytes.TrimRight(buf.Bytes(), "\n"), nil
}

type CreatePayout struct {
    Currency    string  `json:"currency"`
    Network     string  `json:"network"`
    Amount      string  `json:"amount"`
    ToAddress   string  `json:"to_address"`
    OrderID     string  `json:"order_id"`
    URLCallback string  `json:"url_callback"`
    Memo        *string `json:"memo"`
    FeeOption   string  `json:"fee_option"`
}

func main() {
    const projectUUID   = "YOUR_PROJECT_UUID"
    const payoutAPIKey  = "YOUR_PAYOUT_API_KEY"

    data := CreatePayout{
        Currency:    "TRX",
        Network:     "TRX-TRC20",
        Amount:      "1.00",
        ToAddress:   "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t",
        OrderID:     "9ed25264-8be4-439f-acf5-2a8732538d27",
        URLCallback: "https://your-site.com/webhook/payout",
        Memo:        nil,
        FeeOption:   "deduct",
    }

    body, err := marshalCanonical(data)
    if err != nil {
        panic(err)
    }
    sign := apiSign(body, payoutAPIKey)

    req, _ := http.NewRequest("POST",
        "https://api.2328.io/api/v1/payout",
        bytes.NewReader(body))
    req.Header.Set("Content-Type", "application/json")
    req.Header.Set("User-Agent", "MyShop/1.0 (+https://myshop.example)")
    req.Header.Set("project", projectUUID)
    req.Header.Set("sign", sign)

    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        panic(err)
    }
    defer resp.Body.Close()
}
```

</CodeTabs>

### Exemple de réponse

```json
{
  "state": 0,
  "result": {
    "uuid": "019dea62-1727-72aa-ac2c-eaf2ade193ef",
    "order_id": "9ed25264-8be4-439f-acf5-2a8732538d27",
    "status": "pending",
    "currency": "TRX",
    "network": "TRX-TRC20",
    "amount": "1.00",
    "merchant_amount": "1",
    "network_amount": "0.89",
    "amount_usd": "0.33",
    "to_address": "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t",
    "memo": null,
    "txid": null,
    "block_number": null,
    "error_type": null,
    "created_at": "2026-05-02T23:29:50+03:00",
    "updated_at": "2026-05-02T23:29:50+03:00"
  }
}
```

> **INFO:** **Frais.** Par défaut, `fee_option: deduct` — les frais de réseau + frais de plateforme sont déduits de `amount` (le destinataire reçoit `amount - fees`). Passez `fee_option: add` pour facturer les frais en supplément — le destinataire reçoit exactement `amount` et le marchand est débité de `amount + fees`.

## Calculer un retrait

Estime les montants et frais d'un retrait **sans créer de retrait** ni débiter votre solde. Utilisez-le pour afficher à l'utilisateur le montant exact qu'il recevra (ou paiera) avant qu'il confirme.

`POST /v1/payout/calc`

### Paramètres de la requête

Identiques à [Créer un retrait](#create-payout) — mêmes champs, même signature. `order_id`, `url_callback`, `to_address` et `memo` sont acceptés mais ignorés : aucun retrait n'est enregistré et aucun callback n'est envoyé.

### Exemple de requête

```bash
curl -X POST https://api.2328.io/api/v1/payout/calc \
  -H "Content-Type: application/json" \
  -H "User-Agent: MyShop/1.0 (+https://myshop.example)" \
  -H "project: YOUR_PROJECT_UUID" \
  -H "sign: YOUR_HMAC_SIGNATURE" \
  -d '{"currency":"USDT","network":"TRX-TRC20","amount":"100","fee_option":"add"}'
```

### Exemple de réponse

```json
{
  "state": 0,
  "result": {
    "currency": "USDT",
    "network": "TRX-TRC20",
    "amount": "100",
    "fee_option": "add",
    "merchant_amount": "103.00000000",
    "network_amount": "100",
    "total_fee": "3.00000000",
    "total_fee_usd": "3.00000000"
  }
}
```

> **INFO:** **Aperçu uniquement.** Ce endpoint est en lecture seule — aucun solde n'est débité et aucun enregistrement de retrait n'est créé. Appelez-le autant que nécessaire pour afficher le détail des frais dans votre interface.

## Statut du retrait

Récupérez le statut d'une demande de retrait.

`GET /v1/payout/status/{uuid}`

### Paramètres de chemin

| Champ | Type | Requis | Description |
|-------|------|--------|-------------|
| `uuid` | string | oui | UUID du retrait (depuis `result.uuid` à la création) |

### Exemple de réponse

```json
{
  "state": 0,
  "result": {
    "uuid": "019dff1f-0dbd-7277-8d45-271e7775388f",
    "order_id": "4dfdcc84402b1185b71cbe399321533e",
    "status": "completed",
    "currency": "TRX",
    "network": "TRX-TRC20",
    "amount": "3.00",
    "merchant_amount": "3.00",
    "network_amount": "3.00",
    "amount_usd": "1.04",
    "to_address": "THauRv5tcucQRohXg8NiyGTk16DX1XQG5x",
    "memo": null,
    "txid": "9242e533703704ef3eaba840f70b4a26333e72c943377ee375fea17badb53def",
    "block_number": null,
    "error_type": null,
    "created_at": "2026-05-07T00:08:38+03:00",
    "updated_at": "2026-05-07T00:08:54+03:00",
    "from_currency": "USDT",
    "debited_amount": "1.050735",
    "debited_currency": "USDT"
  }
}
```

> **INFO:** Pour cette requête GET, la signature est calculée à partir d'un corps vide :
`hash_hmac('sha256', base64_encode(''), $apiKey)`

## Champs de réponse

Champs renvoyés dans `result` par `POST /v1/payout` et `GET /v1/payout/status/{uuid}` :

| Champ | Type | Description |
|-------|------|-------------|
| `uuid` | string | UUID du retrait attribué par le système |
| `order_id` | string | Votre identifiant interne de retrait (unique au sein du projet) |
| `status` | string | Statut actuel du retrait (voir ci-dessous) |
| `currency` | string | Devise du retrait |
| `network` | string | Code de réseau |
| `amount` | string | Montant du retrait demandé |
| `merchant_amount` | string | Montant débité du solde marchand |
| `network_amount` | string | Montant réellement envoyé on-chain (après frais de réseau + plateforme) |
| `amount_usd` | string | Équivalent en USD du montant du retrait |
| `to_address` | string | Adresse blockchain du destinataire |
| `memo` | string \| null | Tag de destination / mémo (TON, SOL). `null` sinon |
| `txid` | string \| null | Hash de la transaction blockchain. `null` jusqu'à l'envoi de la transaction |
| `block_number` | int \| null | Numéro du bloc dans lequel la transaction a été incluse. `null` tant qu'elle n'est pas incluse |
| `error_type` | string \| null | Raison de l'échec lorsque `status = failed` (voir Types d'erreurs ci-dessous). `null` sinon |
| `created_at` | string (ISO 8601) | Date de création du retrait |
| `updated_at` | string (ISO 8601) | Date du dernier changement de statut |
| `from_currency` | string \| null | Solde source débité pour le retrait lorsqu'une conversion automatique a été utilisée (par ex. `USDT` pour un retrait en `BTC`). `null` s'il n'y a pas eu de conversion |
| `debited_amount` | string \| null | Montant réellement débité du solde source après conversion. Présent uniquement lorsque la conversion automatique est utilisée |
| `debited_currency` | string \| null | Devise de `debited_amount` — le solde depuis lequel les fonds ont été débités |

## Statuts de retrait

Le champ `status` peut prendre les valeurs suivantes :

| Statut | Description |
|--------|-------------|
| `pending` | Créé, en attente de traitement |
| `completed` | Terminé avec succès — `txid` est défini |
| `failed` | Erreur d'envoi — voir `error_type` |
| `cancelled` | Annulé |

## Types d'erreurs

Lorsque `status = failed`, le champ `error_type` indique la raison :

| Code | Description |
|------|-------------|
| `aml_risk` | Retrait bloqué par les contrôles de risque AML (adresse du destinataire signalée comme à haut risque) |

## Notifications webhook

Lorsque le statut d'un retrait change, le système envoie un webhook `POST` à l'URL `url_callback` fournie lors de la création du retrait. Si `url_callback` n'a pas été fourni, aucun webhook n'est envoyé pour ce retrait.

- **Méthode :** `POST`
- **Content-Type :** `application/json`
- **Signature :** champ `sign` dans le corps de la requête, calculé avec la **Payout API key** (la même clé utilisée pour signer les requêtes de retrait).

Le payload reflète l'objet `result` de `GET /v1/payout/status/{uuid}` plus un champ `sign` pour la vérification.

### Payload

```json
{
  "uuid": "019dff1f-0dbd-7277-8d45-271e7775388f",
  "order_id": "4dfdcc84402b1185b71cbe399321533e",
  "status": "completed",
  "currency": "TRX",
  "network": "TRX-TRC20",
  "amount": "3.00",
  "merchant_amount": "3.00",
  "network_amount": "3.00",
  "amount_usd": "1.04",
  "to_address": "THauRv5tcucQRohXg8NiyGTk16DX1XQG5x",
  "memo": null,
  "txid": "9242e533703704ef3eaba840f70b4a26333e72c943377ee375fea17badb53def",
  "block_number": null,
  "error_type": null,
  "created_at": "2026-05-07T00:08:38+03:00",
  "updated_at": "2026-05-07T00:08:54+03:00",
  "from_currency": "USDT",
  "debited_amount": "1.050735",
  "debited_currency": "USDT",
  "sign": "925ad7bf3d6841864101f7cc2c7e30652e70a06cdb04dbe07a0129480000ce4a"
}
```

> **WARNING:** **Vérification de la signature.** Utilisez le même algorithme que pour les [webhooks de paiement](/docs/webhooks), mais signez avec votre **Payout API key** au lieu de l'API key classique. Retirez le champ `sign`, encodez le payload restant en JSON, puis en Base64, puis calculez `hash_hmac('sha256', $base64, $payoutApiKey)` et comparez avec le `sign` reçu.