# Create Withdrawal

> Create a withdrawal from a token balance to a recipient wallet, choose the payout network, sign the request, and track its status.

`POST /v1/withdrawals`

:::endpoint
POST /v1/withdrawals
:::

**API key:** `Payout`

:::danger
Withdrawals are irreversible once the transaction is signed and broadcast to the chain. Verify `destination_address` and `network` before you send — a request lands in `created` and can be [cancelled](/docs/withdrawals/cancel.md) only until execution starts, which may be moments later. After broadcast there is no chargeback path and a wrong address means lost funds.
:::

A withdrawal is not queued for a payout run: execution starts as soon as the request is accepted. How long the transfer then takes to land is the payout network's own speed under current chain conditions.

## Request body

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `amount` | string | Yes | Withdrawal amount as a decimal string (e.g. `"100.00"`). It may carry no more decimal places than the token has on the chosen `network` — six for `USDT` on `TRC20`. A longer amount is refused with `400 amount_too_many_decimals`, not rounded |
| `currency` | string | Yes | Crypto asset symbol. Any asset you hold a balance in can be withdrawn; the available networks depend on the asset (see [Supported Currencies](/docs/supported-currencies.md)) |
| `network` | string | Yes | Payout network code — `TRC20`, `ERC20`, `BEP20`, `POLYGON`, `ARBITRUM`, `OPTIMISM`, `BASE`, `TON`, `AVAX`, `SOL`, or `PLASMA`. Eleven of the thirteen networks that accept payments: `NEAR` and `SUI` take deposits only and cannot be whitelisted as a destination (see [Supported Currencies](/docs/supported-currencies.md)) |
| `destination_address` | string | Yes | Destination wallet address. While the merchant's withdrawal whitelist is on (the default), it must already be on that list; with the whitelist off, any valid address on the chosen network is accepted |
| `external_order_id` | string | Yes | Your payout identifier (max 200 chars), unique per merchant. Used for idempotency — a repeat with the same value returns the existing withdrawal instead of sending again |

## Example request

```json
{
  "amount": "100.00",
  "currency": "USDT",
  "network": "TRC20",
  "destination_address": "TN3W4H6rK2ce4vX9YnFQHwKENnHjoxb3m9",
  "external_order_id": "payout-001"
}
```

## Code examples

Each tab uses an official SDK with a Payout key. Signing, serialization, safe retries, and typed errors are handled by the SDK.

```curl
API_KEY_ID="rk_live_xxxxxxxxxxxx"
API_SECRET="sk_live_xxxxxxxxxxxx"
BODY='{"destination_address":"TN3W4H6rK2ce4vX9YnFQHwKENnHjoxb3m9","network":"TRC20","currency":"USDT","amount":"50.00","external_order_id":"payout-123"}'

TS=$(date +%s)
BODY_HASH=$(printf '%s' "$BODY" | openssl dgst -sha256 -hex | sed 's/^.* //')
SIGNATURE=$(printf '%s\n%s\n%s\n%s\n%s' "$TS" POST /v1/withdrawals '' "$BODY_HASH" \
  | openssl dgst -sha256 -hmac "$API_SECRET" -binary | base64)

curl -sS https://api.paymos.io/v1/withdrawals \
  -H "Authorization: HMAC-SHA256 $API_KEY_ID:$SIGNATURE" \
  -H "X-Request-Timestamp: $TS" \
  -H "Content-Type: application/json" \
  -d "$BODY"
```

```javascript
import { Paymos, externalOrderId } from '@paymos/sdk';

const paymos = new Paymos({
  apiKey: process.env.PAYMOS_PAYOUT_KEY,
  apiSecret: process.env.PAYMOS_API_SECRET,
});
const withdrawal = await paymos.withdrawals.create({
  destinationAddress: 'TN3W4H6rK2ce4vX9YnFQHwKENnHjoxb3m9',
  network: 'TRC20',
  currency: 'USDT',
  amount: '50.00',
  externalOrderId: externalOrderId('payout'),
});

console.log(withdrawal.withdrawalId, withdrawal.status);
```

