Skip to main content
BVNK

Migrate crypto payments from v.1

Beta

Payments v.3 API endpoints are in beta and may change as the product evolves. If you already have an existing integration with /payment/v1/ endpoints, you can continue using it. There is currently no mandatory migration or sunset date for existing endpoints, allowing you to migrate each flow individually at your own pace.

BVNK is unifying its fiat and crypto payments APIs. Crypto payouts and crypto payment links (payins) move onto /payment/v3/*, using the same envelope, originator and beneficiary structure, and webhook taxonomy as fiat payments and Internal Transfers.

Version 3 introduces key changes: payments now have two named sides instead of a single wallet and direction flag, three currencies instead of one amount and display field, and a quote as a property of the payment rather than a separate resource. These changes drive most of the field mapping in this guide.

Benefits of migrating to v.3:

  • One endpoint per direction: /payment/v3/payouts and /payment/v3/payins replace a single /api/v1/pay/summary that switched behaviour on a type flag.
  • A display currency you can price against: quote a payment in any currency, including ones BVNK does not settle in, and let BVNK derive the debit and the credit.
  • Quotes without a second resource: create, reprice, and accept on the payment itself, instead of creating an estimate and converting it into a payment.
  • Pricing you can show a customer: the rate before spread, the rate including spread, and the spread between them are disclosed separately.
  • One webhook per resource: two status-change events replace a dozen v.1 event names, with labels[] telling you why a payment is unusual.
  • Payments that link to each other: relatedPayments[] ties a refund to the payin that caused it.

Migration scope​

  • Covered in this guide: crypto payouts and crypto payment links (payins).
  • Fiat payouts: also supported on v.3, through the same /payment/v3/payouts endpoint. BVNK infers fiat versus crypto from beneficiary.paymentInstrument.type. To move fiat payouts from v.2, see Migrate fiat payments from v.2.
  • Fiat pay-ins and refunds: not on v.3 yet. Keep using the current fiat endpoints.
  • Not affected: orchestrations, wallets, and the ledger. Internal Transfers are managed through a separate v.3 endpoint and require no changes.

What changes from v.1​

The concepts v.3 is built on are explained in Payments API v.3. These are the differences from v.1 that drive most of the field mapping in this guide:

  • Two sides instead of a direction flag: v.1 described a payment from your point of view, with one walletId, one amount, and a type of IN or OUT. v.3 has an originator and a beneficiary, and direction follows from the endpoint you call. type: "IN" and type: "OUT" are no longer accepted on requests and have no effect if included. Responses return the direction as direction: OUT on payouts and IN on payins. walletId moves under originator on payouts and under beneficiary on payins. See Originator and beneficiary.
  • Three named currencies: v.1 only exposed the third currency through displayCurrency. v.3 names the originator, beneficiary, and display currencies explicitly. See Currency types.
  • One fixed amount, against any of the three currencies: v.1 let you set the wallet amount or the paid amount, never both. v.3 applies the same rule across all three currencies. See Amounts.
  • One payment instrument: v.1's payOutDetails and address objects are replaced by paymentInstrument, discriminated by type. Requests carry no method: BVNK infers fiat versus crypto from paymentInstrument.type and returns method on responses. See Payment methods.
  • Quotes on the payment, not a separate estimate resource: see Payout lifecycle.
  • Travel Rule data on entity: v.1 sent party details in complianceDetails.partyDetails[]. See Travel Rule.
  • Identifier naming: identifiers ending in Reference are yours, and identifiers ending in Id are issued by BVNK. This is why customerId changes meaning; see Breaking changes.

Endpoint mapping​

Flowv.1 endpointv.3 endpointGuide
Create and send a payoutPOST /api/v1/pay/summary (type: "OUT")POST /payment/v3/payoutsSend stablecoin payouts
Quote a payout: create, update, acceptPOST /api/v1/pay/estimate
PUT /api/v1/pay/estimate/{externalId}/update
POST /api/v1/pay/estimate/{externalId}/accept
POST /payment/v3/payouts
PATCH /payment/v3/payouts/{id}
POST /payment/v3/payouts/{id}/action
Send stablecoin payouts
Send a quoted payoutPUT /api/v1/pay/{uuid}/confirm/summaryFolded into POST /payment/v3/payouts/{id}/action with ACCEPT_QUOTESend stablecoin payouts
Hold a payout for confirmation (two-step)POST /api/v1/pay/summary with "twoStep": "true"
PUT /api/v1/pay/{uuid}/confirm/summary
POST /payment/v3/payouts with "confirmationRequired": true
POST /payment/v3/payouts/{id}/action
Send stablecoin payouts
Get a payoutGET /api/v1/pay/{uuid}GET /payment/v3/payouts/{id}Send stablecoin payouts
Preview a payoutPOST /api/v1/pay/summary/dry-runNo v.3 equivalent yet. Keep using the v.1 endpoint.—
Validate a crypto addressPUT /api/v1/pay/validateNo v.3 equivalent yet. Keep using the v.1 endpoint.—
Create a payment linkPOST /api/v1/pay/summary (type: "IN")POST /payment/v3/payinsReceive stablecoins via Payment Links
Get a payinGET /api/v1/pay/{uuid}GET /payment/v3/payins/{id}Receive stablecoins via Payment Links
Refresh a quote with no rate changePUT /api/v1/pay/estimate/{externalId}/update (empty body)POST /payment/v3/payouts/{id}/action with REFRESH_QUOTE and digestSend stablecoin payouts
Reprice a quote with a new amountPUT /api/v1/pay/estimate/{externalId}/update with amountPATCH /payment/v3/payouts/{id} with the new amountSend stablecoin payouts
Add beneficiary details before acceptingNot possible in v.1PATCH /payment/v3/payouts/{id} with beneficiary.entity and beneficiary.paymentInstrumentSend stablecoin payouts
Accept a quotePOST /api/v1/pay/estimate/{externalId}/accept with payOutDetails and complianceDetailsPOST /payment/v3/payouts/{id}/action with ACCEPT_QUOTESend stablecoin payouts

Two v.1 endpoints currently have no v.3 equivalent: payout dry-run and address validation. Continue using these v.1 endpoints while migrating other payout flows to v.3.

Payout lifecycle​

In v.1, an estimate was a resource in its own right. You created one, optionally refreshed it, accepted it, which turned it into a payment, then confirmed the payment to send it: four calls across two resources.

In v.3, what v.1 called an estimate is called a quote, and it is a property of the payout rather than a resource. POST /payment/v3/payouts is a single create whose result depends on how complete your payload is. See What you send decides what happens. A complete payout plus "confirmationRequired": true is held at PENDING_VERIFICATION and replaces the v.1 two-step payout.

Completeness is the trigger, not a flag

There is no quote-only parameter. Withholding the destination address is what keeps a payout a quote, so including an address you happen to have will send live funds.

Two resources and up to four calls: an estimate that you refresh and accept, then a payment that you confirm.

Compared with v.1:

  • An empty-body estimate update becomes a REFRESH_QUOTE action carrying the current conversion.digest.
  • An estimate update with a new amount becomes a PATCH on the payout, which reprices the quote.
  • Accepting the estimate and confirming the payment become one ACCEPT_QUOTE action. It requires beneficiary.entity or beneficiary.contactId to be set by then, either at create time or by a PATCH. There is no separate confirm call.

For how the digest and repricing work, see the Quote flow.

Endpoint changes​

Several changes in this section are silent: existing integrations may continue to run but return incorrect values instead of errors. Start by addressing the two most critical changes.

Breaking changes​

Response amounts move into objects. This is the highest-risk change in the release. Requests are unchanged: you still send a flat currency and amount, with the amount on one of originator, beneficiary, or checkout. In responses and webhooks, the paths change. v.1's amount (asked for) becomes required.amount, and v.1's actual (paid) becomes actual.amount, on originator, beneficiary, and checkout. Fees follow the same pattern with estimated and actual. The meaning is the same as in v.1, so code still reading a flat amount from a response finds nothing rather than the wrong figure. Review all uses of amount and actual before migrating any flows. See Amounts.

customerId changes owner. In v.1, customerId was your own reference for the customer. In v.3, customerId is issued by BVNK, and your reference becomes customerReference, following the Reference and Id rule above. Both fields are now at the top level of originator and beneficiary. If your v.1 value was a BVNK-issued ID, it remains unchanged. If it was your own reference, move it to customerReference.

Conventions and fields​

Changev.1v.3Notes
Money amountsNumbers, for example 9.99Strings, for example "9.99"Applies to all monetary fields.
Amount objectsPer-currency objects with amount (asked for) and actual (paid): walletCurrency, paidCurrency, and displayCurrency. Fees as feeCurrency and networkFeeCurrencyRequests: unchanged, a flat currency and amount, with an amount on one of originator, beneficiary, or checkout.
Responses and webhooks: originator, beneficiary, and checkout carry required and actual; processingFee and networkFee carry estimated and actual, and a payout's customerFee carries required and actual. Each is a {currency, amount} object. conversion.spread, trackingDetails[], and instructions[] keep a plain currency and amount
required or estimated, and actual, are always present, even when they match. On payouts, actual has an amount of 0.00 until money moves: originator.actual shows the debit once the quote is accepted, and the rest fill in at COMPLETED. On payins, actual is null until PROCESSING. If the customer sends the wrong coin, required shows the coin asked for and actual the coin that arrived.
TimestampsEpoch milliseconds, for example 1741985003161ISO 8601, for example "2026-05-05T12:30:00Z"Applies to all timestamp fields.
Directiontype: "IN" or "OUT" on the requestInferred from the endpoint namespace. Responses return direction: OUT on payouts, IN on payinsNot accepted on requests. Ignored if included.
amount and actualamount is what you asked for,
actual is what was paid
required.amount is what you asked for,
actual.amount is what was paid
Highest-risk change in the release. The meaning is the same as in v.1, but the paths change: v.1 amount becomes required.amount, and v.1 actual becomes actual.amount. Code still reading a flat amount from a response finds nothing rather than the wrong figure. Requests are unchanged. See Breaking changes.
customerIdYour own reference to your customerThe ID of a customer that BVNK issues.Your own free-text reference becomes customerReference. See Breaking changes.
complianceDetails wrapperTop level on requests, with a partyDetails[] arrayRemoved. Its contents move onto beneficiary on a payout and originator on a payinentityType becomes entity.type, countryCode becomes entity.address.country, and requesterIpAddress is retained on the party.
Travel Rule fields (EU crypto)—On entity: placeOfBirth (Pair A), or identification[] plus a full address (Pair B), for INDIVIDUAL originators; legalEntityIdentifier (optional) for COMPANY entitiesINDIVIDUAL payin originators must send exactly one pair. COMPANY originators need a full address. Responses echo what was sent.
StatusesA small enum shared by both directionsExpanded and normalised per directionSee the Status reference.
Network and protocolBoth protocol (token standards such as ERC20) and network (chain names such as ETHEREUM) are supportednetwork only, chain names onlyIf you already send network, the value is unchanged. Remap the values if you still send protocol. See Network and protocol values.
Payment instrumentsAn address object with an alternatives[] arraypaymentInstrument: write-only on requests, read-only on responsesA request sets beneficiary.paymentInstrument.address, a single destination. A response returns beneficiary.paymentInstrument.addresses[] holding that one address, and originator.paymentInstrument.addresses[] holding one entry per address a payin customer sent from, across late or partial payments. The asymmetry is deliberate. On payins, the accepted networks appear as instructions[] instead, one entry per network, with no primary/alternative split.
DurationsexpiryMinutes: 30expiryDuration: "PT30M"Changed to ISO 8601 duration format.
Exchange ratesexchangeRate.{base, counter, rate}, plus a reciprocal displayRateA conversion block:
- baseRate before spread
- allInRate including spread
- conversion.spread: spread itself
v.1's single rate maps to allInRate, not baseRate. checkout.displayRate is a separate leg: the display-currency conversion, not the inverse of allInRate.
Quote validityquoteExpiryDate and acceptanceExpiryDate, with a short acceptance windowOne window: conversion.validityDuration (for example "PT60M") and conversion.expiresAtExpired quotes are rejected. There is no separate price lock.
Pegged paymentsA top-level pegged booleanconversion.pegAdjustment objectDetect pegging by checking for the object. Omitted on non-pegged payments.
On-chain transactionstransactions[]trackingDetails[], one entry per on-chain transaction, with hash, amount, currency, network, and confirmedAtSending addresses move to originator.paymentInstrument.addresses[].
Status historyNot availablestatusHistory[], an array of {status, timestamp} on every payment resourceNew field. No migration required.
metadataAvailableFree-form object, unchangedNo migration required. Use customerReference for customer references instead of metadata.
labels[]
relatedPayments[]
Not availableNew. Classify why a payment exists, and link it to its counterpartSee Exceptions, refunds, and linked payments.

Hawk authentication and the Idempotency-Key header function the same way in v.3 as in v.1. No authentication changes are required.

Network and protocol values​

If you currently send protocol (a token standard) rather than network (a chain name), map the values before switching:

v.1 protocolv.3 network
ERC20ETHEREUM
TRC20TRON
BEP20BSC
POLYGONPOLYGON
SOLSOLANA
BTCBITCOIN
LTCLITECOIN
XRPXRP
ARBITRUMARBITRUM
BASEBASE

Exceptions, refunds, and linked payments​

v.1 gave a payment that went wrong its own webhook name. v.3 records it on the ordinary payment resource instead, with two fields that have no v.1 equivalent: labels[] classifies the payment, and relatedPayments[] links it to its counterpart, for example a refund payout to the overpaid payin it corrects. The event name no longer tells you what happened; the labels do. See Labels and related payments, and Consolidate webhooks for the event mapping.

Status reference​

v.3 has more statuses than v.1, but several are conditional and only occur if the corresponding step applies, so not all of them appear in every flow. For what each v.3 status means and the payout status diagram, see Payment statuses.

v.1 statusv.3 statusNotes
PENDING (payout)PROCESSINGA payout sent in one call starts at PROCESSING, or at PENDING_APPROVAL if your account requires approvals.
PENDING (payment link)PENDING_CUSTOMER_PAYMENTThe payin is priced and waiting for your customer to send funds.
PENDING with quoteStatus: "TEMPLATE"PENDING_QUOTE_ACCEPTANCEAn unpriced payout, or a quote not yet accepted. v.3 has no quoteStatus.
PENDING with quoteStatus: "ACCEPTED" on a two-step payoutPENDING_VERIFICATIONFully priced, quote accepted, and held until your customer confirms it.
PROCESSINGPROCESSINGUnchanged.
COMPLETECOMPLETEDRenamed.
UNDERPAIDCOMPLETED with labels: ["UNDER"]An underpayment is a label, not a status.
EXPIREDEXPIREDUnchanged. A payin that arrives late and still completes moves on to PROCESSING and COMPLETED, with labels: ["LATE"].

Migrate payouts​

The simplest payout is the one where you already know the destination: a beneficiary currency, a crypto address, and an amount in exactly one of the three currencies. In v.1 that was one call to a shared endpoint with type: "OUT"; in v.3 it is one call to a dedicated one, with the merchant's details on originator and the payee's on beneficiary.

Request: POST /api/v1/pay/summary
{
"walletId": "a:25022613287255:zmHs0pg:1",
"type": "OUT",
"amount": 10,
"currency": "USD",
"expiryMinutes": 30,
"reference": "REF46730",
"returnUrl": "https://merchant.com/status",
"payOutDetails": {
"code": "crypto",
"currency": "USDT",
"network": "ETHEREUM",
"address": "0x02ae6765C6991813a3EAa86fe63ebBCA1c9EC156",
"tag": ""
},
"customerId": "9420b652-c6e9",
"complianceDetails": {
"requesterIpAddress": "77.71.188.87",
"partyDetails": [
{
"type": "BENEFICIARY",
"entityType": "INDIVIDUAL",
"firstName": "John",
"lastName": "Mirra",
"dateOfBirth": "1984-06-30",
"relationshipType": "THIRD_PARTY",
"countryCode": "DE"
}
]
}
}

Request field changes:

All destination fields are present. The payout is created and sent in one call.

v.1 fieldv.3 fieldNotes
walletIdoriginator.walletIdMoved into the originator block. The wallet determines originator.currency automatically.
type: "OUT"❌Direction is inferred from the /payouts namespace.
amount: numberoriginator.amount, beneficiary.amount, or checkout.amount: "string"The display amount. Set exactly one anchor. Numbers become strings.
currencycheckout.currencyThe display currency. Can be a currency BVNK does not settle in.
displayCurrencycheckout.currencyMaps to the same field as currency. v.1 provided two ways to set the display currency.
expiryMinutes: 30expiryDuration: "PT30M"Changed to ISO 8601 duration format.
referencereferenceUnchanged.
returnUrlcheckout.returnUrlDestination for a customer hosted page, if used.
payOutDetails.code: "crypto"beneficiary.paymentInstrument.type: "CRYPTO_ADDRESS"Replaced by the payment instrument type. Required because v.3 uses the same endpoint for fiat and crypto and has no request-side method: fiat versus crypto is inferred from this field. method is returned on responses.
payOutDetails.currencybeneficiary.currencyThe currency the payee receives.
payOutDetails.network or payOutDetails.protocolbeneficiary.paymentInstrument.networkv.1 accepted both network and protocol. If you send network, the value is unchanged. If you send protocol, map it first. See Network and protocol values.
payOutDetails.addressbeneficiary.paymentInstrument.addressMoved. One destination per payout. Write-only: responses return it in addresses[].
payOutDetails.tagbeneficiary.paymentInstrument.tagMoved. v.1 used "" for absent; v.3 uses null.
customerIdbeneficiary.customerIdThe BVNK-issued ID of the customer. Your own references become beneficiary.customerReference. See Breaking changes.
embeddedCustomerDetails.reference❌Identifies the intermediary wallet for flow: "EMBEDDED_CRYPTO" payout routing, a different concept from the beneficiary. No v.3 equivalent yet.
contactIdbeneficiary.contactIdReferences a stored contact instead of an inline entity. Send contactId or entity, not both.
complianceDetails (wrapper)❌Removed. Contents moved onto beneficiary.
complianceDetails.requesterIpAddressbeneficiary.requesterIpAddressMoved onto the beneficiary.
partyDetails[].type: "BENEFICIARY"❌Redundant. Position under beneficiary conveys the meaning.
partyDetails[].entityTypebeneficiary.entity.typeRenamed. Values are still INDIVIDUAL or COMPANY.
...firstName
lastName
dateOfBirth
beneficiary.entity.firstName
.lastName
.dateOfBirth
Moved. INDIVIDUAL entities only.
...legalName
registrationNumber
beneficiary.entity.legalName
.registrationNumber
Moved. COMPANY entities only.
...relationshipTypebeneficiary.entity.relationshipTypeMoved. Values unchanged (SELF_OWNED, THIRD_PARTY). Affects fee pricing on crypto.
...countryCode: "DE"beneficiary.entity.address.country: "DE"Renamed and nested deeper. v.3 accepts a full structured address; v.1 accepted a country code only.
❌fees.customerFeeNew. Optional. Your markup, deducted from the payee's amount.

Response field changes:

v.1 fieldv.3 fieldNotes
uuididRenamed. Still a UUID.
merchantId❌Derived from auth context.
merchantDisplayName❌Replaced by the wallet name.
walletIdoriginator.walletIdMoved into the originator block.
type: "OUT"type: "payment:payout"
direction: "OUT"
Split into two fields. type is now a resource discriminator; direction carries what v.1's type conveyed.
subType: "merchantPayOut"❌Covered by type.
❌method: "CRYPTO"Returned on responses and webhooks, so you can query and filter payments by payment method. Inferred from beneficiary.paymentInstrument.type; not sent on requests.
status: "PENDING"status: "PROCESSING"Status values changed. On payment links, PENDING becomes PENDING_CUSTOMER_PAYMENT. See Status reference.
dateCreated (epoch ms)createdAt (ISO 8601)Renamed and reformatted.
lastUpdated (epoch ms)updatedAt (ISO 8601)Renamed and reformatted.
expiryDate (epoch ms)expiresAt (ISO 8601)Renamed and reformatted.
quoteExpiryDateconversion.expiresAtBVNK accepts the quote at creation on this flow. conversion.expiresAt matches the payout's own expiresAt.
acceptanceExpiryDate❌No separate acceptance window. The rate is locked for conversion.priceLockDuration, 30 seconds, until conversion.priceLockExpiresAt.
quoteStatus: "ACCEPTED"❌Inferred from status.
displayCurrency.currencycheckout.required.currency
checkout.actual.currency
The display currency, now inside each amount object.
displayCurrency.amount (asked for)checkout.required.amountSame meaning as v.1, new path.
displayCurrency.actual (paid)checkout.actual.amountSame meaning as v.1, new path. checkout.actual has an amount of 0.00 until the payout reaches COMPLETED.
walletCurrency.{currency, amount, actual}originator.required.{currency, amount}
originator.actual.{currency, amount}
Moved into the originator block. Numbers to strings. v.1 amount becomes required.amount, and v.1 actual becomes actual.amount. Fees are not folded in; see totalWalletAmount below.
paidCurrency.{currency, amount, actual}beneficiary.required.{currency, amount}
beneficiary.actual.{currency, amount}
Moved into the beneficiary block, with the same path change. v.1's "paid" prefix referred to the payee.
feeCurrency.{currency, amount}fees.processingFee.estimated.{currency, amount}
fees.processingFee.actual.{currency, amount}
Moved into the fees block, as quoted and as charged. Charged as a separate wallet line item.
networkFeeCurrency.{currency, amount}fees.networkFee.estimated.{currency, amount}
fees.networkFee.actual.{currency, amount}
Moved into the fees block, as quoted and as charged. Charged as a separate wallet line item.
totalWalletAmount❌Calculate by summing originator.actual.amount and each fee's actual.amount, or required and estimated before the payout completes. v.1 excluded the network fee.
❌fees.customerFeeNew. Echoed from the request as required, with actual at 0.00 until it's deducted; null when not supplied. Deducted from the payee's amount.
displayRate.{base, counter, rate}checkout.displayRate.{from, to, rate}Restructured with named fields. This is the display-currency leg, not the inverse of allInRate.
exchangeRate.{base, counter, rate}conversion.baseRate
conversion.allInRate
v.1's single rate maps to allInRate. baseRate is new: the rate before spread.
❌conversion.spread.{percentage, amount, currency}New. Spread disclosed separately rather than embedded in the rate.
❌conversion.type: "QUOTE"New. QUOTE on this flow too: BVNK accepts the quote for you and sends the payout immediately.
❌conversion.pegAdjustment.*New. Present on pegged payments only. Omitted otherwise.
address.{address, tag, network}beneficiary.paymentInstrument.{addresses[], tag, network}Moved onto the beneficiary's payment instrument. addresses[] holds the one destination.
address.uri❌Payment URIs apply to collections, not payouts.
address.alternatives[]❌Payouts target a single network and address.
returnUrlcheckout.returnUrlMoved into the checkout block.
redirectUrlcheckout.redirectUrlMoved into the checkout block. Always points to the BVNK hosted payments page.
transactions[]trackingDetails[]Renamed. One entry per on-chain transaction. Populated as the payout settles.
transactions[].dateCreated
.dateConfirmed
trackingDetails[].createdAt
.confirmedAt
Renamed and reformatted.
transactions[].hashtrackingDetails[].hashUnchanged.
transactions[].amounttrackingDetails[].amountNumbers to strings.
❌trackingDetails[].network
.currency
New fields on each entry.
transactions[].networkFeeCurrency
.networkFeeAmount
❌Only the aggregate fees.networkFee is returned, not per transaction. v.1 reported native gas units per transaction; v.3 reports the wallet currency.
transactions[].risk
.displayRate
.exchangeRate
❌Rates are no longer duplicated per transaction.
transactions[].sources[]❌Payin-only. On payins, these surface as originator.paymentInstrument.addresses[].
refund
refunds[]
❌Replaced by labels: ["REFUND"] and relatedPayments[] on the payout. See Exceptions, refunds, and linked payments.
currencyOptions[]checkout.currencyOptions[]Empty on this flow. The currency is already selected.
❌statusHistory[]New. Array of {status, timestamp} on the payment resource.
❌labels[]
relatedPayments[]
New. Populated when this payout relates to another payment, for example a refund. See Exceptions, refunds, and linked payments.

For worked examples of each flow, see Send stablecoin payouts.

Migrate payins​

On a payin the sides are reversed. Your customer is the originator, who pays, and you are the beneficiary, whose wallet is credited. Everything that described the paying side in v.1, including the pay-in currency, the network, and the compliance party, moves to originator, and walletId moves to beneficiary.

Request: POST /api/v1/pay/summary
{
"walletId": "a:24092328494070:G5i4XZ9:1",
"type": "IN",
"amount": 223.05,
"currency": "EUR",
"expiryMinutes": 20,
"reference": "myUniqueRef333",
"returnUrl": "https://my-shop.com/payment-complete?ref=xyz",
"payInDetails": {
"currency": "ETH",
"network": "ETHEREUM"
},
"customerId": "d063635e-0f83-4e47-a1f3-fc9484df1509",
"complianceDetails": {
"requesterIpAddress": "172.16.254.1",
"partyDetails": [
{
"type": "BENEFICIARY",
"entityType": "COMPANY",
"legalName": "BVNK LTD",
"registrationNumber": "5493001KJTIIGC8Y1R12",
"relationshipType": "SELF_OWNED",
"countryCode": "GB"
}
]
}
}

Request field changes:

v.1 fieldv.3 fieldNotes
walletIdbeneficiary.walletIdMoved to the beneficiary. On a payin, you are the party being credited.
payInDetails.currency
.network
originator.currency
originator.paymentInstrument.network
Moved to the originator. Your customer pays, so their currency belongs on their side.
customerIdoriginator.customerIdMoved to the originator. Your own references become originator.customerReference.
partyDetails[].type: "BENEFICIARY"Removedv.1 labelled the paying customer as BENEFICIARY. v.3 places them under originator, making the type redundant.
currencyOptions[]checkout.currencyOptions[]Optional. Restricts which currencies your customer can pay in. Omit to allow all supported currencies.

Response field changes:

v.1 fieldv.3 fieldNotes
type: "IN"type: "payment:payin"
direction: "IN"
Split into two fields. type is now a resource discriminator; direction carries what v.1's type conveyed.
status: "PENDING"status: "PENDING_CUSTOMER_PAYMENT"v.1's PENDING becomes an explicit state: created, priced, and awaiting customer payment.
address.{address, tag, network, uri}instructions[0].{address, tag, network, uri}Changed from a single object to an array, one entry per accepted network.
address.alternatives[]Additional instructions[] entriesEach accepted network is a flat entry. No primary/alternative distinction.
transactions[]trackingDetails[]Renamed. One entry per on-chain transaction with hash, amount, network, and confirmation time.
transactions[].sources[]originator.paymentInstrument.addresses[]Moved from the transaction onto the originator. An array because a customer may send from multiple addresses, across late or partial payments.
—labels[]New. Values: OVER, UNDER, or LATE when the payment did not arrive as expected. Empty on exact, on-time payments.

For the full request and response details, see Receive stablecoins via Payment Links. For how labels[] replaces the separate overpayment, underpayment, and late-payment webhooks, see Handle exception scenarios.

Consolidate webhooks​

v.1 assigned a unique event name to almost every notable moment, requiring payin integrations to subscribe to detection, confirmation, settlement, lateness, and holds separately. v.3 uses one status-change event per resource. The status field indicates what happened, and labels[] explains any unusual circumstances. Most handlers now branch on these two fields instead of switching on the event name.

v.1 eventv.3 eventNotes
bvnk:payment:crypto:status-change (OUT)payment:v3:payout:status-changeAll payout transitions use one event. Read the progression from statusHistory[].
bvnk:payment:crypto:refund-initiatedpayment:v3:payout:status-changeRefunds are payouts with labels: ["REFUND"] and relatedPayments[] linking to the originating payin.
bvnk:payment:crypto:status-change (IN)
bvnk:payment:crypto:transaction-detected
bvnk:payment:crypto:transaction-confirmed
payment:v3:payin:status-changeDetection, confirmation, and completion are one event at different status values.
bvnk:payment:crypto:transaction-latepayment:v3:payin:status-changeDetected via labels: ["LATE"], not a distinct event. statusHistory[] shows EXPIRED, then PROCESSING, then COMPLETED.
bvnk:payment:crypto:status-change (status UNDERPAID), and the overpayment status changepayment:v3:payin:status-changeDetected via labels: ["UNDER"] or labels: ["OVER"].
bvnk:payment:crypto:transaction-on-holdpayment:v3:payin:status-changeIndicated by status: "HELD", not a distinct event.
bvnk:payment:crypto:conversion-completedpayment:v3:payin:conversion-completedProvisional. May change before release.
bvnk:payment:crypto:transaction-settled—No direct equivalent. Covered by payment:v3:payin:status-change with status: "COMPLETED".

Full event schemas and example payloads live in the Payments v.3 webhooks reference.

Migration checklist​

Follow these steps in order. The first two steps help prevent data inconsistencies. You can complete the remaining steps for each flow individually.

  1. Audit the two breaking changes. Find every place your integration reads amount or actual from a payment response or webhook, and read required.amount or actual.amount instead. The meaning is unchanged, and your request code needs no change. Find every place you write or read customerId and move your own references to customerReference.
  2. Update money and timestamp parsing. Every amount is now a string, and every timestamp is ISO 8601 rather than epoch milliseconds.
  3. Update payout creation. Replace POST /api/v1/pay/summary (type: "OUT") with POST /payment/v3/payouts.
    • Move walletId into originator.walletId, and the destination details into beneficiary.
    • Remove method from the request. Send the destination as beneficiary.paymentInstrument with type: "CRYPTO_ADDRESS", and read it back from beneficiary.paymentInstrument.addresses[] on responses.
    • Choose exactly one amount anchor: originator.amount, beneficiary.amount, or checkout.amount.
    • Flatten complianceDetails.partyDetails[] into beneficiary.entity, or reference a stored contact with beneficiary.contactId.
  4. Replace the estimate flow. There is no estimate resource. Create an incomplete payout to get a quote, PATCH it to add details, and send an ACCEPT_QUOTE action to release it. Use a REFRESH_QUOTE action carrying the current conversion.digest in place of an empty-body estimate update.
  5. Update two-step payouts. Replace "twoStep": "true" with "confirmationRequired": true, and replace PUT /api/v1/pay/{uuid}/confirm/summary with an ACCEPT_QUOTE action on POST /payment/v3/payouts/{id}/action.
  6. Update payin creation. Replace POST /api/v1/pay/summary (type: "IN") with POST /payment/v3/payins. Move walletId to beneficiary.walletId and everything describing the paying customer to originator. Read deposit instructions from instructions[] rather than address and alternatives[].
  7. Consolidate your webhook handlers. Subscribe to payment:v3:payout:status-change and payment:v3:payin:status-change in place of the individual v.1 events, and branch on status and labels[] instead of the event name.
  8. Leave your v.1 integration in place until each flow is cut over. Nothing is being switched off, so v.1 traffic keeps working while you migrate.

Related guides:

Was this page helpful?