Skip to main content
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. For step-by-step implementation, see the quickstarts for on-ramps and off-ramps. For request and response shapes, see the endpoint pages.

Conversion types

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

Lifecycle

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.

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. 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. For payload field definitions, see Conversion 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.