```python
import os

from paymos import Paymos

paymos = Paymos(
    api_key=os.environ["PAYMOS_PAYOUT_KEY"],
    api_secret=os.environ["PAYMOS_API_SECRET"],
)
withdrawal = paymos.withdrawals.create(
    destination_address="TN3W4H6rK2ce4vX9YnFQHwKENnHjoxb3m9",
    network="TRC20",
    currency="USDT",
    amount="50.00",
    external_order_id="payout-001",
)

print(withdrawal["withdrawal_id"], withdrawal["status"])
```

```php
<?php
use Paymos\Client;
use Paymos\ClientConfig;
use Paymos\IdempotencyKey;

$paymos = new Client(new ClientConfig(
    getenv('PAYMOS_PAYOUT_KEY'),
    getenv('PAYMOS_API_SECRET')
));
$withdrawal = $paymos->withdrawals()->create(array(
    'destination_address' => 'TN3W4H6rK2ce4vX9YnFQHwKENnHjoxb3m9',
    'network' => 'TRC20',
    'currency' => 'USDT',
    'amount' => '50.00',
    'external_order_id' => IdempotencyKey::externalOrderId('payout'),
));

echo $withdrawal['withdrawal_id'] . ' ' . $withdrawal['status'];
```

```go
package main

import (
	"context"
	"fmt"
	"os"

	paymos "github.com/Paymos-labs/go-sdk/v2"
)

func main() {
	client, err := paymos.NewClient(os.Getenv("PAYMOS_PAYOUT_KEY"), os.Getenv("PAYMOS_API_SECRET"))
	if err != nil {
		panic(err)
	}
	withdrawal, err := client.Withdrawals.Create(context.Background(), paymos.CreateWithdrawalParams{
		DestinationAddress: "TN3W4H6rK2ce4vX9YnFQHwKENnHjoxb3m9",
		Network:            "TRC20",
		Currency:           "USDT",
		Amount:             "50.00",
		ExternalOrderID:    "payout-001",
	})
	if err != nil {
		panic(err)
	}
	fmt.Println(withdrawal.WithdrawalID, withdrawal.Status)
}
```

```csharp
using Paymos;

using var paymos = new PaymosClient(
    Environment.GetEnvironmentVariable("PAYMOS_PAYOUT_KEY")!,
    Environment.GetEnvironmentVariable("PAYMOS_API_SECRET")!);

var withdrawal = await paymos.Withdrawals.CreateAsync(new CreateWithdrawalRequest(
    DestinationAddress: "TN3W4H6rK2ce4vX9YnFQHwKENnHjoxb3m9",
    Network: "TRC20",
    Currency: "USDT",
    Amount: "50.00",
    ExternalOrderId: "payout-001"));

Console.WriteLine($"{withdrawal.WithdrawalId} {withdrawal.Status}");
```

```java
import io.paymos.CreateWithdrawalRequest;
import io.paymos.PaymosClient;
import io.paymos.Withdrawal;

PaymosClient paymos = new PaymosClient(
    System.getenv("PAYMOS_PAYOUT_KEY"),
    System.getenv("PAYMOS_API_SECRET"));

Withdrawal withdrawal = paymos.withdrawals.create(new CreateWithdrawalRequest(
    "TN3W4H6rK2ce4vX9YnFQHwKENnHjoxb3m9",
    "TRC20",
    "USDT",
    "50.00",
    "payout-001"));

System.out.println(withdrawal.withdrawalId() + " " + withdrawal.status());
```

