Skip to main content
BVNK

Handle underpayments

Beta

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.

Underpayments occur when customers do not transfer enough cryptocurrency to fulfill their payment obligation.

This can occur if the customer is charged a network fee by their exchange platform that exceeds the amount they anticipated from their payment. The customer might also unintentionally send less than intended. For example, they mean to send 0.13275995 ETH but send 0.1 ETH instead. In this situation, the payin still transitions to a COMPLETED status, and BVNK adds "UNDER" to its labels[] array so you can tell it apart from an exact payment.

How can I see the actual amount received?​

To see the actual received amount, on the payment:v3:payin:status-change webhook, or on a GET /payment/v3/payins/{id} response, compare the fields on originator:

  • originator.actual: the currency and amount the customer actually sent.
  • originator.required: the currency and amount BVNK originally asked for.

The same objects also appear on beneficiary and checkout, so you can read the shortfall in whichever currency you settle or display in.

Example payin — underpaid
{
"id": "2B95E4A1-6D77-42C9-B3F8-0A5C81E9D742",
"reference": "myUniqueRef335",
"type": "payment:payin",
"direction": "IN",
"status": "COMPLETED",
"method": "CRYPTO",
"fees": {
"customerFee": null,
"processingFee": { "estimated": { "currency": "EUR", "amount": "0.84" }, "actual": { "currency": "EUR", "amount": "0.84" } },
"networkFee": null
},
"originator": {
"required": { "currency": "ETH", "amount": "0.13275995" },
"actual": { "currency": "ETH", "amount": "0.10000000" },
"customerId": "d063635e-0f83-4e47-a1f3-fc9484df1509",
"customerReference": null,
"requesterIpAddress": "172.16.254.1",
"entity": {
"type": "COMPANY",
"legalName": "BVNK LTD",
"registrationNumber": "5493001KJTIIGC8Y1R12",
"relationshipType": "SELF_OWNED",
"address": { "country": "GB" }
},
"paymentInstrument": {
"type": "CRYPTO_ADDRESS",
"network": "ETHEREUM",
"addresses": ["0x3f5ce5fbfe3e9af3971dd833d26ba9b5c936f0be"],
"tag": null
}
},
"beneficiary": {
"required": { "currency": "EUR", "amount": "223.05" },
"actual": { "currency": "EUR", "amount": "168.01" },
"walletId": "a:24092328494070:G5i4XZ9:1"
},
"conversion": {
"type": "QUOTE",
"baseRate": "1688.54",
"allInRate": "1680.1",
"spread": { "percentage": "0.005", "amount": "0.84", "currency": "EUR" },
"validityDuration": "PT60M",
"expiresAt": "2026-03-14T21:51:02Z"
},
"instructions": [
{
"type": "CRYPTO",
"network": "ETHEREUM",
"currency": "ETH",
"amount": "0.13275995",
"address": "0x8f2b06d1c493a75e0b18d4f6a29c53081e7b6d40",
"tag": null,
"uri": "ethereum:0x8f2b06d1c493a75e0b18d4f6a29c53081e7b6d40?value=1.3276e17"
}
],
"checkout": {
"required": { "currency": "EUR", "amount": "223.05" },
"actual": { "currency": "EUR", "amount": "168.01" },
"displayRate": { "from": "ETH", "to": "EUR", "rate": "1680.1" },
"redirectUrl": "https://pay.bvnk.com/payin/2B95E4A1-6D77-42C9-B3F8-0A5C81E9D742",
"returnUrl": "https://my-shop.com/payment-complete?ref=xyz"
},
"trackingDetails": [
{
"amount": "0.10000000",
"currency": "ETH",
"hash": "0x9d40c8b3e17f2a65904d1e8b3f7c2a60518d9e4b7f0a2c6d3e91b5f8a4c07d26",
"network": "ETHEREUM",
"createdAt": "2026-03-14T20:56:49Z",
"confirmedAt": "2026-03-14T20:58:14Z"
}
],
"labels": ["UNDER"],
"relatedPayments": [],
"statusHistory": [
{ "status": "PENDING_CUSTOMER_PAYMENT", "timestamp": "2026-03-14T20:51:02Z" },
{ "status": "PROCESSING", "timestamp": "2026-03-14T20:56:49Z" },
{ "status": "COMPLETED", "timestamp": "2026-03-14T20:58:14Z" }
],
"metadata": {},
"expiryDuration": "PT20M",
"expiresAt": "2026-03-14T21:11:02Z",
"updatedAt": "2026-03-14T20:58:14Z",
"createdAt": "2026-03-14T20:51:02Z"
}

BVNK asked for 0.13275995 ETH, worth 223.05 EUR at the quoted rate, but the customer sent only 0.1 ETH, worth 168.01 EUR: a shortfall of 0.03275995 ETH (55.04 EUR). The payin still reaches COMPLETED, tagged labels: ["UNDER"], with relatedPayments[] left empty here because this merchant is configured to settle whatever arrives rather than auto-refund it. Compare this to the overpayment example in Handle overpayments, where the same shape is used for the opposite case.

Handle underpayments​

To ensure the best customer experience when managing underpayments, it's advisable to adjust the customer's final payment to reflect the lower total amount received. This approach prevents the need to return the entire sum of crypto, which may be slightly less than expected due to fees or rounding errors, and instead credits the revised amount to the customer's account.

