> ## Documentation Index
> Fetch the complete documentation index at: https://docs.services.payward.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Deposits and withdrawals

> This guide walks you through enabling crypto deposits and withdrawals for your users via the Payward Services API.

## Prerequisites

* Payward Services API credentials (see [Authentication Guide](/tabs/developer-documentation/get-started/authentication))
* A verified user with at least one account

<Note>
  Only cryptocurrency deposits are currently available through the Payward Services API. Cryptocurrency withdrawals,
  fiat deposits, and fiat withdrawals are not yet available.
</Note>

## Crypto deposits

### Deposit workflow

<Steps>
  <Step title="List deposit methods">
    Query available deposit methods for the target asset.

    `GET /v1/accounts/{account_id}/funds/deposits/methods/{asset_symbol}`
  </Step>

  <Step title="List deposit addresses">
    Check whether the account already has an address for the selected method.

    `GET /v1/accounts/{account_id}/funds/deposits/addresses`
  </Step>

  <Step title="Claim deposit address">
    If needed, claim an address for the selected method. `POST /v1/accounts/{account_id}/funds/deposits/addresses`
  </Step>

  <Step title="Display address to user">
    Display the address and any required tag or memo. The user sends crypto from their external wallet to this address.
  </Step>

  <Step title="Track the deposit">
    Subscribe to `funds.deposit_*` webhook events, then reconcile deposits on the receiving account through `GET
            /v1/accounts/{account_id}/portfolio/transactions?types=deposit`.
  </Step>
</Steps>

