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

# Conversion rules

> The conversion rules API automates fiat ↔ crypto on/off-ramps using regulated banking rails: persistent rules that fire on every matching deposit.

The conversion rules 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?** Conversion rules 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_rule_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 conversion rules 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_rule_id}`, the rule no longer fires and a subsequent `GET` returns `404 Not Found`. Deleted rules are never returned by `listConversionRules`.

## Managing a rule

Update a rule's destination, label, or status with `PUT /v1/accounts/{account_id}/conversions/{conversion_rule_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_rule_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. See [Conversion
  webhooks](#conversion-webhooks).
</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"
```

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

## Conversion webhooks

Conversion lifecycle webhook payloads include optional enrichment fields. They are omitted when the source event does not carry the corresponding fact.

| Event                               | Optional enrichment fields                                                                                                 |
| ----------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| `conversion.deposit_completed`      | `deposit_amount`, `deposit_currency`, `network`, `deposit_reference`                                                       |
| `conversion.deposit_failed`         | `error_code`, `deposit_amount`, `deposit_currency`                                                                         |
| `conversion.deposit_held`           | `deposit_amount`, `deposit_currency`                                                                                       |
| `conversion.quote_execution_failed` | `error_code`, `source_amount`, `source_currency`, `destination_currency`                                                   |
| `conversion.travel_rule_blocked`    | `error_code`, `source_amount`, `source_currency`, `destination_currency`                                                   |
| `conversion.withdrawal_completed`   | `withdrawal_amount`, `withdrawal_currency`, `destination_address`, `network`, `chain_references.blockchain_transaction_id` |
| `conversion.withdrawal_failed`      | `error_code`, `withdrawal_amount`, `withdrawal_currency`, `destination_address`                                            |

`error_code`, when present, is one of: `amount_too_small`, `quote_expired`, `destination_rejected`, `wallet_not_whitelisted`, `wallet_verification_method_not_supported`, `travel_rule_data_missing`, or `unknown`.

`destination_address` is the crypto destination only. It is never a fiat destination, tag, or memo.

`chain_references.blockchain_transaction_id` is the on-chain transaction id or hash for a completed crypto withdrawal. It is distinct from `withdraw_txid`.

### Resolving a Travel Rule block

`conversion.travel_rule_blocked` means a pre-trade Travel Rule check stopped the conversion before any quote was executed, because the destination wallet needs address-ownership verification (or the check could not be evaluated). The `error_code` tells you how to resolve it:

* `wallet_not_whitelisted` — the destination address is not yet ownership-verified. Whitelist it with `POST /v1/users/{user_id}/travel-rule/verifications` (hosted declaration or self-attestation); once the address is verified, subsequent conversions to it will proceed.
* `wallet_verification_method_not_supported` — the address requires Satoshi-test or digital-signature verification, which is not available via the API yet. Contact Support to resolve.
* `travel_rule_data_missing` — eligibility could not be evaluated because required account information (for example, the user's country) is missing. Complete the user's profile and retry; contact Support if the block persists.

## Whitelisting external wallets

Before a conversion can pay out to an external crypto address, that address must pass a Travel Rule address-ownership check. Whitelist it with `POST /v1/users/{user_id}/travel-rule/verifications`: use `method: hosted_declaration` when the wallet is held by another custodial service, or `method: self_attestation` (supplying the end user's ownership evidence) when the end user controls the wallet.

```bash theme={null}
curl -X POST "https://api.services.payward.com/v1/users/AA55N84GQOIRA67A/travel-rule/verifications" \
  -H "API-Key: $PWS_API_KEY" \
  -H "API-Sign: $PWS_API_SIGN" \
  -H "Content-Type: application/json" \
  -d '{
    "address": "0x8ba1f109551bD432803012645Ac136ddd64DBA72",
    "method": "hosted_declaration"
  }'
```

```json theme={null}
{
  "data": {
    "verification_id": "ver_0195e4d2-a4d0-7b12-8f4a-123456789abc",
    "wallet_type": "hosted",
    "method": "hosted_declaration",
    "status": "verified"
  }
}
```

Satoshi-test and digital-signature verification are not available via the API yet; contact Support for addresses that require them.

## 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_rule_id}`              | Get a single conversion rule by ID                            |
| `PUT /v1/accounts/{account_id}/conversions/{conversion_rule_id}`              | Update a rule's `to`, `name`, and `status` (full replacement) |
| `DELETE /v1/accounts/{account_id}/conversions/{conversion_rule_id}`           | Soft-delete a conversion rule                                 |
| `GET /v1/accounts/{account_id}/conversions/{conversion_rule_id}/transactions` | List the transactions produced by a rule                      |
| `POST /v1/users/{user_id}/travel-rule/verifications`                          | Whitelist an external wallet address (Travel Rule)            |

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