Skip to main content
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-user 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 the user has defined. Two patterns are supported:
  • Standing rules — a persistent fromto configuration that fires automatically on every matching deposit. The right model for recurring on-ramp and off-ramp flows.
  • One-shot conversions — a discrete per-transaction operation, initiated explicitly per conversion. The right model for on-demand or price-locked execution.
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. If you want to execute a pure trade against a locked quote without managing rails, see Swap. For fiat-to-crypto purchases via a hosted checkout UI, see Ramp.

How it works

The Conversions API is built around two complementary services:
  • PWS Conversion API — handles the rails. Pulls funds from a source (external wallet, bank account, or prefunded balance) and pushes the converted asset to a destination.
  • PWS Swaps API — handles pricing and trade execution. Called explicitly when you want to lock a rate before funds move, or implicitly at settlement time when you don’t.

Part 1 — Standing conversion rules

A standing rule is a persistent fromto configuration. PWS provisions a dedicated inbound endpoint — an IBAN, sort code, or deposit address — and any matching credit automatically triggers a conversion and settlement. Rules remain active and reusable as long as the user’s KYC profile is valid.

Rule matching

Rules use strict 1:1:1 matching. A credit fires a rule only if it lands on that rule’s inbound.id, its asset matches from.asset.symbol, and (when set) its amount matches from.asset.amount exactly. Credits that don’t match any rule land in the account balance rather than misrouting.

Creating a rule — fiat on-ramp (EUR → USDG)

An end customer sends EUR via SEPA and automatically receives USDG on Solana.
curl -X POST "https://api.services.payward.com/v1/accounts/NL00KRAK0123456789/conversion-rules" \
  -H "API-Key: $PWS_API_KEY" \
  -H "API-Sign: $PWS_API_SIGN" \
  -H "Idempotency-Key: 7a1c9b45-f832-4d09-a312-55e6c1234567" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "End-customer 12345: EUR to USDG",
    "status": "active",
    "from": {
      "asset": {
        "symbol": "EUR",
        "type": "fiat"
      },
      "via": "sepa"
    },
    "to": {
      "asset": {
        "symbol": "USDG",
        "type": "stablecoin"
      },
      "destination": {
        "type": "external_wallet",
        "wallet_id": "wl_client_usdg",
        "user_id": "cust_12345"
      }
    }
  }'
The response returns an inbound block with the provisioned IBAN and BIC:
{
  "data": {
    "id": "rule_eur_usdg_01",
    "status": "active",
    "current_uses": 0,
    "inbound": {
      "id": "vacct_01H7X",
      "via": "sepa",
      "lifecycle": "reusable",
      "routing": {
        "iban": "DE89370400440532013000",
        "bic": "KRAKDEFFXXX"
      }
    }
  }
}
Share inbound.routing.iban and inbound.routing.bic with the end customer as their payment instructions. Every SEPA credit to that IBAN fires the rule and produces a new Conversion. current_uses increments on each successful firing.

Inbound routing fields by rail

viaFields to share with the end customer
sepa, swiftinbound.routing.iban + inbound.routing.bic
fpsinbound.routing.sort_code + inbound.routing.account_number
fedwire, achinbound.routing.routing_number + inbound.routing.account_number
Any crypto networkinbound.address

Creating a rule — crypto off-ramp (USDC/Ethereum → EUR)

An end customer sends USDC on Ethereum and automatically receives EUR via SEPA to their bank account.
curl -X POST "https://api.services.payward.com/v1/accounts/NL00KRAK0123456789/conversion-rules" \
  -H "API-Key: $PWS_API_KEY" \
  -H "API-Sign: $PWS_API_SIGN" \
  -H "Idempotency-Key: b83c1f47-e291-4a72-bc34-112233445566" \
  -H "Content-Type: application/json" \
  -d '{
    "status": "active",
    "from": {
      "asset": {
        "symbol": "USDC",
        "type": "stablecoin"
      },
      "via": "ethereum"
    },
    "to": {
      "asset": {
        "symbol": "EUR",
        "type": "fiat"
      },
      "destination": {
        "type": "bank_account",
        "bank_account": {
          "via": "sepa",
          "beneficiary_name": "Alice Jones",
          "iban": "DE89370400440532013000",
          "bic": "DEUTDEFFXXX"
        }
      }
    }
  }'
