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

# Conversions

> The Conversions API automates fiat ↔ crypto on/off-ramps using regulated banking rails: persistent, rule-driven conversions that fire on every matching deposit.

The Conversions API enables automated fiat ↔ crypto flows using regulated banking rails. It is designed for partners building wallet-native or payments products who want to offer their users a seamless on- and off-ramp experience — without relying on card widgets or assembling multiple providers for KYC, rails, conversion, and settlement.

The core primitive is a **conversion rule**: a persistent per-account configuration that provisions a dedicated inbound endpoint (a named virtual bank account for fiat deposits, or a deposit address for crypto deposits) and automatically converts and settles any matching deposit to the destination you have defined. Each time a matching deposit lands, the rule fires and produces a **conversion transaction**.

<Note>
  **Not what you're looking for?** Conversions handle asset exchange with automatic fund movement across rails. If you
  need to move funds between accounts in the same asset without conversion, see
  [Transfers](/tabs/developer-documentation/payments/transfers). To execute an institutional FX-style trade against a
  locked quote, see [Swap](/tabs/developer-documentation/trade/swap). For fiat-to-crypto purchases via a hosted checkout
  UI, see [Ramp](/tabs/developer-documentation/payments/ramp).
</Note>

## How it works

A conversion rule is a persistent `from` → `to` configuration scoped to a PWS account (identified by its IIBAN). When you create a rule, PWS provisions a dedicated inbound endpoint — a virtual IBAN for fiat, or a deposit address for crypto — and returns it on the rule's `from` side. You share those payment instructions with your end customer. Every matching credit to that endpoint automatically triggers a conversion and settles the converted asset to the rule's `to` destination.

Rules remain active and reusable as long as the account's KYC profile is valid. There is no per-transaction "one-shot" endpoint — all conversions flow through a standing rule.

## Creating a rule — fiat on-ramp (EUR → USDC on Polygon)

An end customer sends EUR via SEPA and automatically receives USDC on Polygon at the destination wallet you specify.

On the `from` (source) side you supply only `symbol`, `type`, and the rail (`bank_account.via` / `wallet.via`). The inbound routing is provisioned by PWS and returned on the response — any routing fields you send on `from` are ignored.

```bash theme={null}
curl -X POST "https://api.services.payward.com/v1/accounts/AA23N84GGQN4WE6I/conversions" \
  -H "API-Key: $PWS_API_KEY" \
  -H "API-Sign: $PWS_API_SIGN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "EUR autoramp to Polygon vault",
    "from": {
      "symbol": "EUR",
      "type": "bank",
      "bank_account": { "via": "sepa" }
    },
    "to": {
      "symbol": "USDC",
      "type": "wallet",
      "wallet": {
        "via": "polygon",
        "address": "0x1234567890abcdef1234567890abcdef12345678"
      }
    }
  }'
```

The response is the created rule. PWS populates `from.bank_account` with the provisioned virtual IBAN and BIC:

```json theme={null}
{
  "data": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "name": "EUR autoramp to Polygon vault",
    "from": {
      "symbol": "EUR",
      "type": "bank",
      "bank_account": {
        "via": "sepa",
        "iban": "DE89370400440532013000",
        "bic": "COBADEFFXXX"
      }
    },
    "to": {
      "symbol": "USDC",
      "type": "wallet",
      "wallet": {
        "via": "polygon",
        "address": "0x1234567890abcdef1234567890abcdef12345678"
      }
    },
    "status": "active",
    "created_at": "2026-05-25T10:30:00Z",
    "updated_at": "2026-05-25T10:30:00Z"
  }
}
```

Share `from.bank_account.iban` and `from.bank_account.bic` with the end customer as their payment instructions. Every SEPA credit to that IBAN fires the rule and produces a new conversion transaction.

<Note>
  **Provisioning:** For some fiat rails the inbound account is provisioned asynchronously. The rule is returned with
  `status: provisioning` and the `from` routing fields are populated once provisioning completes (the rule then moves to
  `active`). Fetch the rule with `GET /v1/accounts/{account_id}/conversions/{conversion_id}` to read the final routing.
</Note>

## Creating a rule — crypto off-ramp (USDC on Ethereum → EUR via SEPA)

An end customer sends USDC on Ethereum and automatically receives EUR via SEPA to the bank account you specify.

```bash theme={null}
curl -X POST "https://api.services.payward.com/v1/accounts/AA23N84GGQN4WE6I/conversions" \
  -H "API-Key: $PWS_API_KEY" \
  -H "API-Sign: $PWS_API_SIGN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "USDC off-ramp to SEPA",
    "from": {
      "symbol": "USDC",
      "type": "wallet",
      "wallet": { "via": "ethereum" }
    },
    "to": {
      "symbol": "EUR",
      "type": "bank",
      "bank_account": {
        "via": "sepa",
        "iban": "DE89370400440532013000",
        "bic": "DEUTDEFFXXX",
        "bank": "Deutsche Bank"
      }
    }
  }'
```

On the response, `from.wallet.address` carries the provisioned Ethereum deposit address. Share it with the end customer — any USDC deposit to that address triggers the rule and initiates the SEPA payout.

