# Migrate from the Quote API to Transfers v.3

> The Transfers v.3 API replaces the legacy Quote API for wallet-to-wallet currency conversion. Instead of creating a standalone quote and accepting it through a redirect flow, currency conversion is now a property of a transfer. Migrating to v.3 provides a single-field status model, digest-based acceptance that ties a client to the exact rate shown, mandatory idempotency, and the option to replace polling with a webhook. This guide walks you through the required changes for a successful migration.

The Transfers v.3 API replaces the legacy Quote API for wallet-to-wallet currency conversion. Instead of creating a standalone quote and accepting it through a redirect flow, currency conversion is now a property of a transfer. Migrating to v.3 provides a single-field status model, digest-based acceptance that ties a client to the exact rate shown, mandatory idempotency, and the option to replace polling with a webhook. This guide walks you through the required changes for a successful migration.

The core conceptual shift is that a quote is no longer a separate entity you convert. You create a **transfer**, and if the two wallets hold different currencies, you attach a **conversion** to it.

## Endpoint mapping

| Operation | Legacy Quote API | Transfers v.3 |
| :--- | :--- | :--- |
| Create | [`POST /api/v1/quote`](/bvnk/api-explorer/endpoints/quote-create) | [`POST /payment/v3/transfers`](/bvnk/api-explorer/endpoints/transfer-create-v-3) |
| Preview (estimate) | [`POST /api/v1/quote?estimate=true`](/bvnk/api-explorer/endpoints/quote-create) | [`POST /payment/v3/transfers/dry-run`](/bvnk/api-explorer/endpoints/transfer-dry-run-v-3) |
| Accept | [`PUT /api/v1/quote/accept/{uuid}`](/bvnk/api-explorer/endpoints/quote-accept) | [`POST /payment/v3/transfers/{transferId}/actions`](/bvnk/api-explorer/endpoints/transfer-action-v-3) |
| Get | [`GET /api/v1/quote/{uuid}`](/bvnk/api-explorer/endpoints/quote-read) | [`GET /payment/v3/transfers/{transferId}`](/bvnk/api-explorer/endpoints/transfer-read-v-3) |

:::tip Historical quotes remain accessible
The Quote API endpoints stay available for retrieving quotes created before you migrate. Query them with [`GET /api/v1/quote/{uuid}`](/bvnk/api-explorer/endpoints/quote-read) as before. New conversions should be created through Transfers v.3.
:::

## Flow comparison

The legacy flow was always create, accept, then poll two status fields. In v.3, the flow depends on the conversion type you choose, and status is a single field. Select a tab to compare the shapes before mapping fields.

  

Create, accept, then poll `paymentStatus` until the conversion settles.

```mermaid
%%{init: {'theme': 'base', 'themeVariables': {'primaryColor': '#EBF1FE', 'primaryBorderColor': '#3071F2', 'lineColor': '#3071F2', 'primaryTextColor': '#344054', 'actorBkg': '#EBF1FE', 'actorBorder': '#3071F2', 'actorTextColor': '#344054', 'signalColor': '#3071F2', 'signalTextColor': '#344054', 'noteBkgColor': '#EBF1FE', 'noteBorderColor': '#3071F2', 'activationBkgColor': '#D4E3FD', 'activationBorderColor': '#3071F2', 'fontSize': '15px'}, 'sequence': {'actorMargin': 80}}}%%
sequenceDiagram
    participant Client as Your integration
    participant API as BVNK API

    Client->>API: POST /api/v1/quote
    API-->>Client: quoteStatus: PENDING
    Client->>API: PUT /api/v1/quote/accept/{uuid}
    API-->>Client: quoteStatus: PAYMENT_OUT_PROCESSED
    loop Poll until settled
        Client->>API: GET /api/v1/quote/{uuid}
        API-->>Client: paymentStatus: SUCCESS (or FAILED)
    end
```

  
  

The closest analogue to the legacy flow: a firm rate is presented, then accepted with the `digest`.

