Refund payments
The Payments v.3 API endpoints are in beta and may change as we enhance the product. See Migrate crypto payments from v.1 for background on this project.
In the case where a payment exception scenario has taken place, BVNK provides the option to automatically refund the cryptocurrency for merchants who do not want to hold these cryptocurrencies, due to any compliance issues, or for merchants that do not want to internally process any payments that have fallen outside of the agreed upon payment rules.
Auto Refund does not automatically return the funds to the sender's address.
Instead, it generates a payout and sends a webhook. This payout's checkout.redirectUrl should be forwarded to the customer, allowing them to provide their address and accept the refund.
How a refund works
Refunds function as an automatically generated payout, which needs to be sent to the customer to complete. In this case, the customer needs to enter their desired wallet address to which they want the funds sent and confirm the payout.
Contact your Merchant Support or Integration Manager to have the refund options enabled.
Refund exception scenarios
As stated on the previous pages, the three payment exception scenarios to consider are:
- Overpayments
- Underpayments
- Late payments
All three resolve the payin to the same terminal status, COMPLETED, with the exception recorded in labels[] instead. This matters for reconciliation: check labels[], not status, to detect an overpayment, underpayment, or late arrival on an otherwise-ordinary COMPLETED payin.
- Overpayments
- Underpayments
- Late payments
When an overpayment has occurred, the payin's status is updated from PROCESSING to COMPLETED, tagged labels: ["OVER"]. This should be seen as a standard successful payment on the merchant's side.
The original payment amount requested is credited to the merchant's wallet as usual. Still, the additional amount of overpaid cryptocurrency is deposited into a holding account, ensuring no impact on the merchant's wallet balance.
This additional amount is then the total amount that will be returned to the sender as an automatically generated payout, carrying labels: ["REFUND"] with relatedPayments[] linking back to the overpaid payin.
Refund flow
The ideal flow of an automatically generated refund is given as follows:
- The merchant creates the payin for a customer.
- The merchant forwards the payment link returned from BVNK to the customer.
- The customer completes the payment by sending funds to the given address.
- The total funds amount is too low, too high, or received late, triggering the appropriate refund.
- BVNK sends a
payment:v3:payout:status-changewebhook to the merchant, withlabels: ["REFUND"]andcheckout.redirectUrlset to the URL where the refund can be claimed. - The merchant notifies the customer about the URL.
- The customer navigates to the URL and enters their wallet address.
- Once BVNK receives this address, the displayed amount of cryptocurrency is sent through, less the fees.
- The merchant receives the same webhook again, with a new
statusHistory[]entry showing the refund payout'sstatusmove toCOMPLETED. - The process is complete.
Expired refund
If the customer never enters their wallet address and confirms the refund within the expiry limit:
- The refund payout becomes
EXPIRED. The default expiry limit for a refund is set to three months (expiryDuration: "P3M"). - When the limit is reached, the total refund amount is transferred from the held account to the merchant's MID wallet.
- The same
payment:v3:payout:status-changewebhook fires again, with astatus: "EXPIRED"entry appended tostatusHistory[], to indicate that the refund has expired and the amount has been credited to the appropriate wallet.
Refund webhook
BVNK automatically initiates the refund process, which in effect acts as a payout. A refund is reported on the ordinary payment:v3:payout:status-change webhook, the same event every other payout uses. Three things identify it as a refund rather than an everyday payout:
labels: contains"REFUND".relatedPayments[]: contains an entry withtype: "payment:payin"and thepaymentIdof the payin it corrects.checkout.redirectUrl: the URL to send to the customer, so they can enter their address and claim the refund.checkout.actual, like every otheractual, has an amount of0.00until the refund payout reachesCOMPLETED.
status moves through the same payout state machine as any other payout: PENDING_VERIFICATION while the refund waits for the customer to submit an address, PROCESSING once they have and the funds are sending, then COMPLETED once they land, or EXPIRED if the three-month claim window passes unclaimed.
{
"event": "payment:v3:payout:status-change",
"eventId": "019e25af-4239-7000-8000-b03e7a5c9142",
"timestamp": "2026-03-14T20:52:48Z",
"data": {
"id": "d4f81c66-3a29-4b7e-9c05-8e2f1a7b6d30",
"reference": "myUniqueRef334-refund",
"type": "payment:payout",
"status": "PENDING_VERIFICATION",
"direction": "OUT",
"method": "CRYPTO",
"fees": {
"customerFee": null,
"processingFee": null,
"networkFee": null
},
"originator": {
"required": { "currency": "ETH", "amount": "0.01224005" },
"actual": { "currency": "ETH", "amount": "0.00" },
"walletId": "a:24092328494070:K2mPfLt:1"
},
"beneficiary": {
"required": { "currency": "ETH", "amount": "0.01224005" },
"actual": { "currency": "ETH", "amount": "0.00" },
"customerId": "d063635e-0f83-4e47-a1f3-fc9484df1509",
"customerReference": null,
"entity": {
"type": "COMPANY",
"legalName": "BVNK LTD",
"registrationNumber": "5493001KJTIIGC8Y1R12",
"relationshipType": "SELF_OWNED",
"address": { "country": "GB" }
},
"paymentInstrument": { "type": "CRYPTO_ADDRESS", "network": null, "addresses": [], "tag": null }
},
"conversion": null,
"checkout": {
"required": { "currency": "ETH", "amount": "0.01224005" },
"actual": { "currency": "ETH", "amount": "0.00" },
"redirectUrl": "https://pay.bvnk.com/refund/d4f81c66-3a29-4b7e-9c05-8e2f1a7b6d30",
"returnUrl": null
},
"instructions": [],
"trackingDetails": [],
"labels": ["REFUND"],
"relatedPayments": [
{ "type": "payment:payin", "paymentId": "7C41D2E8-9B03-4A17-8E52-1D6F3A0B7C94" }
],
"statusHistory": [
{ "status": "PENDING_VERIFICATION", "timestamp": "2026-03-14T20:52:48Z" }
],
"flow": "default",
"metadata": {},
"expiryDuration": "P3M",
"expiresAt": "2026-06-14T20:52:48Z",
"updatedAt": "2026-03-14T20:52:48Z",
"createdAt": "2026-03-14T20:52:48Z"
}
}
This is the refund created for the overpayment worked through in Handle overpayments: the customer sent 0.145 ETH against a required 0.13275995 ETH, and this payout returns exactly the 0.01224005 ETH difference. relatedPayments[] points back at that payin, 7C41D2E8-9B03-4A17-8E52-1D6F3A0B7C94. The same shape, with the amount and relatedPayments[] target changed, covers the underpayment and late-payment refunds worked through in Handle underpayments and Handle late payments.
If the refund goes unclaimed, the same event fires again with a second statusHistory[] entry:
[
{ "status": "PENDING_VERIFICATION", "timestamp": "2026-03-14T20:52:48Z" },
{ "status": "EXPIRED", "timestamp": "2026-06-14T20:52:48Z" }
]
The wallet debited for the refund is identified by originator.walletId, the same field you'd read on any other payout.
For more details, see the payment:v3:payout:status-change webhook reference.