---
title: Create a withdrawal
section: API Reference / Withdrawals
---
# Create a withdrawal

<span class="badge post">POST</span> `/api/v1/partner/customers/{customerId}/withdrawals`

> Required scope: `withdrawals:write`, plus the `withdrawals` capability — which
> is KYB-gated, so it turns on when the customer's verification is approved.

Sends crypto from the customer's wallet to a registered destination.

Needs **both** an `Idempotency-Key` and an `X-Withdrawal-Signature`. See
[Signing withdrawals](/getting-started/withdrawal-signing.html) for how to build
the assertion — it is the same construction for both money-out endpoints.

Register the destination with
[Register a withdrawal wallet](/api/withdrawal-wallets/create.html) first; `to`
must match one that already exists for this customer.

## Request

```bash
curl -X POST https://www.niftipay.com/api/v1/partner/customers/{customerId}/withdrawals \
  -H "Authorization: Bearer <partner_api_key>" \
  -H "Idempotency-Key: <uuid>" \
  -H "X-Withdrawal-Signature: <ed25519-assertion>" \
  -H "Content-Type: application/json" \
  -d '{ "chain": "BTC", "asset": "BTC", "amount": "0.5", "to": "bc1qdest" }'
```

| Field    | Type   | Required | Notes |
|----------|--------|----------|-------|
| `chain`  | string | yes      | Uppercased. A registered destination must exist for this chain+address. |
| `asset`  | string | yes      | Uppercased. |
| `amount` | string | yes      | Decimal string. |
| `to`     | string | yes      | Destination address. Must match a registered withdrawal wallet for this customer. |
| `network`| string | no       | If present, must be `mainnet` (anything else is refused). |

Only `chain`, `asset`, `amount`, and `to` are forwarded to the platform service; everything else in the body is ignored. `userId` is set server-side from the relationship and cannot be overridden. `destinationTag`, if any, is taken from the **registered** destination row, never from the request body.

## The merchant may have a minimum

Each merchant carries a minimum withdrawal value in EUR. A request whose gross
converts to less than that is refused, whatever the asset. The default is no
minimum, so most merchants have none — but the ones that do can set it high
(€300 is a real value in production).

There is no endpoint that reports it, so the refusal carries it instead:

```json
{ "error": { "code": "below_minimum",
             "message": "The amount is below the minimum withdrawal this merchant is configured for. See `details[0].minimum_eur`.",
             "request_id": "req-1",
             "details": [ { "minimum_eur": 300 } ] } }
```

`minimum_eur` is the threshold in euros. Compare your gross against it and
retry with a larger amount — this is a refusal you can act on without contacting
anyone. It is absent, rather than null, on the rare occasion we cannot read a
number, so you can tell "we did not say" from a value you can use.

## Response `201`

```json
{
  "request_id": "req-1",
  "api_version": "2026-08-01",
  "data": {
    "withdrawal": {
      "tx_hash": "0xabc",
      "chain": "BTC",
      "asset": "BTC",
      "gross": "0.5",
      "fee": "0.001",
      "net": "0.499",
      "gas": "0.0001",
      "to_address": "bc1qdest"
    }
  }
}
```

## Errors
`400 idempotency_key_required` when no `Idempotency-Key` header is provided.
`401 signature_required` when `X-Withdrawal-Signature` is missing or invalid.
`400 invalid_request` for a non-JSON/unreadable body or missing `chain`, `asset`, `amount`, or `to`.
`400 unsupported_network` when `network` is present but not `mainnet`.
`403 destination_not_registered` when `to` is not a registered destination for this customer (identical response for a destination registered to another merchant).
`409 insufficient_balance` when the wallet or token balance is too low.
`400 amount_too_small` when the amount is consumed by gas and fees.
`400 unsupported_asset` when the asset is unsupported on the chain.
`400 invalid_amount` when `amount` is not a valid positive number.
`409 below_minimum` when the gross is under the merchant's configured EUR minimum; `details[0].minimum_eur` carries the threshold.
`400 withdrawal_refused` for any other service refusal not in the mapped set.
`503 temporarily_unavailable` on a 5xx from the service or a transport failure (reservation is kept so a retry is safe).
`500 internal_error` for permanent unclassified failures.
`409 idempotency_key_reuse` when the same key is reused with a different body.
`409 idempotency_key_in_progress` when retried inside the window after a transport failure.