```mermaid
%%{init: {'theme': 'base', 'themeVariables': {'primaryColor': '#EBF1FE', 'primaryBorderColor': '#3071F2', 'lineColor': '#3071F2', 'primaryTextColor': '#344054', 'actorBkg': '#EBF1FE', 'actorBorder': '#3071F2', 'actorTextColor': '#344054', 'signalColor': '#3071F2', 'signalTextColor': '#344054', 'noteBkgColor': '#EBF1FE', 'noteBorderColor': '#3071F2', 'activationBkgColor': '#D4E3FD', 'activationBorderColor': '#3071F2', 'fontSize': '15px'}, 'sequence': {'actorMargin': 80}}}%%
sequenceDiagram
    participant Client as Your integration
    participant API as BVNK API

    Client->>API: POST /payment/v3/transfers (conversion.type: QUOTE)
    API-->>Client: 201 status: PROCESSING → PENDING_QUOTE_ACCEPTANCE<br/>+ conversion.digest, conversion.expiresAt
    Client->>API: POST /payment/v3/transfers/{id}/actions<br/>(ACCEPT_QUOTE + digest)
    API-->>Client: status: PROCESSING → COMPLETED
    alt Poll for status
        Client->>API: GET /payment/v3/transfers/{id}
        API-->>Client: status: COMPLETED
    else Or receive webhook
        API-)Client: payment:v3:transfer:status-change
    end
```

  
  

Executes immediately at market rate with no acceptance step. Fixed-debit only.

```mermaid
%%{init: {'theme': 'base', 'themeVariables': {'primaryColor': '#EBF1FE', 'primaryBorderColor': '#3071F2', 'lineColor': '#3071F2', 'primaryTextColor': '#344054', 'actorBkg': '#EBF1FE', 'actorBorder': '#3071F2', 'actorTextColor': '#344054', 'signalColor': '#3071F2', 'signalTextColor': '#344054', 'noteBkgColor': '#EBF1FE', 'noteBorderColor': '#3071F2', 'activationBkgColor': '#D4E3FD', 'activationBorderColor': '#3071F2', 'fontSize': '15px'}, 'sequence': {'actorMargin': 80}}}%%
sequenceDiagram
    participant Client as Your integration
    participant API as BVNK API

    Client->>API: POST /payment/v3/transfers (conversion.type: AUTO_CONVERSION)
    API-->>Client: 201 status: PROCESSING → COMPLETED
    Note over Client,API: No acceptance step required
```

  
  

When both wallets hold the same currency, omit the `conversion` object. The transfer settles directly.

```mermaid
%%{init: {'theme': 'base', 'themeVariables': {'primaryColor': '#EBF1FE', 'primaryBorderColor': '#3071F2', 'lineColor': '#3071F2', 'primaryTextColor': '#344054', 'actorBkg': '#EBF1FE', 'actorBorder': '#3071F2', 'actorTextColor': '#344054', 'signalColor': '#3071F2', 'signalTextColor': '#344054', 'noteBkgColor': '#EBF1FE', 'noteBorderColor': '#3071F2', 'activationBkgColor': '#D4E3FD', 'activationBorderColor': '#3071F2', 'fontSize': '15px'}, 'sequence': {'actorMargin': 80}}}%%
sequenceDiagram
    participant Client as Your integration
    participant API as BVNK API

    Client->>API: POST /payment/v3/transfers (no conversion field)
    API-->>Client: 201 status: PROCESSING → COMPLETED
```

  

## What changed

### Create quote