The response returns inbound.address on Ethereum. Share it with the end customer — any USDC deposit to that address triggers the rule and initiates SEPA payout.
Fiat destinations: The beneficiary name on the bank account must match the user’s KYC identity. Third-party bank accounts are not permitted.
Flexible asset intake for off-ramp rules: Because EVM-compatible networks share address formats, a deposit address generated for one asset may receive other tokens on the same or compatible networks. The system evaluates the actual deposited asset at detection time and proceeds with conversion if the asset is supported. Deposits that cannot be converted to the rule’s output fiat currency are rejected.

Multi-network support

Rules enforce strict 1:1:1 matching, so accepting the same asset on multiple networks requires one rule per network. Each rule gets its own inbound address, its own current_uses counter, and can be paused or deleted independently — making reconciliation and per-network reporting straightforward. To accept USDC on Ethereum, Solana, and Polygon, create three rules identical except for from.via:
# Rule 1: USDC on Ethereum
"from": { "asset": { "symbol": "USDC", "type": "stablecoin" }, "via": "ethereum" }

# Rule 2: USDC on Solana
"from": { "asset": { "symbol": "USDC", "type": "stablecoin" }, "via": "solana" }

# Rule 3: USDC on Polygon
"from": { "asset": { "symbol": "USDC", "type": "stablecoin" }, "via": "polygon" }

Rule lifecycle

StateMeaning
activeRule is live. Deposits are processed normally.
pausedDeposits are not processed. Can be reactivated by the partner or customer.
disabledDisabled by the system due to compliance or risk controls. Reactivatable once the underlying issue is resolved.
deletedPermanently deactivated. Cannot be reactivated.
Rules can be paused or reactivated at any time by the partner via API or by the end customer via your UI. When paused, deposits are not processed but deposit credentials remain valid and associated with the rule.

Monitoring rule-triggered conversions

When a rule fires, PWS emits a conversion.created webhook. When the withdrawal confirms, conversion.completed is emitted. To poll instead, filter by conversion_rule_id and set page_size: 1 to fetch the latest conversion for a given rule:
curl -X GET "https://api.services.payward.com/v1/accounts/NL00KRAK0123456789/conversions?conversion_rule_id=rule_eur_usdg_01&page_size=1" \
  -H "API-Key: $PWS_API_KEY" \
  -H "API-Sign: $PWS_API_SIGN"

Part 2 — One-shot conversions

One-shot conversions are discrete per-transaction operations. There is no standing rule — each conversion is initiated explicitly via POST /v1/accounts/{account_id}/conversions. There are three variants depending on the funding source and whether you want to lock the exchange rate.

Variant A — Balance-sourced

Use this when the account has a prefunded balance and you want to convert on demand without any inbound push. The conversion executes synchronously on POST and returns status: completed immediately.
curl -X POST "https://api.services.payward.com/v1/accounts/NL00KRAK0123456789/conversions" \
  -H "API-Key: $PWS_API_KEY" \
  -H "API-Sign: $PWS_API_SIGN" \
  -H "Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000" \
  -H "Content-Type: application/json" \
  -d '{
    "from": {
      "asset": {
        "symbol": "USD",
        "type": "fiat",
        "amount": "10000.00"
      },
      "source": { "type": "balance" }
    },
    "to": {
      "asset": {
        "symbol": "USDG",
        "type": "stablecoin"
      },
      "destination": {
        "type": "external_wallet",
        "wallet_id": "wl_client_usdg",
        "user_id": "cust_12345"
      }
    },
    "transaction_id": "your_tx_99001"
  }'
No inbound block is returned. The response includes to.amount, fees, and chain_references.withdraw_txid once the withdrawal is confirmed.

Variant B — Push-sourced, indicative price

