Migrate fiat payments from v.2
Payments v.3 API endpoints are in beta and may change as the product evolves.
Fiat payouts are supported on v.3 and share the crypto endpoints, but payment rail selection, the required fields for each local banking network, and the Verification of Payee (VoP) confirmation flow are not finalized yet. This guide shows what is confirmed today and marks every interim mapping.
BVNK is unifying its fiat and crypto payments APIs. Fiat payouts move from /payment/v2/payouts to /payment/v3/payouts, the same endpoint that crypto payouts use, with the same envelope, status model, and webhook taxonomy as crypto payments and Internal Transfers.
Fiat v.2 is already close to the v.3 shape. v.2 payouts have an originator and a beneficiary, an entity object for the payee, and ISO 8601 timestamps. Most of the migration comes down to three changes: amounts become strings anchored on one side of the payment, method changes meaning, and statuses are renamed to match the model shared with crypto.
Benefits of migrating to v.3:
- One payout endpoint for fiat and crypto:
/payment/v3/payoutsserves both, andmethodon the response tells you which one a payout is. - One status model: fiat and crypto payouts share the same statuses, including new recall statuses for fiat.
- Verification as a payout status: VoP is expected to become the
PENDING_VERIFICATIONstatus on an existing payout, instead of a202response that creates no payout. - Payments that link to each other:
labels[]andrelatedPayments[]tie a refund to the pay-in it corrects. - An audit trail on every payout:
statusHistory[]records each status change with a timestamp.
v.3 is available alongside v.2. There is currently no mandatory migration or sunset date for the v.2 fiat endpoints, allowing you to migrate each flow individually at your own pace.
Migration scope
- In scope: fiat payouts on
/payment/v2/payouts, and the fiat payout and pay-in webhooks. - Not yet on v.3: receiving fiat pay-ins, refunds, beneficiary pre-verification, proof of payment, and pay-in simulation. Keep using the current endpoints. See Endpoint mapping.
- Not affected: virtual accounts, wallets, payment rules, payment limits, and the ledger. Transfers between your wallets are managed through Internal Transfers v.3, a separate endpoint.
What changes from v.2
The concepts v.3 is built on are explained in Payments API v.3. These are the differences from v.2 fiat payouts that drive most of the field mapping in this guide:
- Two sides, now in blocks: v.2 already described a payout as an
originatorand abeneficiary, but only the payee lived in its own block, underbeneficiary;walletId,amount, andcurrencysat at the top level. v.3 moves your side intooriginatortoo. Direction follows from the endpoint, and payout responses still returndirection: "OUT", as in v.2. See Originator and beneficiary. - One fixed amount, against any of three currencies: v.2 fixed only the debit. v.3 fixes the amount against exactly one of
originator.amount(the debit, matching v.2 behavior),beneficiary.amount(the credit), orcheckout.amount(the display value), and derives the other two. See Amounts. - Payment method and payment instrument: in v.2,
methodon the request selected the payment rail, for exampleSEPA_CT,ACH, orSWIFT. v.3 requests carry nomethod: BVNK infers fiat versus crypto frombeneficiary.paymentInstrument.typeand returnsmethod: "FIAT"or"CRYPTO"on responses. For a bank account,paymentInstrument.typeisBANK_ADDRESS. How a v.3 payout selects a rail is not specified yet. See Payment methods. - Verification of Payee as a status: in v.2, a EUR payout whose beneficiary name fails the VoP check returns
202withstatus: "PAYEE_VERIFICATION_REQUIRED", and no payout exists until you confirm it withCONFIRM_BENEFICIARYwithin five minutes. In v.3, this becomes thePENDING_VERIFICATIONstatus shared with crypto: the payout exists, is fully priced, and waits for confirmation before BVNK sends it. The v.3 confirmation action for fiat, the shape of the name-match result, and the confirmation window are not specified yet. See Payment statuses.
Identifiers ending in Reference are user-supplied, while those ending in Id are issued by BVNK. For example, customerReference and reference are provided by you, whereas customerId, id, contactId, and paymentId are provided by BVNK. This rule applies to all identifiers in the API.
Endpoint mapping
| Flow | Current endpoint | v.3 endpoint | Guide |
|---|---|---|---|
| Create and send a payout | POST /payment/v2/payouts | POST /payment/v3/payouts | Send fiat payouts |
| Get a payout | GET /payment/v2/payouts/{transactionId} | GET /payment/v3/payouts/{id} | Send fiat payouts |
| Confirm a payout after a VoP mismatch | POST /payment/v2/payouts/{Id}/actions with CONFIRM_BENEFICIARY | Expected: the payout is held at PENDING_VERIFICATION. The confirmation action is not specified yet. | Send fiat payouts |
| Verify a beneficiary before the payout | POST /platform/v1/beneficiaries/verification, then the X-Verification-Id header on the payout | No v.3 equivalent yet. Keep using the current endpoint with v.2 payouts. | Send payments |
| Get proof of payment | GET /payment/v2/payouts/{transactionId}/proof-of-payment | No v.3 equivalent yet. Keep using the v.2 endpoint. | — |
| Receive a fiat pay-in | No endpoint: pay-ins arrive at your virtual account | No change. Keep receiving fiat at your virtual accounts. | Receive fiat pay-ins |
| Estimate a refund and refund a pay-in | POST /payment/v1/payins/{transactionReference}/refund/estimatePOST /payment/v1/payins/{transactionReference}/refund | No v.3 endpoint. Fiat refunds on v.3 are not finalized. Keep using the current endpoints. | Refund payments |
| Simulate a pay-in in sandbox | POST /payment/v2/payins/simulation | No v.3 equivalent yet. Keep using the v.2 endpoint. | — |
Only payout creation and retrieval move to v.3 today. Continue using the current endpoints for every other fiat flow while you migrate payouts.
Payout lifecycle
In v.2, a payout with complete details is created and sent in one call. A EUR payout that fails the VoP name check is the exception: BVNK returns 202, creates no payout, and waits for your confirmation.
In v.3, a payout with complete details is also created and sent in one call. Verification is expected to be a status on the payout rather than a separate response.
- v.2: create, confirm if VoP fails
- v.3: one call
One call, plus a confirmation call when the beneficiary name does not match.
Endpoint changes
Most changes in this section are silent: existing parsing code may keep running but return incorrect values instead of errors. Start with the breaking changes.
Breaking changes
method changes meaning. In v.2, method is the payment rail, sent on the request and echoed on the response, for example SEPA_CT. In v.3, requests carry no method, and responses return FIAT or CRYPTO. Reporting or routing code that reads the rail from method keeps running but receives FIAT.
dateOfBirth changes format. v.2 takes DD-MM-YYYY, for example 15-01-1990. v.3 takes ISO 8601 YYYY-MM-DD, for example 1990-01-15. Convert stored dates before you send them.
type drops the :fiat suffix. v.2 returns type: "payment:payout:fiat". v.3 returns type: "payment:payout" for fiat and crypto alike. Filter on method: "FIAT" instead.
Status values change. ON_HOLD becomes HELD, and CANCELLED splits into REJECTED and EXPIRED. Handlers that compare status strings fall through silently. See the Status reference.
Conventions and fields
| Change | v.2 | v.3 | Notes |
|---|---|---|---|
| Money amounts | Numbers, for example 1000.5 | Strings, for example "1000.50" | Applies to all monetary fields, including fees. |
| Timestamps | ISO 8601 | ISO 8601 | Unchanged. |
| Date of birth | DD-MM-YYYY | YYYY-MM-DD | See Breaking changes. |
| Wallet | walletId at the top level | originator.walletId | originator.currency is derived from the wallet. |
| Amount and currency | amount and currency at the top level | One amount anchor, plus beneficiary.currency | Set originator.amount to keep v.2 behavior. See Amounts. |
| Response amounts | A flat amount | required.amount is what you asked for,actual.amount is what was paid | Applies on originator, beneficiary, and checkout, each a {currency, amount} object. Fees carry estimated and actual, except customerFee, which carries required and actual. originator.actual shows the debit once the quote is accepted, and every other actual has an amount of 0.00 until the payout reaches COMPLETED. Requests are unchanged. |
| Reference | reference, optional | reference, required | Reusing a reference returns a validation error and does not create a duplicate. |
| Payment method | method: the payment rail, on the request and response | method: FIAT or CRYPTO, on the response only | Rail selection is not specified yet. See Breaking changes. |
| Resource type | payment:payout:fiat | payment:payout | Shared with crypto payouts. |
| Direction | direction: "OUT" on responses | direction: "OUT" on responses | Unchanged. |
| Statuses | v.2 payout enum | The v.3 enum shared with crypto | See the Status reference. |
| Bank details | beneficiary.bankAccount, including intermediaryBanks[] | beneficiary.paymentInstrument with "type": "BANK_ADDRESS", including intermediaryBanks[] | bank.identificationCode becomes bank.bic, and bank.country moves into bank.address. |
| Tracking details | trackingDetails[] on v.2 webhooks, with uetr, imad, omad, traceNumber, and externalTransactionId | trackingDetails[] with on-chain fields: hash, network, amount, currency, and confirmedAt | Bank-rail tracking is not yet modeled. |
| Status history | Not available | statusHistory[], an array of {status, timestamp} | New. No migration required. |
labels[]relatedPayments[] | Not available | New. Classify why a payment exists, and link it to its counterpart | See Migrate pay-ins and refunds. |
metadata | Up to five string values | Unchanged | No migration required. |
Hawk authentication works the same way in v.3 as in v.2. No authentication changes are required.
Fields not yet modeled in v.3
The v.3 spec does not yet define these v.2 fiat fields. Until it does, treat any v.3 shape for them as provisional:
- Payment rail: the v.2 request
method, for exampleSEPA_INSTorFEDWIRE. - Remittance information:
beneficiary.remittanceInformation, the payment reference your payee sees on their statement. - Requester IP address:
requestDetails.originator.ipAddress. The closest v.3 field isbeneficiary.requesterIpAddress, which v.3 describes as the IP address of the customer receiving the payout. Confirm with BVNK which one applies before mapping it. - Verification of Payee: the
X-Verification-Idheader, theCONFIRM_BENEFICIARYaction, and theverificationResultobject. - Bank-rail tracking: the
uetr,imad,omad,traceNumber, andexternalTransactionIdfields ontrackingDetails[].
Status reference
v.3 payouts use one status model for fiat and crypto. Several statuses are conditional and only occur if the corresponding step applies, so a payout that needs no approval or verification goes straight to PROCESSING. For the full list of v.3 statuses and the payout status diagram, see Payment statuses.
| v.2 status | v.3 status | Notes |
|---|---|---|
PENDING | No direct equivalent | A new v.3 payout is at PROCESSING, or at a conditional status such as PENDING_APPROVAL or PENDING_VERIFICATION. |
PAYEE_VERIFICATION_REQUIRED (202 response, no payout yet) | PENDING_VERIFICATION (conditional, expected) | The payout exists and waits for confirmation. See What changes from v.2. |
PENDING_APPROVAL | PENDING_APPROVAL (conditional) | Unchanged. A member of your team must approve the payout. |
PROCESSING | PROCESSING | Unchanged. |
ON_HOLD | HELD | Renamed. Held for compliance reasons. Reversible: a held payout can return to PROCESSING. |
COMPLETED | COMPLETED | Unchanged. The beneficiary bank can still return the funds afterwards, which moves the payout to RETURNED. |
CANCELLED | REJECTED or EXPIRED | Split in two. REJECTED when your team declines the approval, EXPIRED when the approval is not given within the seven-day window. Neither is retryable: create a new payout. |
EXPIRED | EXPIRED | Unchanged. In v.2, this status appears on webhooks only. |
FAILED | FAILED | Unchanged. The payout was attempted from PROCESSING and did not succeed. |
RETURNED | RETURNED (conditional) | Unchanged, and now also reached when a recall succeeds. Terminal. |
| ❌ | PENDING_RECALL (conditional) | New. A recall has been requested on a payout that is already in flight. |
| ❌ | RECALL_REJECTED (conditional) | New. Terminal. The beneficiary bank declined the recall, and the payout stands. |
Migrate payouts
The v.2 payout below sends 1,000.50 EUR over SEPA to an individual payee. In v.3, your wallet and the amount move into originator, the currency moves into beneficiary, and the rail is no longer sent.
- v.2
- v.3
{
"walletId": "a:24092324785677:o6rhCEZ:1",
"amount": 1000.5,
"currency": "EUR",
"method": "SEPA_CT",
"reference": "PAYOUT-2025-001",
"beneficiary": {
"remittanceInformation": "Invoice INV-2025-0412",
"entity": {
"type": "INDIVIDUAL",
"firstName": "Anna",
"lastName": "Schmidt",
"dateOfBirth": "15-01-1990",
"relationshipType": "THIRD_PARTY",
"address": {
"addressLine1": "Hauptstrasse 42",
"city": "Munich",
"postCode": "80331",
"country": "DE"
}
},
"bankAccount": {
"accountNumber": "DE89370400440532013000",
"bank": {
"identificationCode": "COBADEFFXXX",
"country": "DE"
}
}
},
"fees": {
"customerFee": {
"amount": 5,
"currency": "EUR"
}
},
"metadata": {
"invoiceId": "INV-2025-0412"
},
"requestDetails": {
"originator": {
"ipAddress": "77.71.188.87"
}
}
}
Request field changes:
| v.2 field | v.3 field | Notes |
|---|---|---|
walletId | originator.walletId | Moved into the originator block. The wallet determines originator.currency automatically. |
amount: number | originator.amount: "string" | Numbers become strings. Fixes the debit, as in v.2. Set only one amount anchor. |
currency | beneficiary.currency | The currency the payee receives. |
method | ❌ | Not accepted on v.3 requests. Rail selection is not specified yet. |
reference | reference | Now required. Reusing a reference returns a validation error. |
beneficiary.remittanceInformation | ❌ | Not yet modeled. |
beneficiary.entity.type | beneficiary.entity.type | Unchanged. Values are still INDIVIDUAL or COMPANY. |
...firstNamelastName | beneficiary.entity.firstName.lastName | Unchanged. INDIVIDUAL entities only. |
...legalName | beneficiary.entity.legalName | Unchanged. COMPANY entities only. |
...dateOfBirth: "15-01-1990" | beneficiary.entity.dateOfBirth: "1990-01-15" | Format changes from DD-MM-YYYY to YYYY-MM-DD. |
...relationshipType | beneficiary.entity.relationshipType | Unchanged. Values are still SELF_OWNED or THIRD_PARTY. |
...address.* | beneficiary.entity.address.* | Same field names. v.3 requires only country, where v.2 also required addressLine1 and city. Keep sending the full address: banking partners may reject payouts with incomplete details. |
| ❌ | beneficiary.entity.registrationNumber | New and optional. COMPANY entities only. |
beneficiary.bankAccount | beneficiary.paymentInstrument | Renamed. Add "type": "BANK_ADDRESS", which marks the payout as fiat. |
...accountNumber | beneficiary.paymentInstrument.accountNumber | Unchanged. Set accountNumberFormat, for example IBAN, SCAN, or ABA, to match. |
...accountType | beneficiary.paymentInstrument.accountType | Unchanged. Mandatory for USD payouts. |
...bank.identificationCode | beneficiary.paymentInstrument.bank.bic | Renamed. For a local USD payout, send the ABA routing number in bank.nid. |
...bank.nid | beneficiary.paymentInstrument.bank.nid | Unchanged. |
...bank.country | beneficiary.paymentInstrument.bank.address.country | Moved into the bank address. |
...bank.address | beneficiary.paymentInstrument.bank.address | Unchanged. |
...intermediaryBanks[] | beneficiary.paymentInstrument.intermediaryBanks[] | Moved into the payment instrument. Each entry carries name, bic, nid, branchCode, and address. |
| ❌ | beneficiary.contactId | New. References a stored contact instead of an inline entity. Send contactId or entity, never both. |
| ❌ | beneficiary.customerIdbeneficiary.customerReference | New. The BVNK-issued ID of the customer, and your own reference for them. |
fees.customerFee.amount: number | fees.customerFee.amount: "string" | Numbers become strings. Still deducted from the payee's amount. |
metadata | metadata | Unchanged. Up to five string values. |
requestDetails.originator.ipAddress | ❌ | No originator-side IP field in v.3. The closest field is beneficiary.requesterIpAddress. See Fields not yet modeled in v.3. |
X-Verification-Id header | ❌ | Not specified yet. |
Response field changes:
| v.2 field | v.3 field | Notes |
|---|---|---|
id | id | Unchanged. Still a UUID. |
type: "payment:payout:fiat" | type: "payment:payout" | Shared with crypto payouts. Filter on method instead. |
method: "SEPA_CT" | method: "FIAT" | Same field name, different purpose. v.2 returned the rail; v.3 returns the payment method. |
direction: "OUT" | direction: "OUT" | Unchanged. |
status | status | Values change. See the Status reference. |
reference | reference | Unchanged. |
fees.processingFee.{amount, currency} | fees.processingFee.estimated.{currency, amount}fees.processingFee.actual.{currency, amount} | Numbers become strings. estimated is the fee as quoted; actual is the fee charged, with an amount of 0.00 until COMPLETED. |
fees.customerFee.{amount, currency} | fees.customerFee.required.{currency, amount}fees.customerFee.actual.{currency, amount} | Numbers become strings. required is the fee you set; actual is the fee deducted, with an amount of 0.00 until COMPLETED. null when not supplied. |
| ❌ | fees.networkFee | Crypto payouts only. Omitted on fiat payouts. |
originator.amount: number | originator.required.amountoriginator.actual.amount | Numbers become strings. required is what you asked for, and actual is what was debited, set once the quote is accepted. |
originator.currencyoriginator.walletId | originator.required.currencyoriginator.walletId | currency moves inside the amount objects. walletId is unchanged. |
originator.customerId | ❌ | Not returned on the v.3 payout originator. |
originator.entity | ❌ | Not returned on the v.3 payout originator. |
beneficiary.amount: number | beneficiary.required.amountbeneficiary.actual.amount | Numbers become strings. actual has an amount of 0.00 until COMPLETED. |
beneficiary.currency | beneficiary.required.currency | Moves inside the amount objects. |
beneficiary.remittanceInformation | ❌ | Not yet modeled. |
beneficiary.entity | beneficiary.entity | Same fields. dateOfBirth uses YYYY-MM-DD. |
beneficiary.bankAccount | beneficiary.paymentInstrument | "type": "BANK_ADDRESS", with the same field changes as on the request. accountNumberFormat is not returned. |
createdAtupdatedAt | createdAtupdatedAt | Unchanged. ISO 8601. |
metadata | metadata | Unchanged. |
requestDetails | ❌ | Not returned. |
| ❌ | conversion | New. null when the payout involves no currency conversion. |
| ❌ | checkout | New. The display currency block shared with crypto payouts. Mirrors the originator currency and amount when you set no display currency. |
| ❌ | instructions[] | New on the shared schema. Always empty on payouts. |
| ❌ | flow | New. default for the standard flow. |
| ❌ | trackingDetails[] | New on the payout resource. The v.3 entries carry on-chain fields only. Bank-rail tracking is not yet modeled. |
| ❌ | statusHistory[] | New. Array of {status, timestamp} on the payout. |
| ❌ | labels[]relatedPayments[] | New. Populated when this payout relates to another payment, for example a refund. |
202 body: Id, status, requiredAction, verificationResult | ❌ | Expected to be replaced by status: "PENDING_VERIFICATION" on the payout. The name-match result shape is not specified yet. |
For the full v.3 fiat payout request and response, see Send fiat payouts.
Migrate pay-ins and refunds
Pay-ins
v.2 has no endpoint to create a fiat pay-in: funds arrive at your virtual account, and BVNK sends a payment:v2:payin:status-change webhook. This does not change. POST /payment/v3/payins creates payment links whose deposit instructions are crypto only (instructions[].type is always CRYPTO), so keep receiving fiat at your virtual accounts. See Receive fiat pay-ins.
Refunds
Today, you refund a fiat pay-in with the refund endpoints described in Refund payments. In v.3, a refund is not a separate endpoint or event. It is an ordinary payout that carries labels: ["REFUND"], with relatedPayments[] linking back to the pay-in it corrects, and the pay-in linking forward to the refund. How a fiat refund payout is created on v.3, and whether funds return to the original bank account automatically, is not finalized. Keep using the current refund endpoints.
Consolidate webhooks
v.3 uses one status-change event per resource, shared by fiat and crypto. The status field indicates what happened, method tells fiat from crypto, and labels[] explains any unusual circumstances.
| v.2 event | v.3 event | Notes |
|---|---|---|
payment:v2:payout:status-change | payment:v3:payout:status-change | The same event as crypto payouts. data.method returns FIAT instead of the rail, data.type drops the :fiat suffix, and data.direction is not sent. Read the progression from statusHistory[]. The fiat bank-detail shape is not finalized. |
payment:v2:payin:status-change | payment:v3:payin:status-change | Planned. The v.3 webhook spec states that fiat shares this event, but the fiat pay-in payload is not finalized. Keep your v.2 handler in place. |
Full event schemas and example payloads live in the Payments v.3 webhooks reference.
Migration checklist
Follow these steps in order. The first three steps help prevent silent data errors.
- Audit the breaking changes. Find every place your integration reads the rail from
method, filters ontype: "payment:payout:fiat", or compares payout status strings, and plan the v.3 replacements. - Update money and date handling. Every amount, including fees, is now a string. Convert
dateOfBirthfromDD-MM-YYYYtoYYYY-MM-DD. - Map statuses. Rename
ON_HOLDtoHELD, splitCANCELLEDintoREJECTEDandEXPIRED, and handle the newPENDING_RECALLandRECALL_REJECTEDstatuses. - Update payout creation. Replace
POST /payment/v2/payoutswithPOST /payment/v3/payouts.- Move
walletIdintooriginator.walletId,amountintooriginator.amount, andcurrencyintobeneficiary.currency. - Remove
methodfrom the request. - Always send a unique
reference.
- Move
- Plan for the VoP change. Prepare to handle
PENDING_VERIFICATIONon an existing payout instead of a202response, once BVNK publishes the fiat confirmation action. - Consolidate your webhook handlers. Subscribe to
payment:v3:payout:status-changeand branch onstatus,method, andlabels[]. - Leave the other fiat flows on their current endpoints. Pay-ins, refunds, beneficiary pre-verification, proof of payment, and pay-in simulation have no v.3 equivalent yet.
- Leave your v.2 integration in place until each flow is cut over. Nothing is being switched off, so v.2 traffic keeps working while you migrate.
Related guides:
- Send fiat payouts: the v.3 fiat payout request and response, including bank details.
- Migrate crypto payments from v.1: the v.3 concepts shared with crypto, including quotes, the three currencies, and the full status model.
- Send payments: the current v.2 fiat payout integration, including VoP.