The legacy request described a conversion between two wallets with a flat set of fields. In v.3, the request describes a transfer with an `originator` and a `beneficiary`, and conversion is an optional nested object. Every v.3 create also requires an `Idempotency-Key` header.

  

  ```json title="Request: POST /api/v1/quote"
  {
    "from": "EUR",
    "to": "USDC",
    "fromWalletLsid": "a:25052137356562:qjOSsJf:1",
    "toWalletLsid": "a:24092328494070:G5i4XZ9:2",
    "amountIn": 500,
    "useMinimum": false,
    "useMaximum": false,
    "payInMethod": "wallet",
    "payOutMethod": "wallet"
  }
  ```

  
  

  ```json title="Request: POST /payment/v3/transfers (header Idempotency-Key: 1e74002e-74a1-48fd-b707-147c3187a3e1)"
  {
    "reference": "treasury-123",
    "originator": {
      "amount": 500,
      "currency": "EUR",
      "walletId": "a:25052137356562:qjOSsJf:1"
    },
    "beneficiary": {
      "currency": "USDC",
      "walletId": "a:24092328494070:G5i4XZ9:2"
    },
    "conversion": {
      "type": "QUOTE",
      "validityDuration": "PT20S"
    }
  }
  ```

  

**Field changes:**

| Legacy field | v.3 field | Notes |
| :--- | :--- | :--- |
| `from` | `originator.currency` | Currency to convert from. |
| `to` | `beneficiary.currency` | Currency to convert to. |
| `fromWalletLsid` | `originator.walletId` | Wallet being debited. |
| `toWalletLsid` | `beneficiary.walletId` | Wallet being credited. |
| `amountIn` | `originator.amount` | Fixed-debit. With `QUOTE` you may instead set `beneficiary.amount` for a fixed-credit conversion — specify exactly one side. |
| `useMinimum`, `useMaximum` | — | Removed. |
| `payInMethod`, `payOutMethod` | — | Removed. v3 transfers are always `BOOK` (internal wallet) transfers. |
| — | `reference` | Now required. A unique reference for the transfer. |
| — | `conversion.type` | `QUOTE` or `AUTO_CONVERSION`. Omit the whole `conversion` object for same-currency transfers. |
| — | `conversion.validityDuration` | Optional. `PT20S` or `PT60M`. Applies to `QUOTE` only. |
| — | `metadata` | Optional key-value map for your own tracking data. |
| — | `Idempotency-Key` header | **Required.** A client-generated UUID, reused on retries. |

