Partners
Markdown

Create a withdrawal

POST /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 for how to build the assertion — it is the same construction for both money-out endpoints.

Register the destination with Register a withdrawal wallet first; to must match one that already exists for this customer.

Request

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:

{
  "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

{
  "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.