<Note>
  **Fiat destinations:** The beneficiary on a fiat destination is the account's KYC identity — third-party bank accounts
  are not permitted. The beneficiary is derived from KYC, so there is no beneficiary field on the request.
</Note>

## Supported rails and networks

Bank destinations specify a `bank_account.via` and the routing fields that rail requires; crypto destinations specify a
`wallet.via` (network) and `address`:

| `via`                                                           | `type`   | Required destination fields                       |
| --------------------------------------------------------------- | -------- | ------------------------------------------------- |
| `sepa`                                                          | `bank`   | `iban` (+ optional `bic`, `bank`)                 |
| `fps`                                                           | `bank`   | `sort_code`, `account_number` (+ optional `bank`) |
| Any crypto network (`bitcoin`, `ethereum`, `polygon`, `xrp`, …) | `wallet` | `address` (+ optional `tag` / `memo`)             |

The optional `bank` field on any bank rail is the name of the beneficiary's financial institution; it is passed through to the underlying provider.

The Conversions API currently supports `sepa` and `fps` bank destinations. An unsupported `via` returns `400`.

<Note>
  **Only SEPA and FPS bank destinations are currently supported.** Unsupported bank rails, including ACH and Fedwire,
  are rejected with `400`.
</Note>

### Crypto destination tags and memos

Networks that need a destination tag or memo to route a deposit accept them as optional fields on the wallet
destination — the XRP destination `tag`, or the `memo` used by chains such as XLM, EOS, and Cosmos:

```json theme={null}
"to": {
  "symbol": "XRP",
  "type": "wallet",
  "wallet": { "via": "xrp", "address": "rXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", "tag": "1234567" }
}
```

Omit `tag` / `memo` for networks that don't use them.

## Multi-network support

A rule matches a single asset on a single network. Accepting the same asset on multiple networks requires one rule per network. Each rule gets its own provisioned inbound address and can be paused or deleted independently — keeping reconciliation and per-network reporting straightforward.

To accept USDC on Ethereum and Polygon, create two rules identical except for `from.wallet.via`:

```bash theme={null}
# Rule 1: USDC on Ethereum
"from": { "symbol": "USDC", "type": "wallet", "wallet": { "via": "ethereum" } }

# Rule 2: USDC on Polygon
"from": { "symbol": "USDC", "type": "wallet", "wallet": { "via": "polygon" } }
```

## Rule lifecycle

| State          | Meaning                                                                               |
| -------------- | ------------------------------------------------------------------------------------- |
| `provisioning` | Inbound endpoint is being provisioned. Transitions to `active` once routing is ready. |
| `active`       | Rule is live. Matching deposits are processed normally.                               |
| `paused`       | Deposits are not processed. Can be reactivated. Provisioned credentials remain valid. |

Deleting a rule is a **soft delete**: after `DELETE /v1/accounts/{account_id}/conversions/{conversion_id}`, the rule no longer fires and a subsequent `GET` returns `404 Not Found`. Deleted rules are never returned by `listConversions`.

## Managing a rule

Update a rule's destination, label, or status with `PUT /v1/accounts/{account_id}/conversions/{conversion_id}`. This is a **full-state replacement** of `to`, `name`, and `status` — the source side (`from`) is immutable, so re-create the rule to change input symbol, rail, or source type. `status` must be `active` or `paused`.

```bash theme={null}
# Pause a rule
curl -X PUT "https://api.services.payward.com/v1/accounts/WVSD33HRMGSZUBM7/conversions/550e8400-e29b-41d4-a716-446655440000" \
  -H "API-Key: $PWS_API_KEY" \
  -H "API-Sign: $PWS_API_SIGN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "EUR autoramp to Polygon vault",
    "status": "paused",
    "to": {
      "symbol": "USDC",
      "type": "wallet",
      "wallet": {
        "via": "polygon",
        "address": "0x1234567890abcdef1234567890abcdef12345678"
      }
    }
  }'

# Delete (soft delete) a rule
curl -X DELETE "https://api.services.payward.com/v1/accounts/AA23N84GGQN4WE6I/conversions/550e8400-e29b-41d4-a716-446655440000" \
  -H "API-Key: $PWS_API_KEY" \
  -H "API-Sign: $PWS_API_SIGN"
```

## Monitoring conversions

Each firing of a rule is a **conversion transaction**. List the transactions for a rule with `GET /v1/accounts/{account_id}/conversions/{conversion_id}/transactions`:

```bash theme={null}
curl -X GET "https://api.services.payward.com/v1/accounts/AA23N84GGQN4WE6I/conversions/550e8400-e29b-41d4-a716-446655440000/transactions" \
  -H "API-Key: $PWS_API_KEY" \
  -H "API-Sign: $PWS_API_SIGN"
```