:::note Estimate mode on v.3
The legacy `?estimate=true` query parameter maps to a dedicated preview endpoint on v.3: [`POST /payment/v3/transfers/dry-run`](/bvnk/api-explorer/endpoints/transfer-dry-run-v-3). It returns the rate, spread, fees, and converted amounts without persisting a transfer or moving funds. See [Preview a transfer](../create-internal-transfer-v3#preview-a-transfer).
:::

### Choose a conversion type

The legacy API had one conversion path. v.3 gives you three, chosen by the `conversion` object:

- **`QUOTE`** presents a firm rate that your customer must accept before the transfer settles. This is the closest analogue to the legacy create-then-accept flow.
- **`AUTO_CONVERSION`** executes immediately at the prevailing market rate with no acceptance step. Only fixed-debit is supported (set `originator.amount`).
- **Same-currency** when both wallets hold the same currency, omit the `conversion` object entirely and the transfer settles directly.

For the full request and response shapes of each type, see [Create an internal transfer (v.3)](../create-internal-transfer-v3).

### Accept the quote

In the legacy API, acceptance was a redirect-oriented step: you sent a `successUrl` and the conversion executed. In v3, you accept by sending an `ACCEPT_QUOTE` action carrying the `digest` from the Create endpoint's response. The digest is a fingerprint of the quoted rate. Passing it back proves the client agreed to the specific rate shown. There is no redirect.

  

  ```json title="Request: PUT /api/v1/quote/accept/{uuid}"
  {
    "successUrl": "https://app.sandbox.bvnk.com/wallets/convert/complete/{{quote_id}}"
  }
  ```

  
  

  ```json title="Request: POST /payment/v3/transfers/{transferId}/actions"
  {
    "type": "ACCEPT_QUOTE",
    "digest": "a1b2c3d4e5f6"
  }
  ```

  

If the quote expires before you accept it, send a `REFRESH_QUOTE` action to obtain a new rate and `digest`, then accept:

```json title="Request: POST /payment/v3/transfers/{transferId}/actions"
{
  "type": "REFRESH_QUOTE"
}
```

### Status model

The legacy API required you to interpret two fields, `quoteStatus` and `paymentStatus`, to determine an outcome. v.3 consolidates these into a single `status` field, which is the source of truth.

| Outcome | Legacy `quoteStatus` / `paymentStatus` | v.3 `status` |
| :--- | :--- | :--- |
| Just created (non-quote) | `PENDING` / `PENDING` | `PROCESSING` |
| Awaiting quote acceptance | `PENDING` / `PENDING` | `PENDING_QUOTE_ACCEPTANCE` |
| Accepted, settling | `ACCEPTED` / `PENDING` | `PROCESSING` |
| Completed | `PAYMENT_OUT_PROCESSED` / `SUCCESS` | `COMPLETED` |
| Conversion failed | `CONVERSION_FAILED` / `FAILED` | `FAILED` |
| Pay-out failed | `PAYMENT_OUT_FAILED` / `FAILED` | `FAILED` |
| Pay-in failed | `PAYMENT_IN_FAILED` / `FAILED` | `FAILED` |
| Quote validity elapsed | — | `EXPIRED` |

:::caution EXPIRED is a distinct terminal state
In the legacy API, an unaccepted quote surfaced as a failure. In v.3, a transfer whose quote is not accepted in time transitions to `EXPIRED`, separate from `FAILED`. Update any logic that treats `FAILED` as the only non-success terminal state.
:::

### Response and conversion details

The response is now nested by participant, timestamps are ISO-8601 instead of Unix epoch, and rate and spread details live under the `conversion` object.

| Legacy field | v.3 field | Notes |
| :--- | :--- | :--- |
| `uuid` | `id` | UUID identifier of the transfer. |
| `amountIn` | `originator.amount` | Now nested. |
| `amountOut` | `beneficiary.amount` | Now nested. |
| `from` | `originator.currency` | Plain currency code in both. |
| `to` | `beneficiary.currency` | Plain currency code in both. |
| `quoteStatus` + `paymentStatus` | `status` | Merged into a single field. |
| `price` | `conversion.allInRate` | Effective rate including spread. |
| `fee`, `fees` | `fees.processingFee` | BVNK processing fee applied to the debiting wallet. |
| `acceptanceExpiryDate` | `conversion.expiresAt` | When the quoted rate expires. Now ISO-8601 instead of epoch milliseconds. See the note below on the two expiry fields. |
| `dateCreated` | `createdAt` | Now ISO-8601. |
| `lastUpdated` | `updatedAt` | Now ISO-8601. |
| `payInMethod`, `payOutMethod` | `method: "BOOK"` | Always `BOOK` for Transfers v.3. |
| — | `conversion.baseRate` | New. Mid-market rate before spread. |
| — | `conversion.spread` | New. Nested `percentage`, `amount`, and `currency`. |
| — | `conversion.digest` | New. Required to accept the quote. |

:::note Two expiry fields in v.3
The legacy `acceptanceExpiryDate` maps to `conversion.expiresAt`, not the top-level `expiresAt`. v.3 exposes both:

- `conversion.expiresAt` — when the **quoted rate** expires. Accept the quote before this time, or refresh it with a `REFRESH_QUOTE` action.
- `expiresAt` (top level) — when the **transfer itself** expires. Unaccepted quote transfers expire 24 hours after creation, regardless of the shorter quote validity window.
:::

A `QUOTE` create response returns the conversion details you need for acceptance:

```json title="Response: POST /payment/v3/transfers"
{
  "id": "d4a904e0-b040-4132-a6d3-ed7d03cdfce7",
  "reference": "treasury-123",
  "type": "payment:transfer",
  "status": "PENDING_QUOTE_ACCEPTANCE",
  "fees": {
    "processingFee": {
      "amount": 0.5,
      "currency": "EUR"
    }
  },
  "originator": {
    "amount": 500,
    "currency": "EUR",
    "walletId": "a:25052137356562:qjOSsJf:1"
  },
  "beneficiary": {
    "amount": 537.10,
    "currency": "USDC",
    "walletId": "a:24092328494070:G5i4XZ9:2"
  },
  "conversion": {
    "type": "QUOTE",
    "spread": {
      "percentage": 0.01,
      "amount": 5.00,
      "currency": "EUR"
    },
    "allInRate": 1.0742,
    "baseRate": 1.0850,
    "validityDuration": "PT20S",
    "expiresAt": "2025-07-30T10:30:20Z",
    "digest": "a1b2c3d4e5f6"
  },
  "expiresAt": "2025-07-31T10:00:00Z",
  "createdAt": "2025-07-30T10:00:00Z",
  "updatedAt": "2025-07-30T10:00:00Z",
  "method": "BOOK"
}
```

The `conversion` object is present in the response only for cross-currency transfers. Its fields, and the legacy quote fields they replace, are:

| v.3 `conversion` field | Replaces (legacy) | Description |
| :--- | :--- | :--- |
| `conversion.type` | — | `QUOTE` or `AUTO_CONVERSION`. |
| `conversion.allInRate` | `price` | Effective rate including spread (originator → beneficiary). |
| `conversion.baseRate` | — | Base market rate before spread. |
| `conversion.spread.percentage` | — | Spread as a decimal fraction (for example, `0.01` = 1%). |
| `conversion.spread.amount` | — | Spread amount in the spread currency. |
| `conversion.spread.currency` | — | Currency of the spread amount. |
| `conversion.validityDuration` | — | Quote validity window. `QUOTE` only. |
| `conversion.expiresAt` | `acceptanceExpiryDate` | When the quoted rate expires. `QUOTE` only. |
| `conversion.digest` | — | Opaque value passed back in the `ACCEPT_QUOTE` action. `QUOTE` only. |

The legacy `fee` field is not part of the conversion object in v.3; the BVNK processing fee is reported separately under `fees.processingFee`.

### Notifications (new in v.3)

The legacy quote flow had no outbound webhooks. You polled [`GET /api/v1/quote/{uuid}`](/bvnk/api-explorer/endpoints/quote-read) for status updates.

Transfers v.3 supports the [Transfer webhook](/bvnk/api-explorer/bvnk-webhooks/payment-transfer-status-change-v-3/) (`payment:v3:transfer:status-change`) as an alternative to polling. Adopting it is recommended but not mandatory; polling [`GET /payment/v3/transfers/{transferId}`](/bvnk/api-explorer/endpoints/transfer-read-v-3) remains available. The webhook fires on each status change, and its payload mirrors the [`GET /payment/v3/transfers/{transferId}`](/bvnk/api-explorer/endpoints/transfer-read-v-3) response, including the `conversion` object for cross-currency transfers.

Each event carries a `direction` so you know which side of the transfer it concerns:

- `OUT` is delivered to the originator of the transfer.
- `IN` is delivered to the beneficiary of the transfer.

When the originator and beneficiary are the same wallet, both an `IN` and an `OUT` event are delivered.

## Flow differences to be aware of

1. **Idempotency is now mandatory.** The legacy quote create had no idempotency mechanism. In v.3, every [`POST /payment/v3/transfers`](/bvnk/api-explorer/endpoints/transfer-create-v-3) call requires a unique `Idempotency-Key` header, a client-generated UUID. Reuse the same key when retrying after a network failure so the transfer is not created twice.

2. **Acceptance uses a digest, not a redirect.** The legacy [`PUT /api/v1/quote/accept/{uuid}`](/bvnk/api-explorer/endpoints/quote-accept) took a `successUrl` and completed the conversion through a redirect. In v.3, you accept by sending an `ACCEPT_QUOTE` action carrying the `digest` from the create (or GET) response. Store the `digest` and pass it back at acceptance time. This ties the acceptance to the exact rate the client was shown, with no redirect involved.

3. **Quote validity is now a fixed set of durations.** The legacy API set the acceptance window server-side with no way to request a duration. In v.3, you can optionally set `conversion.validityDuration` to `PT20S` or `PT60M`. If omitted, the server applies a default.

4. **Fixed-credit amounts are now possible, but only with `QUOTE`.** The legacy quote request accepted only a fixed debit (`amountIn`). In v.3, `AUTO_CONVERSION` remains fixed-debit only (set `originator.amount`). `QUOTE` additionally lets you fix the credit side by setting `beneficiary.amount` instead. Specify exactly one side.

5. **The estimate mode is now a dedicated dry-run endpoint.** The legacy API supported [`POST /api/v1/quote?estimate=true`](/bvnk/api-explorer/endpoints/quote-create) to get an indicative rate without persisting a quote. Send [`POST /payment/v3/transfers/dry-run`](/bvnk/api-explorer/endpoints/transfer-dry-run-v-3) instead. It takes the same request body as the create call and returns the rate, spread, fees, and converted amounts without persisting a transfer or moving funds. It works for both `QUOTE` and `AUTO_CONVERSION` conversions but not for same-currency transfers, where there is nothing to preview. See [Preview a transfer](../create-internal-transfer-v3#preview-a-transfer).

6. **The Quote API stays available for historical records.** The legacy [`GET /api/v1/quote/{uuid}`](/bvnk/api-explorer/endpoints/quote-read) is not deprecated. Quotes created through the Quote API before you migrate remain retrievable through it. Transfers created via v.3 must be queried with [`GET /payment/v3/transfers/{transferId}`](/bvnk/api-explorer/endpoints/transfer-read-v-3) instead.

## Migration checklist

1. **Update quote creation:** replace [`POST /api/v1/quote`](/bvnk/api-explorer/endpoints/quote-create) with [`POST /payment/v3/transfers`](/bvnk/api-explorer/endpoints/transfer-create-v-3).
   - Generate and send an `Idempotency-Key` header on every request.
   - Map `from`/`to` → `originator.currency`/`beneficiary.currency` and `fromWalletLsid`/`toWalletLsid` → `originator.walletId`/`beneficiary.walletId`.
   - Move `amountIn` to `originator.amount`; add a required `reference`.
   - Add `conversion: { type: "QUOTE" }` for firm-rate flows, `AUTO_CONVERSION` for immediate execution, or omit `conversion` for same-currency transfers.

2. **Update acceptance:** replace [`PUT /api/v1/quote/accept/{uuid}`](/bvnk/api-explorer/endpoints/quote-accept) with [`POST /payment/v3/transfers/{transferId}/actions`](/bvnk/api-explorer/endpoints/transfer-action-v-3).
   - Store `conversion.digest` from the create response and send `{ "type": "ACCEPT_QUOTE", "digest": "<digest>" }`.
   - Handle expired quotes by sending a `REFRESH_QUOTE` action to get a new digest, then re-accepting.

3. **Update status handling:** replace the dual `quoteStatus` + `paymentStatus` checks with the single `status` field.

  Treat `EXPIRED` as a distinct terminal state and `PENDING_QUOTE_ACCEPTANCE` as a non-terminal state that requires client action.

4. **Update response parsing:** read amounts from `originator.amount` / `beneficiary.amount`, the rate from `conversion.allInRate`, the fee from `fees.processingFee`, and switch timestamp parsing from Unix epoch to ISO-8601 (`createdAt`, `updatedAt`, `conversion.expiresAt`).

5. **Update status retrieval:** replace [`GET /api/v1/quote/{uuid}`](/bvnk/api-explorer/endpoints/quote-read) polling with [`GET /payment/v3/transfers/{transferId}`](/bvnk/api-explorer/endpoints/transfer-read-v-3), and optionally subscribe to the [Transfer webhook](/bvnk/api-explorer/bvnk-webhooks/payment-transfer-status-change-v-3/) to eliminate polling.

6. **Keep the Quote API for historical records:** continue using [`GET /api/v1/quote/{uuid}`](/bvnk/api-explorer/endpoints/quote-read) to retrieve quotes created before migration.

---

**Related guides:**
- [Create an internal transfer (v.3)](../create-internal-transfer-v3): full v.3 integration guide covering all three conversion types.
