> ## 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 supported. Fiat deposits are not available through the Payward Services API.
</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 title="Create deposit address">
    Generate a new deposit address for the user. `POST /v1/accounts/{account_id}/funds/deposits/addresses`
  </Step>

  <Step title="Display address to user">
    Retrieve and display the deposit address in your UI. `GET /v1/accounts/{account_id}/funds/deposits/addresses`
  </Step>

  <Step title="User sends crypto">The user sends crypto from their external wallet to the deposit address.</Step>
  <Step title="Receive webhooks">Listen for deposit status updates. `deposit.status_updated`</Step>
</Steps>

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

### Step 1: list deposit methods

Query available deposit methods for a crypto asset. Use the method's `id` from the response (passed as `method_id`) when creating an address in Step 2.

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

      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()


  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 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();
  }

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

#### Response example

```json theme={null}
{
  "data": [
    {
      "id": "b3a7d8b4-9f36-4c2e-9c1e-7b9e6f9a0a11",
      "network": "Bitcoin",
      "network_info": {
        "explorer": "https://blockstream.info/tx/{txid}",
        "confirmations": "3",
        "est_confirmation_time": "30 minutes",
        "contract_address": null
      },
      "fee": { "symbol": "BTC", "name": "Bitcoin", "type": "crypto", "amount": "0.00000000" },
      "fee_percentage": "0.00",
      "minimum": { "symbol": "BTC", "name": "Bitcoin", "type": "crypto", "amount": "0.00010000" },
      "maximum": { "symbol": "BTC", "name": "Bitcoin", "type": "crypto", "amount": "50.00000000" },
      "address_setup_fee": { "symbol": "BTC", "name": "Bitcoin", "type": "crypto", "amount": "0.00000000" }
    }
  ],
  "next_page_token": null
}
```

Key fields to display to users: `network`, `fee`, `minimum`, and `network_info.est_confirmation_time`. Pass the method's `id` back as `method_id` when creating a deposit address.

### Step 2: create a deposit address

Generate a deposit address 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.

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

      body = {
          "asset_symbol": asset_symbol,
          "method_id": method_id,
      }

      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()


  address = create_deposit_address(
      account_id, "BTC", "b3a7d8b4-9f36-4c2e-9c1e-7b9e6f9a0a11", str(uuid.uuid4())
  )
  print(f"Deposit address: {address['data']['address']}")
  ```

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

    const body = {
      asset_symbol: assetSymbol,
      method_id: methodId,
    };

    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();
  }

  const address = await createDepositAddress(
    accountId,
    'BTC',
    'b3a7d8b4-9f36-4c2e-9c1e-7b9e6f9a0a11',
    crypto.randomUUID(),
  );
  console.log('Deposit address:', address.data.address);
  ```
</CodeGroup>

```json theme={null}
{
  "data": {
    "address": "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh",
    "name": "Bitcoin",
    "tag": null,
    "memo": null,
    "expire_time": null,
    "is_new": true
  }
}
```

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

### Step 3: list deposit addresses

Retrieve existing deposit addresses for a given asset and method. Use this to display previously generated addresses to users without creating new ones each time.

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

      params = {
          "asset_symbol": asset_symbol,
          "method_id": method_id,
      }
      if cursor:
          params["cursor"] = cursor

      signature = get_payward_signature(endpoint, None, API_SECRET, params)

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

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


  addresses = list_deposit_addresses(
      account_id, "BTC", "b3a7d8b4-9f36-4c2e-9c1e-7b9e6f9a0a11"
  )
  for addr in addresses["data"]:
      print(f"Address: {addr['address']}")
  ```

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

    const params = { asset_symbol: assetSymbol, method_id: methodId };
    if (cursor) params.cursor = cursor;

    const signature = getPaywardSignature(endpoint, null, API_SECRET, 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-Sign': signature,
      },
    });

    return response.json();
  }

  const addresses = await listDepositAddresses(accountId, 'BTC', 'b3a7d8b4-9f36-4c2e-9c1e-7b9e6f9a0a11');
  for (const addr of addresses.data) {
    console.log('Address:', addr.address);
  }
  ```
</CodeGroup>

#### Response example

```json theme={null}
{
  "data": [
    {
      "asset": { "symbol": "BTC", "type": "crypto" },
      "method": {
        "id": "b3a7d8b4-9f36-4c2e-9c1e-7b9e6f9a0a11",
        "name": "Bitcoin"
      },
      "address": "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh",
      "tag": null,
      "memo": null,
      "expire_time": null
    }
  ],
  "next_page_token": null
}
```

### Recommended 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.

## 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 via webhooks (`withdrawal.status_updated`) or transaction polling.
  </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 with webhooks: Subscribe to `withdrawal.status_updated` and reconcile events against your internal withdrawal records.

### 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   | Create a new deposit address                                                   |
| `/v1/accounts/{account_id}/funds/deposits/addresses`                 | GET    | List existing deposit addresses                                                |
| `/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/funds/transactions`                                             | GET    | List funding (deposit and withdrawal) transactions                             |
| `/v1/webhooks`                                                       | POST   | Register for `deposit.status_updated` and `withdrawal.status_updated` webhooks |
