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

# Conversions API overview

> How on-ramp and off-ramp conversions behave over time: lifecycle states, quote binding, webhook events, terminal errors, and what COMPLETED means.

This page defines how a conversion behaves over time: the states it moves through, what each one means, how quotes bind to a conversion, and what to do when something fails.

For what a conversion is, see [Stablecoin on/off-ramp conversions](/guides/concepts/stablecoin-conversions). For step-by-step implementation, see the quickstarts for [on-ramps](/guides/quickstarts/on-ramp) and [off-ramps](/guides/quickstarts/off-ramp). For request and response shapes, see the endpoint pages.

## Conversion types

| Type       | Direction    | Created by                                            |
| ---------- | ------------ | ----------------------------------------------------- |
| `ON_RAMP`  | USD to USDC  | Automatically, when a wire lands at a Virtual Account |
| `OFF_RAMP` | USDC to fiat | `POST /v2/conversions`                                |

Both types share the same lifecycle and the same webhook event type.

## Lifecycle

```text theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
INITIATED ──→ PROCESSING ──→ PAYOUT_INITIATED ──→ PAYOUT_IN_PROGRESS ──→ COMPLETED
    │              │                 │                      │
    ▼              ▼                 ▼                      ▼
  FAILED         FAILED           FAILED                RETURNED
```

`COMPLETED`, `FAILED`, and `RETURNED` are terminal. Every other state is transient.

`FAILED` is reachable from `INITIATED`, `PROCESSING`, and `PAYOUT_INITIATED`. Once the payout is in flight the terminal error is `RETURNED` instead, because the funds left and are coming back. A successful submit call does not mean a conversion will reach payout, so handle a terminal error out of every transient state.

| State                | Terminal | Meaning                                                                                                                                                                                                                                         |
| -------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `INITIATED`          | No       | The conversion exists and has been accepted. For an off-ramp, the submit call succeeded. For an on-ramp, the incoming wire has been recognized                                                                                                  |
| `PROCESSING`         | No       | Bastion is converting between the stablecoin and fiat legs. The source funds are committed at this point                                                                                                                                        |
| `PAYOUT_INITIATED`   | No       | The outbound leg has been handed to the payment provider or the destination chain                                                                                                                                                               |
| `PAYOUT_IN_PROGRESS` | No       | The provider or network is delivering the funds                                                                                                                                                                                                 |
| `COMPLETED`          | Yes      | Funds have been delivered. `completed_at` is populated                                                                                                                                                                                          |
| `FAILED`             | Yes      | The conversion could not be completed and the payout never went out. `failure_reason` describes the cause                                                                                                                                       |
| `RETURNED`           | Yes      | The payout went out and was sent back, most commonly by the receiving bank rejecting it. `failure_reason` describes the cause; `refund_details` carries the credited USDC amount, cross-app credit identifiers, and structured failure metadata |

### What `COMPLETED` means

`COMPLETED` means Bastion and its payment partner have delivered the funds. It does not mean the receiving bank has credited the recipient's account. Receiving banks apply their own processing, including compliance screening, so funds can appear in the recipient's account later than `completed_at`.

### Recovering from a terminal error

`FAILED` means the payout never went out, so no funds are in flight. Retry if `failure_reason` points to a transient cause, otherwise route to support.

`RETURNED` means the payout went out and came back. Retrying against the same payment instructions will usually fail the same way, so have the customer verify the destination bank details and register corrected instructions first.

Treat `failure_reason` as diagnostic. It reports what the provider or bank said, which belongs in your logs and support tooling rather than in front of a customer.

### Values populated during the lifecycle

Some fields have no value until the conversion advances far enough to produce one.

| Field                | Behavior                                                                                                            |
| -------------------- | ------------------------------------------------------------------------------------------------------------------- |
| `destination.amount` | Empty until settlement completes. US wire off-ramps are the exception and set it when the conversion is created     |
| `exchange_rate`      | Empty until settlement completes. On an international off-ramp the settled value can differ from the rate quoted    |
| `completed_at`       | Empty until `COMPLETED`                                                                                             |
| `failure_reason`     | Set on `FAILED` and `RETURNED` only. GET and webhooks populate this field from the same mapping logic               |
| `refund_details`     | Present only when `status` is `RETURNED`. Individual keys inside may be omitted until the refund workflow completes |

Show quoted figures as estimates, and use the values from the `COMPLETED` event on receipts and in your ledger.

## Quote lifecycle

International off-ramps must reference a quote. US off-ramps do not use quotes, and a `quote_id` on a US conversion is rejected.

A quote is created by `GET /v2/conversions/quotes`, is persisted, and carries an `expires_at`. It binds fee terms and pricing to the conversion that later references it, which is why fee fields are not accepted on submit.

Three rules govern the binding:

* **Single use.** A quote supports one successful conversion. Retries that reuse the same `request_id` are idempotent and do not consume a second quote.
* **Expiry.** Submitting after `expires_at` returns `QUOTE_EXPIRED`.
* **Consistency.** The conversion must match the quote. The `amount` must equal the quoted `source.amount`, and the destination corridor must be the one that was priced.

Pricing is indicative rather than locked. Re-fetch a quote whenever the customer's input changes, and again before showing a confirmation screen, so the figures they approve are as current as possible.

## Interpreting webhook events

Conversions emit a single event type, `conversion_notification_update`, on every state change. The conversion object arrives in `data`, matching the shape returned by `GET /v2/conversions/{conversion_id}`.

Two identifiers, used for different things:

* The top-level `id` identifies the event and is regenerated if the event is reprocessed, so don't use it as your deduplication key.
* `data.id` is the conversion ID and is stable across every event for that conversion. Deduplicate business logic on `data.id` together with `data.status`.

A delivery can repeat, so the same status may arrive more than once. Make your handler idempotent.

Because `INITIATED` is the first event on an on-ramp, that event is also the first time you learn the conversion's ID. Route it using `account_id` and the Virtual Account it landed against.

For delivery behavior, retries, and sender verification, see [Webhooks](/v2/api-reference/webhooks/overview). For payload field definitions, see [Conversion notifications](/v2/api-reference/webhooks/notifications/conversions-notifications).

## Polling

Webhooks are the recommended way to track a conversion. Poll `GET /v2/conversions/{conversion_id}` as a fallback when an expected webhook has not arrived within a reasonable window.

Polling returns current state, not history, and there is no endpoint that lists conversions. Persist each event as it arrives, keyed on `data.id`, and store `identity_id`, `account_id`, and `payment_instructions_id` alongside your own records. That gives you transition history, event routing, and period reconciliation without querying Bastion.


## Related topics

- [Webhooks API overview](/v2/api-reference/webhooks/overview.md)
- [API authentication overview](/guides/security/api-authentication.md)
- [Compliance API overview](/v2/api-reference/compliance/overview.md)
- [Identities API overview](/v2/api-reference/identities/overview.md)
- [Issuance API overview](/v2/api-reference/issuance/overview.md)