<Note>
  Bitcoin Lightning uses one-time, amount-bound invoices instead of reusable deposit addresses. After listing methods,
  replace the address steps above with the [Bitcoin Lightning invoice flow](#bitcoin-lightning-invoice-flow).
</Note>

<Note>
  The examples below use the signing helpers from the [Authentication
  Guide](/tabs/developer-documentation/get-started/authentication). For GET requests, sign the same ordered query
  parameters that you send.
</Note>

<Warning>Addresses and memos in the examples are illustrative. Do not send funds to them.</Warning>

### Step 1: list deposit methods

Query available deposit methods for a crypto asset. Use the method's `id` from the response as `method_id` when listing or claiming an address.

<Note>
  Most cryptocurrency deposits are free, with minimum deposit amounts varying by asset. A few cryptocurrencies are
  charged an `address_setup_fee` (a one-time fee on the user's first deposit to a new address) or a per-deposit `fee`.
</Note>

<CodeGroup>
  ```python Python theme={null}
  def list_deposit_methods(account_id, asset_symbol):
      endpoint = f"/v1/accounts/{account_id}/funds/deposits/methods/{asset_symbol}"

      nonce = time.time_ns()
      signature = get_payward_signature(endpoint, None, API_SECRET, nonce)

      headers = {
          "API-Key": API_KEY,
          "API-Nonce": str(nonce),
          "API-Sign": signature,
      }

      response = requests.get(
          f"{BASE_URL}{endpoint}",
          headers=headers,
      )
      return response.json()


  methods = list_deposit_methods(account_id, "BTC")
  for m in methods["data"]:
      print(f"{m['network']} (id: {m['id']})")
  ```

  ```javascript Javascript theme={null}
  async function listDepositMethods(accountId, assetSymbol) {
    const endpoint = `/v1/accounts/${accountId}/funds/deposits/methods/${assetSymbol}`;

    const nonce = process.hrtime.bigint().toString();
    const signature = getPaywardSignature(endpoint, null, API_SECRET, nonce);

    const response = await fetch(`${BASE_URL}${endpoint}`, {
      method: 'GET',
      headers: {
        'API-Key': API_KEY,
        'API-Nonce': nonce,
        'API-Sign': signature,
      },
    });

    return response.json();
  }

  const methods = await listDepositMethods(accountId, 'BTC');
  for (const m of methods.data) {
    console.log(`${m.network} (id: ${m.id})`);
  }
  ```
</CodeGroup>

Use `page_token` from `next_page_token` to request the next page. Omit it for the first page.

#### Response example

```json theme={null}
{
  "data": [
    {
      "id": "2fa11f79-eeba-4d4e-afda-029abff6e29e",
      "network": "Bitcoin",
      "network_info": {
        "explorer": "https://mempool.space/tx/",
        "confirmations": "3",
        "est_confirmation_time": "45"
      },
      "minimum": {
        "symbol": "BTC",
        "name": "Bitcoin",
        "type": "crypto",
        "amount": "0.0001"
      }
    }
  ]
}
```

Key fields to display to users are `network`, `method` when present, `fee`, `minimum`, and
`network_info.est_confirmation_time`. Pass the method's `id` back as `method_id` when listing or claiming an address.
Optional fee, limit, and network fields are omitted when they do not apply.

<Note>
  Each asset can have a maximum of five deposit addresses. Some funding methods share an address space, so the same
  address can be returned for different assets or networks. `method_id` selects a context in which the returned address
  is valid; it does not say how the address was originally claimed. List and reuse an existing address before claiming
  another.
</Note>

### Step 2: list deposit addresses

Retrieve deposit addresses available for the selected funding method. Reuse a returned address instead of creating a
new one each time.

<CodeGroup>
  ```python Python theme={null}
  def list_deposit_addresses(account_id, method_id, page_token=None):
      endpoint = f"/v1/accounts/{account_id}/funds/deposits/addresses"

      params = {
          "method_id": method_id,
      }
      if page_token:
          params["page_token"] = page_token

      nonce = time.time_ns()
      signature = get_payward_signature(endpoint, None, API_SECRET, nonce, params)

      headers = {
          "API-Key": API_KEY,
          "API-Nonce": str(nonce),
          "API-Sign": signature,
      }

      response = requests.get(
          f"{BASE_URL}{endpoint}",
          headers=headers,
          params=params,
      )
      return response.json()


  addresses = list_deposit_addresses(
      account_id, "2fa11f79-eeba-4d4e-afda-029abff6e29e"
  )
  for addr in addresses["data"]:
      print(f"Address: {addr['address']}")
  ```

  ```javascript Javascript theme={null}
  async function listDepositAddresses(accountId, methodId, pageToken = null) {
    const endpoint = `/v1/accounts/${accountId}/funds/deposits/addresses`;

    const params = { method_id: methodId };
    if (pageToken) params.page_token = pageToken;

    const nonce = process.hrtime.bigint().toString();
    const signature = getPaywardSignature(endpoint, null, API_SECRET, nonce, params);

    const searchParams = new URLSearchParams(params);
    const url = `${BASE_URL}${endpoint}?${searchParams.toString()}`;
    const response = await fetch(url, {
      method: 'GET',
      headers: {
        'API-Key': API_KEY,
        'API-Nonce': nonce,
        'API-Sign': signature,
      },
    });

    return response.json();
  }

  const addresses = await listDepositAddresses(accountId, '2fa11f79-eeba-4d4e-afda-029abff6e29e');
  for (const addr of addresses.data) {
    console.log('Address:', addr.address);
  }
  ```
</CodeGroup>

#### Response example

```json theme={null}
{
  "data": [
    {
      "address": "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh"
    }
  ]
}
```

Use `page_token` from `next_page_token` to retrieve the next page. Optional `tag`, `memo`, and `expire_time` fields are
omitted when they do not apply.

<Note>
  Completed deposits appear in `GET /v1/accounts/{account_id}/portfolio/transactions?types=deposit`. Portfolio
  visibility is asynchronous.
</Note>

### Step 3: claim a deposit address

If Step 2 returns no suitable address, claim one using the method `id` from Step 1 as `method_id`. Display the address
to the user so they can send crypto from an external wallet.

<Note>
  You can claim a maximum of five deposit addresses per asset. List the account's existing addresses before claiming
  another one.
</Note>

<CodeGroup>
  ```python Python theme={null}
  import json


  def claim_deposit_address(account_id, method_id):
      endpoint = f"/v1/accounts/{account_id}/funds/deposits/addresses"
      body = json.dumps({"method_id": method_id}, separators=(",", ":"))
      nonce = time.time_ns()
      signature = get_payward_signature(endpoint, body, API_SECRET, nonce)

      headers = {
          "API-Key": API_KEY,
          "API-Nonce": str(nonce),
          "API-Sign": signature,
          "Content-Type": "application/json",
      }

      response = requests.post(
          f"{BASE_URL}{endpoint}",
          headers=headers,
          data=body,
      )
      return response.json()


  address = claim_deposit_address(
      account_id,
      "2fa11f79-eeba-4d4e-afda-029abff6e29e",
  )
  print(f"Deposit address: {address['data']['address']}")
  ```

  ```javascript Javascript theme={null}
  async function claimDepositAddress(accountId, methodId) {
    const endpoint = `/v1/accounts/${accountId}/funds/deposits/addresses`;
    const body = JSON.stringify({ method_id: methodId });
    const nonce = process.hrtime.bigint().toString();
    const signature = getPaywardSignature(endpoint, body, API_SECRET, nonce);

    const response = await fetch(`${BASE_URL}${endpoint}`, {
      method: 'POST',
      headers: {
        'API-Key': API_KEY,
        'API-Nonce': nonce,
        'API-Sign': signature,
        'Content-Type': 'application/json',
      },
      body,
    });

    return response.json();
  }

  const address = await claimDepositAddress(accountId, '2fa11f79-eeba-4d4e-afda-029abff6e29e');
  console.log('Deposit address:', address.data.address);
  ```
</CodeGroup>

```json theme={null}
{
  "data": {
    "address": "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh"
  }
}
```

<Warning>
  Address claims are not idempotent. Do not retry automatically after a timeout or another response with an unknown
  outcome. List the account's addresses before deciding whether to make another claim.
</Warning>

<Note>
  Some networks (e.g., XRP, XLM) require a tag or memo in addition to the address. If `tag` or `memo` is present in the
  response, your UI must display it and instruct the user to include it when sending funds. Deposits sent without the
  required tag/memo may be lost.
</Note>

### Bitcoin Lightning invoice flow

For the Bitcoin Lightning deposit method, create a BOLT11 invoice for the exact BTC amount the user intends to deposit.
Use the limits from the deposit-methods response to validate the amount before requesting the invoice. The selected
`account_id` is the account that receives the completed deposit.

<CodeGroup>
  ```python Python theme={null}
  def create_lightning_invoice(account_id, amount):
      endpoint = f"/v1/accounts/{account_id}/funds/deposits/lightning-invoices"
      body = json.dumps(
          {"asset_symbol": "BTC", "amount": amount},
          separators=(",", ":"),
      )
      nonce = time.time_ns()
      signature = get_payward_signature(endpoint, body, API_SECRET, nonce)

      headers = {
          "API-Key": API_KEY,
          "API-Nonce": str(nonce),
          "API-Sign": signature,
          "Content-Type": "application/json",
      }

      response = requests.post(
          f"{BASE_URL}{endpoint}",
          headers=headers,
          data=body,
      )
      return response.json()


  lightning = create_lightning_invoice(account_id, "0.0005")
  print(f"Invoice: {lightning['data']['invoice']}")
  print(f"Expires at: {lightning['data'].get('expires_at')}")
  ```

  ```javascript Javascript theme={null}
  async function createLightningInvoice(accountId, amount) {
    const endpoint = `/v1/accounts/${accountId}/funds/deposits/lightning-invoices`;
    const body = JSON.stringify({ asset_symbol: 'BTC', amount });
    const nonce = process.hrtime.bigint().toString();
    const signature = getPaywardSignature(endpoint, body, API_SECRET, nonce);

    const response = await fetch(`${BASE_URL}${endpoint}`, {
      method: 'POST',
      headers: {
        'API-Key': API_KEY,
        'API-Nonce': nonce,
        'API-Sign': signature,
        'Content-Type': 'application/json',
      },
      body,
    });

    return response.json();
  }

  const lightning = await createLightningInvoice(accountId, '0.0005');
  console.log('Invoice:', lightning.data.invoice);
  console.log('Expires at:', lightning.data.expires_at);
  ```
</CodeGroup>

```json theme={null}
{
  "data": {
    "invoice": "lnbc500u1p3xnhl2pp5jptserfk3zk4qy42tlucycrfwxhydvlemu9pqr93tuzlv9cc7g3sqdqqcqzpgxqyz5vqsp5usyc4lk9chsfp53kvcnvq456ganh60d89reykdngsmtj6yw3nhvq9qyyssqjcewm5cjwz4a6rfjx77c490yced6pemk0upkxhy89cmm7sct66k8gneanwykzgdrwrfje69h9u5u0w57rrcsysas7gadwmzxc8c6t0spjazup6",
    "expires_at": "2026-08-03T12:34:56Z"
  }
}
```

<Warning>
  A Lightning invoice is single-use, must be paid in full, and cannot be paid after `expires_at`. Invoice creation is
  not idempotent. If a request has an unknown outcome, create a new invoice; unpaid invoices expire on their own.
</Warning>

### Reusable-address UI flow

```mermaid theme={null}
flowchart LR
    A[Show Available Cryptocurrencies] --> B[Show Available Methods]
    B --> C[Show Available Addresses]
    C --> D[Create New Address]
    D --> C
    C --> E[Display Address text or QR code]
```

### Best practices

1. **Always display tag/memo:** For networks that require a tag or memo (XRP, XLM, etc.), prominently display it alongside the address. Missing tags/memos can result in lost funds.
2. **Set expectations:** Show `minimum` amounts and `est_confirmation_time` from the methods response so users know what to expect before sending funds.
3. **Use fresh responses:** Available methods, addresses and limits are user-specific and may change based on account standing, remaining limits, or regional regulations. Fetch fresh data before displaying options rather than relying on cached results.

### Deposit webhook events

Register a webhook through `POST /v1/webhooks` and subscribe to the deposit lifecycle events your integration needs:

| Event                    | Meaning                                                  |
| ------------------------ | -------------------------------------------------------- |
| `funds.deposit_received` | The deposit was received and awaits confirmations/checks |
| `funds.deposit_held`     | The deposit is held pending review or additional checks  |
| `funds.deposit_credited` | The deposit was credited and can now be used             |
| `funds.deposit_returned` | The deposit was returned to its source                   |
| `funds.deposit_failed`   | The deposit failed and will not be credited              |

All deposit lifecycle events contain `type`, `transaction_id`, `user_id`, and `account_id`. They can also contain
`amount` and `fee`:

```json theme={null}
{
  "amount": {
    "asset": "USD",
    "value": "100.00"
  },
  "fee": {
    "asset": "USD",
    "value": "0"
  }
}
```

`amount` is the total deposit amount, inclusive of the fee. PWS includes it when both the source value and its asset can
be resolved.

`fee` is optional. PWS includes it only when the source provides both the fee value and its asset and the asset can be
resolved. This includes an explicitly provided zero fee. If the fee or its asset is unavailable, PWS omits `fee` rather
than assuming that it is zero or that it uses the deposit amount's asset.

The availability of `amount` and `fee` is independent of the lifecycle event type. Treat absent fields as unavailable;
PWS does not return them as `null`.

Returned and failed deposits also contain a required `reason` field. PWS normalizes it to one of
`account_details_mismatch`, `limit_exceeded`, `duplicate`, `account_restricted`, or `other`. Other deposit lifecycle
events do not contain `reason`.

A deposit can produce multiple lifecycle events as it progresses. Treat each webhook as a notification and use the
Portfolio Transactions endpoint for reconciliation. Deliveries can be repeated, so use the `svix-id` header as the
deduplication key and return a 2xx response after accepting the event.

These events describe the lifecycle of a deposit. They are distinct from `conversion.deposit_*` events, which describe
the inbound funding leg of a conversion.

## Crypto withdrawals

Withdrawals are key-based: you save an address once, then use its `key` in each withdrawal request.

### Withdrawal workflow

<Steps>
  <Step title="List withdrawal methods">
    Query available withdrawal methods for the target asset. `GET /v1/accounts/{account_id}/funds/withdrawals/methods/     {asset_symbol}`
  </Step>

  <Step title="Validate address (optional)">
    Verify the destination address is valid before saving. `POST /v1/funds/withdrawals/addresses/validate`
  </Step>

  <Step title="Save address (create key)">
    Store the validated withdrawal address for the user. `POST /v1/accounts/{account_id}/funds/withdrawals/addresses`
  </Step>

  <Step title="Preview / submit withdrawal">
    Submit the withdrawal request. `POST /v1/accounts/{account_id}/funds/withdrawals`
  </Step>

  <Step title="Monitor status (webhook / polling)">
    Track the withdrawal through `withdrawal.status_updated` webhooks and reconcile it through `GET /v1/accounts/     {account_id}/portfolio/transactions?types=withdrawal`.
  </Step>
</Steps>

### Step 1: list withdrawal methods

Call this first to determine the method's `id` (passed as `method_id` in later requests), fee estimates, limits, and optional `fee_token`.

<CodeGroup>
  ```python Python theme={null}
  def list_withdrawal_methods(account_id, asset_symbol):
      endpoint = f"/v1/accounts/{account_id}/funds/withdrawals/methods/{asset_symbol}"

      signature = get_payward_signature(endpoint, None, API_SECRET)

      headers = {
          "API-Key": API_KEY,
          "API-Sign": signature,
      }

      response = requests.get(
          f"{BASE_URL}{endpoint}",
          headers=headers,
      )
      return response.json()
  ```

  ```javascript Javascript theme={null}
  async function listWithdrawalMethods(accountId, assetSymbol) {
    const endpoint = `/v1/accounts/${accountId}/funds/withdrawals/methods/${assetSymbol}`;

    const signature = getPaywardSignature(endpoint, null, API_SECRET);

    const response = await fetch(`${BASE_URL}${endpoint}`, {
      method: 'GET',
      headers: {
        'API-Key': API_KEY,
        'API-Sign': signature,
      },
    });

    return response.json();
  }
  ```
</CodeGroup>

#### Response example

```json theme={null}
{
  "data": [
    {
      "id": "00e4796b-a142-4589-a7c1-8927933788c9",
      "network": "Bitcoin",
      "fee": { "symbol": "BTC", "name": "Bitcoin", "type": "crypto", "amount": "0.00020000" },
      "fee_token": "wft_abc123",
      "minimum": { "symbol": "BTC", "name": "Bitcoin", "type": "crypto", "amount": "0.00050000" },
      "maximum": { "symbol": "BTC", "name": "Bitcoin", "type": "crypto", "amount": "1.00000000" }
    }
  ],
  "next_page_token": null
}
```

### Step 2: validate withdrawal address (recommended)

This endpoint validates the destination before you save it.

<CodeGroup>
  ```python Python theme={null}
  def validate_withdrawal_address(asset_symbol, method_id, address, memo=None):
      endpoint = "/v1/funds/withdrawals/addresses/validate"

      body = {
          "asset_symbol": asset_symbol,
          "method_id": method_id,
          "address": address,
          "memo": memo,
      }
      signature = get_payward_signature(endpoint, body, API_SECRET)

      headers = {
          "API-Key": API_KEY,
          "API-Sign": signature,
          "Content-Type": "application/json",
      }

      response = requests.post(
          f"{BASE_URL}{endpoint}",
          headers=headers,
          json=body,
      )
      return response.json()
  ```

  ```javascript Javascript theme={null}
  async function validateWithdrawalAddress(assetSymbol, methodId, address, memo = null) {
    const endpoint = '/v1/funds/withdrawals/addresses/validate';

    const body = { asset_symbol: assetSymbol, method_id: methodId, address, memo };
    const signature = getPaywardSignature(endpoint, body, API_SECRET);

    const response = await fetch(`${BASE_URL}${endpoint}`, {
      method: 'POST',
      headers: {
        'API-Key': API_KEY,
        'API-Sign': signature,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(body),
    });

    return response.json();
  }
  ```
</CodeGroup>

### Step 3: save withdrawal address

Save the address once and keep the returned `key` for future withdrawals.

<CodeGroup>
  ```python Python theme={null}
  def save_withdrawal_address(account_id, asset_symbol, method_id, key, address, idempotency_key, memo=None, tag=None):
      endpoint = f"/v1/accounts/{account_id}/funds/withdrawals/addresses"

      body = {
          "asset_symbol": asset_symbol,
          "method_id": method_id,
          "key": key,
          "address": address,
          "memo": memo,
          "tag": tag,
      }
      signature = get_payward_signature(endpoint, body, API_SECRET)

      headers = {
          "API-Key": API_KEY,
          "API-Sign": signature,
          "Idempotency-Key": idempotency_key,
          "Content-Type": "application/json",
      }

      response = requests.post(
          f"{BASE_URL}{endpoint}",
          headers=headers,
          json=body,
      )
      return response.json()
  ```

  ```javascript Javascript theme={null}
  async function saveWithdrawalAddress(
    accountId,
    assetSymbol,
    methodId,
    key,
    address,
    idempotencyKey,
    memo = null,
    tag = null,
  ) {
    const endpoint = `/v1/accounts/${accountId}/funds/withdrawals/addresses`;

    const body = { asset_symbol: assetSymbol, method_id: methodId, key, address, memo, tag };
    const signature = getPaywardSignature(endpoint, body, API_SECRET);

    const response = await fetch(`${BASE_URL}${endpoint}`, {
      method: 'POST',
      headers: {
        'API-Key': API_KEY,
        'API-Sign': signature,
        'Idempotency-Key': idempotencyKey,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(body),
    });

    return response.json();
  }
  ```
</CodeGroup>

### Step 4: preview and submit a withdrawal

Use `preview=true` to quote fees and totals without creating a withdrawal, then submit with `preview=false`.

<CodeGroup>
  ```python Python theme={null}
  def withdraw_funds(account_id, asset_symbol, key, amount, idempotency_key, preview=False, fee_token=None):
      endpoint = f"/v1/accounts/{account_id}/funds/withdrawals"

      body = {
          "asset_symbol": asset_symbol,
          "key": key,
          "amount": amount,
          "preview": preview,
          "fee_token": fee_token,
      }
      signature = get_payward_signature(endpoint, body, API_SECRET)

      headers = {
          "API-Key": API_KEY,
          "API-Sign": signature,
          "Idempotency-Key": idempotency_key,
          "Content-Type": "application/json",
      }

      response = requests.post(
          f"{BASE_URL}{endpoint}",
          headers=headers,
          json=body,
      )
      return response.json()
  ```

  ```javascript Javascript theme={null}
  async function withdrawFunds(accountId, assetSymbol, key, amount, idempotencyKey, preview = false, feeToken = null) {
    const endpoint = `/v1/accounts/${accountId}/funds/withdrawals`;

    const body = {
      asset_symbol: assetSymbol,
      key,
      amount,
      preview,
      fee_token: feeToken,
    };
    const signature = getPaywardSignature(endpoint, body, API_SECRET);

    const response = await fetch(`${BASE_URL}${endpoint}`, {
      method: 'POST',
      headers: {
        'API-Key': API_KEY,
        'API-Sign': signature,
        'Idempotency-Key': idempotencyKey,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(body),
    });

    return response.json();
  }
  ```
</CodeGroup>

#### Statuses

| Status    | Description                             |
| --------- | --------------------------------------- |
| `pending` | Withdrawal detected and being processed |
| `held`    | Held for review                         |
| `success` | Completed successfully                  |
| `failure` | Failed (terminal)                       |

### Withdrawal best practices

1. Use idempotency keys: Always generate a unique UUIDv4 and send it as the `Idempotency-Key` HTTP header per intended withdrawal to avoid duplicate sends on retries. Replayed responses include `Idempotent-Replayed: true`.
2. Preview first: Run a preview request immediately before submit so users can confirm `amount`, `fee`, and `total`.
3. Refresh expired fee tokens: `fee_token` values are short-lived. If a withdrawal is rejected due to an expired/invalid token, fetch withdrawal methods again (or run a fresh preview) to get a new `fee_token` and retry.
4. Persist key ownership: Store which `key` belongs to each user and enforce access checks in your app.
5. Handle memo/tag networks: For XRP/XLM-like networks, capture and persist memo/tag fields when addresses are saved.
6. Monitor and reconcile: Subscribe to `withdrawal.status_updated` and poll `GET /v1/accounts/{account_id}/portfolio/transactions?types=withdrawal` for recovery and reconciliation.

### Common errors

| HTTP status             | Cause                                                                                 | Remediation                                                                           |
| ----------------------- | ------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- |
| `400 Bad Request`       | Invalid payload (e.g. malformed `key`, `amount`, or unrecognized `asset_symbol`)      | Validate the request payload before sending                                           |
| `401 Unauthorized`      | Missing or invalid API credentials                                                    | Verify `API-Key` and `API-Sign` headers                                               |
| `404 Not Found`         | Saved withdrawal `key` does not exist                                                 | Re-list saved addresses and use an existing `key`                                     |
| `409 Conflict`          | Withdrawal `key` already exists, or `Idempotency-Key` reused with a different payload | Choose a unique `key`, or reuse the same `Idempotency-Key` only for identical retries |
| `429 Too Many Requests` | Rate limit exceeded                                                                   | Back off and retry with exponential delay                                             |

## API reference

| Endpoint                                                             | Method | Description                                                       |
| -------------------------------------------------------------------- | ------ | ----------------------------------------------------------------- |
| `/v1/accounts/{account_id}/funds/deposits/methods/{asset_symbol}`    | GET    | List deposit methods for an asset                                 |
| `/v1/accounts/{account_id}/funds/deposits/addresses`                 | POST   | Claim a deposit address                                           |
| `/v1/accounts/{account_id}/funds/deposits/addresses`                 | GET    | List existing deposit addresses                                   |
| `/v1/accounts/{account_id}/funds/deposits/lightning-invoices`        | POST   | Create a single-use Bitcoin Lightning invoice                     |
| `/v1/accounts/{account_id}/portfolio/transactions`                   | GET    | List deposit, withdrawal, and transfer activity through Portfolio |
| `/v1/accounts/{account_id}/funds/withdrawals/methods/{asset_symbol}` | GET    | List withdrawal methods for an asset                              |
| `/v1/funds/withdrawals/addresses/validate`                           | POST   | Validate a withdrawal address without saving it                   |
| `/v1/accounts/{account_id}/funds/withdrawals/addresses`              | POST   | Save a withdrawal address                                         |
| `/v1/accounts/{account_id}/funds/withdrawals/addresses`              | GET    | List saved withdrawal addresses                                   |
| `/v1/accounts/{account_id}/funds/withdrawals/addresses/{key}`        | PATCH  | Rename a saved withdrawal key                                     |
| `/v1/accounts/{account_id}/funds/withdrawals/addresses/{key}`        | DELETE | Delete a saved withdrawal address                                 |
| `/v1/accounts/{account_id}/funds/withdrawals`                        | POST   | Preview or submit a withdrawal                                    |
| `/v1/webhooks`                                                       | POST   | Register for deposit lifecycle and withdrawal webhooks            |