```json theme={null}
{
  "data": [
    {
      "id": "conv_smoke_001",
      "status": "completed",
      "conversion_rule_id": "550e8400-e29b-41d4-a716-446655440000",
      "from": { "symbol": "EUR", "amount": "100.00", "type": "bank", "via": "sepa", "status": "settled" },
      "to": { "symbol": "USDC", "amount": "108.42", "via": "polygon", "status": "settled" },
      "chain_references": { "deposit_txid": "deposit-eur-001", "withdraw_txid": "withdraw-usdc-001" },
      "created_at": "2026-04-01T12:00:00Z"
    },
    {
      "id": "conv_smoke_002",
      "status": "pending_deposit",
      "conversion_rule_id": "550e8400-e29b-41d4-a716-446655440000",
      "chain_references": {},
      "created_at": "2026-04-02T09:30:00Z"
    }
  ]
}
```

A transaction moves through the following states:

| State             | Meaning                                                               |
| ----------------- | --------------------------------------------------------------------- |
| `pending_deposit` | Awaiting the inbound deposit.                                         |
| `held`            | Deposit received but held pending review; conversion has not started. |
| `converting`      | Funds received; conversion in progress.                               |
| `settling`        | Conversion complete; outbound settlement in progress.                 |
| `completed`       | Settled to the destination. `chain_references` carry the txids.       |
| `failed`          | The transaction failed.                                               |

<Note>
  **Webhooks.** Subscribe to conversion webhook events for real-time updates instead of polling — register your endpoint
  with the [Register Webhook](/api-reference/webhooks/register-webhook) API.
</Note>

You can also list an account's rules and filter by lifecycle state:

```bash theme={null}
curl -X GET "https://api.services.payward.com/v1/accounts/AA23N84GGQN4WE6I/conversions?status=active&page_size=20" \
  -H "API-Key: $PWS_API_KEY" \
  -H "API-Sign: $PWS_API_SIGN"
```

`listConversions` supports only `status`, `page_size`, and `page_token` (cursor) parameters. Use the per-rule transactions endpoint above for transaction history.

## Whitelisting external wallets

Register a reusable external wallet address on an account with `POST /v1/accounts/{account_id}/wallets`. Set `verification_method` to `self-hosted` when the end user controls the wallet, or `hosted` when the wallet is held by another custodial service. This makes the address eligible for Travel Rule checks used by conversions.

```bash theme={null}
curl -X POST "https://api.services.payward.com/v1/accounts/AA23N84GGQN4WE6I/wallets" \
  -H "API-Key: $PWS_API_KEY" \
  -H "API-Sign: $PWS_API_SIGN" \
  -H "Content-Type: application/json" \
  -d '{
    "asset": "BTC",
    "via": "bitcoin",
    "address": "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh",
    "verification_method": "self-hosted"
  }'
```

```json theme={null}
{ "data": { "whitelisted": true } }
```

If the address requires a verification flow that this endpoint does not support, such as Satoshi test or digital signature verification, the API returns `409 Conflict` with `wallet_additional_verification_required`.

## Error handling

### Missing or invalid off-ramp bank details

When you create or update an off-ramp rule (crypto → fiat), the `to.bank_account` fields are validated against the chosen rail before the rule goes live. If a field the rail requires is missing or malformed, the request is rejected with `400 Bad Request` and `code: conversion_invalid_withdrawal_address`. Each `causes[]` entry names the offending field with a `to.bank_account.*` path so you know exactly which value to supply.

```json theme={null}
{
  "error": {
    "type": "conversions_error",
    "status": 400,
    "code": "conversion_invalid_withdrawal_address",
    "instance": "req_01H00000000000000000000000",
    "causes": [
      {
        "field": "to.bank_account.bic",
        "message": "bic is required for this withdrawal method"
      }
    ]
  }
}
```

The exact set of required fields depends on the rail and the provider routing the payout. Fields listed as optional in [Supported rails and networks](#supported-rails-and-networks) may still be required for a specific route — for example, some SEPA payout providers require `bic`. Populate every field the rejection names and resubmit.

## API reference

| Endpoint                                                                 | Description                                                   |
| ------------------------------------------------------------------------ | ------------------------------------------------------------- |
| `POST /v1/accounts/{account_id}/conversions`                             | Create a conversion rule with a provisioned inbound endpoint  |
| `GET /v1/accounts/{account_id}/conversions`                              | List an account's conversion rules (filter by `status`)       |
| `GET /v1/accounts/{account_id}/conversions/{conversion_id}`              | Get a single conversion rule by ID                            |
| `PUT /v1/accounts/{account_id}/conversions/{conversion_id}`              | Update a rule's `to`, `name`, and `status` (full replacement) |
| `DELETE /v1/accounts/{account_id}/conversions/{conversion_id}`           | Soft-delete a conversion rule                                 |
| `GET /v1/accounts/{account_id}/conversions/{conversion_id}/transactions` | List the transactions produced by a rule                      |
| `POST /v1/accounts/{account_id}/wallets`                                 | Whitelist an external wallet address                          |

<Note>
  **Not yet available.** The following are planned but not exposed by the API today: a fee breakdown / executed rate /
  swap-quote on conversions, a structured `failure_reason`, idempotency keys, and an `expires_at` on inbound deposit
  addresses.
</Note>