```ruby
require 'paymos'

paymos = Paymos::Client.new(
  api_key: ENV.fetch('PAYMOS_PAYOUT_KEY'),
  api_secret: ENV.fetch('PAYMOS_API_SECRET')
)
withdrawal = paymos.withdrawals.create(
  destination_address: 'TN3W4H6rK2ce4vX9YnFQHwKENnHjoxb3m9',
  network: 'TRC20',
  currency: 'USDT',
  amount: '50.00',
  external_order_id: 'payout-001'
)

puts "#{withdrawal.withdrawal_id} #{withdrawal.status}"
```

```rust
use paymos::{CreateWithdrawalRequest, PaymosClient};

let paymos = PaymosClient::new(
    std::env::var("PAYMOS_PAYOUT_KEY")?,
    std::env::var("PAYMOS_API_SECRET")?,
)?;
let withdrawal = paymos
    .withdrawals()
    .create(&CreateWithdrawalRequest {
        destination_address: "TN3W4H6rK2ce4vX9YnFQHwKENnHjoxb3m9".to_owned(),
        network: "TRC20".to_owned(),
        currency: "USDT".to_owned(),
        amount: "50.00".to_owned(),
        external_order_id: "payout-001".to_owned(),
    })
    .await?;

println!("{} {}", withdrawal.withdrawal_id, withdrawal.status);
```

## Response (201 Created / 200 if idempotent match)

```json
{
  "withdrawal_id": "wdr_2M8K6Q4P9X1Z7A3B",
  "external_order_id": "payout-001",
  "status": "created",
  "is_final": false,
  "is_test": true,
  "amount": "100.00",
  "currency": "USDT",
  "network": "TRC20",
  "destination_address": "TN3W4H6rK2ce4vX9YnFQHwKENnHjoxb3m9",
  "created_at": 1739289600
}
```

## Response fields

| Field | Type | Description |
|-------|------|-------------|
| `withdrawal_id` | string | Prefixed withdrawal identifier (`wdr_...`) |
| `external_order_id` | string | Your own payout reference, echoed back from the create request |
| `status` | string | Withdrawal status (snake_case — see [Withdrawal Statuses](/docs/withdrawals/get.md#withdrawal-statuses)) |
| `is_final` | boolean | `true` once the withdrawal reached a terminal state (`completed`, `failed`, `cancelled`) |
| `is_test` | boolean | `true` for sandbox withdrawals |
| `amount` | string | Withdrawal amount |
| `fee` | string? | Flat per-token withdrawal fee, resolved from the platform token catalog, in the same asset as `amount`. Fixed per token rather than a live gas quote — it is set below the network's own cost for that route and carries no Paymos commission. Omitted when there is no fee |
| `currency` | string | Crypto asset symbol |
| `network` | string | Blockchain network code |
| `destination_address` | string | Destination address |
| `tx_hash` | string? | On-chain hash of the transfer that settled the payout. Set only once `status` is `completed` |
| `explorer_url` | string? | Direct link to `tx_hash` in the network's block explorer (e.g. Tronscan, Etherscan). Set only when `tx_hash` is set and the network has a configured explorer. Render verbatim as a clickable link |
| `created_at` | integer | Unix timestamp, seconds |
| `completed_at` | integer? | Unix timestamp, seconds. Set once the payout completes |
| `failed_at` | integer? | Unix timestamp, seconds. Set once the payout fails |
| `cancelled_at` | integer? | Unix timestamp, seconds. Set once the withdrawal is cancelled |

## Idempotency

Always send `external_order_id` on a withdrawal. A retry of the same call with the same `external_order_id` returns the existing withdrawal — never a second transfer. The response status is `200 OK` (without a `Location` header) for an idempotent match, `201 Created` for a fresh withdrawal.

This is your safety net against flaky networks, crashed workers, and retry loops. Use the same id for the same payout; pick a new id for a new one.

## Errors

A Payment (`pk_`) key is rejected with `403 payout_key_required` — but only on a well-formed request: the body is parsed before the key type is checked, so a missing or unparseable body returns `400` first. See [Error Codes](/docs/errors/codes.md) for the full catalogue. The `type` URI in every error response deep-links to the matching row.