Use this when you want PWS to provision a one-time deposit address and execute at market rate when the deposit arrives. Simpler than the price-locked flow and appropriate when rate certainty is not required.
curl -X POST "https://api.services.payward.com/v1/accounts/NL00KRAK0123456789/conversions" \
  -H "API-Key: $PWS_API_KEY" \
  -H "API-Sign: $PWS_API_SIGN" \
  -H "Idempotency-Key: 2a4e6c88-1234-4b56-a789-abcdef012345" \
  -H "Content-Type: application/json" \
  -d '{
    "from": {
      "asset": {
        "symbol": "USDT",
        "type": "stablecoin"
      },
      "source": { "type": "external_wallet", "via": "tron" }
    },
    "to": {
      "asset": {
        "symbol": "SOL",
        "type": "crypto"
      },
      "destination": {
        "type": "external_wallet",
        "wallet_id": "wl_client_sol"
      }
    }
  }'
The response returns status: pending_deposit and a single-use inbound.address with an expires_at. Push funds to that address before expiry. Once the deposit lands, the conversion progresses through convertingsettlingcompleted.

Variant C — Push-sourced, price-locked

Use this when you want to show the end customer a guaranteed rate before they send funds. Requires two API calls: first lock a quote from the Swaps API, then create the conversion referencing it. Step 1 — Lock a quote:
curl -X POST "https://api.services.payward.com/v1/accounts/NL00KRAK0123456789/quotes" \
  -H "API-Key: $PWS_API_KEY" \
  -H "API-Sign: $PWS_API_SIGN" \
  -H "Idempotency-Key: aabbccdd-1234-4abc-8def-112233445566" \
  -H "Content-Type: application/json" \
  -d '{
    "from": { "symbol": "USDT", "type": "stablecoin", "amount": "100" },
    "to": { "symbol": "SOL", "type": "crypto" }
  }'
The response includes a quote_id, the locked rate, fees, and expires_at (typically ~2 minutes). Step 2 — Create the conversion referencing the quote:
curl -X POST "https://api.services.payward.com/v1/accounts/NL00KRAK0123456789/conversions" \
  -H "API-Key: $PWS_API_KEY" \
  -H "API-Sign: $PWS_API_SIGN" \
  -H "Idempotency-Key: 7f3d8b92-abcd-4e56-b789-998877665544" \
  -H "Content-Type: application/json" \
  -d '{
    "swap_quote_id": "Q-USDT2SOL-7",
    "from": {
      "asset": {
        "symbol": "USDT",
        "type": "stablecoin"
      },
      "source": { "type": "external_wallet", "via": "tron" }
    },
    "to": {
      "asset": {
        "symbol": "SOL",
        "type": "crypto"
      },
      "destination": {
        "type": "external_wallet",
        "wallet_id": "wl_client_sol"
      }
    }
  }'
Push USDT to the returned inbound.address before the quote expires. PWS executes against the locked quote and withdraws SOL.
Quote expiry: The locked rate is valid for approximately 2 minutes. The balance-sourced variant (A) is better suited for price-locked flows when a prefunded balance is available, since it avoids the timing pressure of an external push.

Webhook events

EventPayload
conversion.createdFull Conversion resource
conversion.deposit.detectedConversion with chain_references.deposit_txid
conversion.completedConversion with to.amount, fees, and rate
conversion.failedConversion with failure_reason
conversion.expiredPush-sourced conversion that timed out
wallet.whitelistedWallet with status transition (pendingapproved or rejected)
Webhook signature, retry, and replay semantics are covered in the Webhooks reference.

API reference

EndpointDescription
POST /v1/accounts/{account_id}/wallets/whitelistVerify and whitelist a destination wallet (required before use)
POST /v1/accounts/{account_id}/depositsFund the account balance for balance-sourced conversions
POST /v1/accounts/{account_id}/conversion-rulesCreate a standing conversion rule with a dedicated inbound endpoint
POST /v1/accounts/{account_id}/conversionsCreate a one-shot conversion
GET /v1/accounts/{account_id}/conversionsList conversions, filterable by rule, status, or end-customer
GET /v1/conversions/{conversion_id}Get a single conversion by ID
POST /v1/accounts/{account_id}/quotesLock a rate before creating a price-locked conversion