The sent amount is indicated in the originator.actual object referenced in the preceding section, which can then be used to modify the customer's total payment amount in your system, aligning with the originator.required object.

There is the Settle Merchant Wallet setting, where the total sent amount will be deposited into the MID wallet that was linked to the transaction.

In certain circumstances, when it is essential that the full amount is received, and an underpayment does not fulfill this need, the following alternative options can be used:

  • Handle manually: will retain the payment in its originally sent cryptocurrency and deposit it into an equivalent cryptocurrency wallet on your account. No funds are sent to the MID-linked wallet. You can then use the payout functionality to manually return the amount to the customer if required, or manually convert to the MID-linked wallet at a new FX spot rate.

  • Auto refund: will generate an automatic payout for the full underpaid amount, keeping it separate from any account wallets. The payout carries labels: ["REFUND"] and relatedPayments[] links it back to the underpaid payin. The payout resolves to a unique URL, which must be sent to the customer for final confirmation of the address where the returned funds should be sent.

    Example: the customer must pay 223.05 EUR but sends only 0.1 ETH (168.01 EUR) instead of the required 0.13275995 ETH. You decide to return the full 0.1 ETH received, stating that the amount is incorrect. BVNK creates a payout carrying labels: ["REFUND"], and the link to claim it is automatically sent to the customer. The link remains active for three months (expiryDuration: "P3M"). If the customer doesn't claim the funds within this period, the funds are settled into the merchant's account.

    Example payout — auto-refund for an underpayment
    {
    "id": "a17c5e92-6f34-4b81-9d02-1e7c4a8f3b56",
    "reference": "myUniqueRef335-refund",
    "type": "payment:payout",
    "status": "PENDING_VERIFICATION",
    "direction": "OUT",
    "method": "CRYPTO",
    "fees": {
    "customerFee": null,
    "processingFee": null,
    "networkFee": null
    },
    "originator": {
    "required": { "currency": "ETH", "amount": "0.10000000" },
    "actual": { "currency": "ETH", "amount": "0.00" },
    "walletId": "a:24092328494070:K2mPfLt:1"
    },
    "beneficiary": {
    "required": { "currency": "ETH", "amount": "0.10000000" },
    "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.10000000" },
    "actual": { "currency": "ETH", "amount": "0.00" },
    "redirectUrl": "https://pay.bvnk.com/refund/a17c5e92-6f34-4b81-9d02-1e7c4a8f3b56",
    "returnUrl": null
    },
    "instructions": [],
    "trackingDetails": [],
    "labels": ["REFUND"],
    "relatedPayments": [
    { "type": "payment:payin", "paymentId": "2B95E4A1-6D77-42C9-B3F8-0A5C81E9D742" }
    ],
    "statusHistory": [
    { "status": "PENDING_VERIFICATION", "timestamp": "2026-03-14T20:58:20Z" }
    ],
    "flow": "default",
    "metadata": {},
    "expiryDuration": "P3M",
    "expiresAt": "2026-06-14T20:58:20Z",
    "updatedAt": "2026-03-14T20:58:20Z",
    "createdAt": "2026-03-14T20:58:20Z"
    }

    Notice the payout's relatedPayments[] points back at 2B95E4A1-6D77-42C9-B3F8-0A5C81E9D742, the underpaid payin from the example above, and its amount matches the full 0.1 ETH the customer actually sent rather than the shortfall. Unlike an overpayment refund, an underpayment refund always returns everything received, since the merchant is refusing the partial amount rather than returning an excess.

  • Lower threshold: you can also set a lower threshold to define an acceptable underpayment variance margin. When it is active, any underpayment that falls within your specified buffer is treated as an exact payment: the payin completes with no UNDER label at all, overriding any Auto refund or Handle manually configurations. If an underpayment exceeds this allowed threshold, the buffer is completely disregarded. In this case, the system reverts to your standard underpayment settings, and the payin completes with labels: ["UNDER"] to handle the full amount received.

    Reach out to your account manager to enable this feature.

    Example: the customer must pay 100 USDT, underpayment refunds are enabled, and the Lower Threshold is set to 5 USDT. If the customer pays 97 USDT, the payin completes with an empty labels[], because the underpayment falls within the accepted threshold buffer. If the customer pays 90 USDT instead, the underpayment exceeds the threshold margin: the payin completes with labels: ["UNDER"], and the system triggers your default underpayment configuration, generating an automatic refund payout for the full 90 USDT.

Underpayment exception flows​

The summary of the flow for four different outcomes is given below:

ConfigurationBalance impactPayin statusLabels
Settle Merchant WalletReceived amount is credited to your MID wallet in your wallet currency. For more information on a MID wallet, refer to the merchant setup guide.COMPLETED["UNDER"]
Handle ManuallyReceived amount is deposited into the merchant cryptocurrency wallet.COMPLETED["UNDER"]
Auto RefundFunctions similarly to Handle Manually, except it is done automatically for you. The total amount of cryptocurrency received is attributed to a new refund payout, auto-created and linked back via relatedPayments[].Payin COMPLETED; refund payout PENDING_VERIFICATIONPayin ["UNDER"]; payout ["REFUND"]
Lower ThresholdThe received amount is automatically accepted and processed as a successful transaction if it falls within your defined buffer, overriding standard underpayment configurations.COMPLETED[] (no exception recorded)
note

To change your configuration or learn more, contact your Account or Integration Manager.

Was this page helpful?