This page describes a feature we are building. It is not available in a released SDK or API version. Read it to plan an integration, and confirm availability with your Handpoint contact before you build against it.
Fee Mitigation
A merchant pays a cost to accept a card. Fee mitigation lets the merchant move part of that cost to the customer, or reward a customer who pays another way.
Handpoint supports four programs. A transaction carries one program. It never carries two.
The four programs
| Program | What the customer sees | Tenders |
|---|---|---|
| Surcharge | The price, plus a separate fee for the use of a credit card | Credit cards only |
| Dual pricing | Two prices: a cash price, and a higher card price | Every card |
| Cash discount | The card price, and a lower price if they pay cash | Cash and e-gift only |
| Admin fee | The price, plus a fee that applies to every payment method | Every tender |
Why the difference matters
The difference between these programs is legal, not arithmetic. Two programs can charge the same money and follow different rules.
- A surcharge is a fee. The card networks regulate it, and several US states prohibit it.
- Dual pricing is a price, not a fee. The merchant posts two prices, so the card price is simply the price. This is why dual pricing avoids the restrictions that apply to a surcharge.
- A cash discount reduces the price when the customer pays cash. The card price is the standard price.
- An admin fee applies to every payment method. It is deliberately universal. A carve-out for cash would turn it into a cash discount, and the rules would change.
Who calculates the fee
You calculate the fee. The gateway decides whether it applies.
Only your application knows the merchant configuration, the tax rate, and the rounding you printed on the customer's screen. So you send the amount you calculated, and the gateway never recalculates it.
The gateway then makes one decision: does this program apply to this card? If it does not, the gateway removes the fee and tells you why.
Your application The gateway
───────────────── ───────────
calculate the fee ──▶ check the card type
apply, or drop and explain
show the result ◀── return the outcome
What the platform guarantees
- The fee is calculated before the authorization.
- The final transaction amount includes the fee.
- A debit card never carries a surcharge.
- The merchant configures the rules, and the platform enforces them.
- The customer sees the final price before they confirm the payment.
Supported integration paths
| Operation | Cloud API | Android (PAX) | Android (HiLite) | iOS (HiLite) | Cordova |
|---|---|---|---|---|---|
| Sale | 🕓 | ✅ | ❌ | ❌ | ❌ |
| Sale with tip | 🕓 | ✅ | ❌ | ❌ | ❌ |
| Sale with tokenization | 🕓 | ✅ | ❌ | ❌ | ❌ |
| Remote Sale | 🕓 | ✅ | ❌ | ❌ | ❌ |
| Pre-Authorization, capture | 🕓 | ✅ | ❌ | ❌ | ❌ |
| Every other operation | ❌ | ❌ | ❌ | ❌ | ❌ |
✅ available · 🕓 planned · ❌ not supported
The Android SDK on a PAX terminal carries the feature first. The Cloud API needs one more change
before it can forward the fee object; see the Code section.
Eligibility
Two facts decide the outcome: the type of the card, and the type of the transaction.
Cards
| Program | Cards |
|---|---|
| Surcharge | Credit only. Never debit, never prepaid |
| Dual pricing | Every card |
| Admin fee | Every card |
| Cash discount | None. The gateway refuses it |
The gateway refuses a cash discount on a card transaction. A card transaction under a cash discount program carries no fee, because the card price is the posted price. Such a request cannot be correct, so the gateway rejects it rather than accepting it quietly.
The gateway applies the debit rule to every program in the current build, and it does not yet
refuse a cash discount. Treat the table above as the target behaviour, and read
reason on every result.
Dropped and refused are different
| Outcome | What happens | Example |
|---|---|---|
| Dropped | The transaction continues. The gateway removes the fee, and the result says why | A surcharge on a debit card |
| Refused | The transaction never reaches the acquirer. The gateway returns an error | A fee that conflicts with surchargeAmount |
Always read the result. A dropped fee means the customer paid less than you asked for, and your receipt and your records must show the amount the gateway actually charged.
Transaction types
| Transaction type | Surcharge | Dual pricing | Admin fee | Cash discount |
|---|---|---|---|---|
| Sale | ✅ | ✅ | ✅ | ❌ |
| Sale with tip | ✅ | ✅ | ✅ | ❌ |
| Sale with tokenization | ✅ | ✅ | ✅ | ❌ |
| Remote Sale | ✅ | ✅ | ✅ | ❌ |
| Pre-Authorization, create | ❌ | ❌ | ❌ | ❌ |
| Pre-Authorization, capture | ✅ | ❌ | ✅ | ❌ |
| Remote Pre-Authorization, capture | ✅ | ❌ | ✅ | ❌ |
| Refund | ❌ | ❌ | ❌ | ❌ |
| Remote Refund | ❌ | ❌ | ❌ | ❌ |
| Reversal | Automatic | Automatic | Automatic | ❌ |
| Capture reversal | Automatic | Automatic | Automatic | ❌ |
| Tip adjustment | ❌ | ❌ | ❌ | ❌ |
| Tokenization | ❌ | ❌ | ❌ | ❌ |
| Batch operations | ❌ | ❌ | ❌ | ❌ |
Four rules explain every ❌ in that table.
A pre-authorization carries no fee. The fee belongs to the capture, because the capture sets the amount the customer pays. If you set a fee on a pre-authorization, the SDK removes it. Set it on the capture instead.
Dual pricing needs a cash price. A pre-authorization has no cash alternative to price against, so dual pricing applies to a sale and to a Remote Sale only.
A refund carries no fee. The card networks require a merchant to return the fee in proportion to the refund. The platform does not calculate that proportion, so you calculate it. Read the note below.
An operation with no amount carries no fee. Tokenization, a tip adjustment and every batch operation set no transaction amount, so no fee can apply to them.
A reversal is automatic. The reversal carries the same fee as the original transaction, because the acquirer requires the two values to match. You set nothing.
RefundOptions accepts a fee, because it inherits the field from the base options type. The gateway
ignores it.
Calculate the returned fee in your own application, and add it to the refund amount you send. A refund that does not return the fee in proportion breaks card network rules, and the merchant carries that risk.
Code
- Cloud API
- Android (PAX)
- Android (HiLite)
- iOS (HiLite)
- Cordova
This feature is implemented in the gateway but not yet publicly released for this integration path. Contact your Handpoint integration engineer for availability.
The terminal already accepts a nested fee object on a cloud request. The Cloud API does not send
one yet: POST /transactions carries the deprecated flat surchargeAmount only. When the Cloud API
adds fee, the shape matches the object below.
{
"operation": "sale",
"amount": "10000",
"currency": "USD",
"terminal_type": "PAXA920",
"serial_number": "082104578",
"fee": {
"amount": "360",
"mitigationProgram": "surcharge",
"taxOnFee": "60"
}
}
Amounts inside fee follow the rest of POST /transactions: minor units, as a string.
Build a Fee, set it on the options, and start the operation. Pass the base amount to
hapi.sale(). The SDK adds fee.amount to it, so never add the fee yourself.
val fee = Fee(
amount = BigInteger("360"), // 3.60
mitigationProgram = FeeMitigationProgram.SURCHARGE,
taxOnFee = BigInteger("60") // 0.60, already inside amount
)
val options = SaleOptions()
options.fee = fee
hapi.sale(BigInteger("10000"), Currency.USD, options) // 100.00
override fun endOfTransaction(result: TransactionResult, device: Device) {
if (result.finStatus == FinancialStatus.AUTHORISED) {
val feeResult = result.fee ?: return
if (feeResult.applied) {
// print a fee line, settle result.totalAmount
} else {
// no fee line — read feeResult.reason
}
}
}
A Remote Sale uses the same object on MoToOptions:
val options = MoToOptions()
options.fee = Fee(BigInteger("360"), FeeMitigationProgram.SURCHARGE, BigInteger("60"))
hapi.moToSale(BigInteger("10000"), Currency.USD, options)
surchargeAmount still worksOptions.surchargeAmount and TransactionResult.surcharge are deprecated, and they keep working.
The SDK reads surchargeAmount as Fee(amount, SURCHARGE) when you set no fee, and it fills
TransactionResult.surcharge as well as TransactionResult.fee for a surcharge. Set one field or
the other. If you set both, the two amounts must match, and the program must be SURCHARGE.
A HiLite card reader does not carry the fee mitigation feature.
Use instead: A PAX terminal with the Android SDK.
The iOS SDK does not carry the fee mitigation feature.
Use instead: A PAX terminal with the Android SDK.
The Cordova plugin does not carry the fee mitigation feature.
Use instead: A PAX terminal with the Android SDK.
Fee fields
The object you send.
| Name | Type | Required | Description |
|---|---|---|---|
amount | integer, minor units | Yes | The whole fee, including any tax inside it. The SDK adds it to the amount of the operation. Never negative |
mitigationProgram | enum | Yes | SURCHARGE, ADMIN_FEE, CASH_DISCOUNT or DUAL_PRICING. On the wire: surcharge, adminFee, cashDiscount, dualPricing |
taxOnFee | integer, minor units | No | The part of amount that is tax. Never added on top. Never negative. Absent reads as zero |
Three rules apply before the transaction starts.
amountandtaxOnFeemust not be negative.- If you set
feeand the deprecatedsurchargeAmounttogether, the two amounts must match, andmitigationProgrammust beSURCHARGE. taxOnFeemust not exceedtaxInformation.taxAmount, when you settaxInformation.
What comes back
Every transaction that carried a fee returns a fee result. A transaction that carried none returns nothing, not a zero.
| Name | Type | Required | Description |
|---|---|---|---|
amount | decimal, major units | Yes | The fee the gateway recorded. When applied is false, this is what you asked for, not what the customer paid |
mitigationProgram | enum | No | The program the gateway applied the fee under. Empty when the gateway reports a program the SDK does not recognise |
taxOnFee | decimal, major units | Yes | The part of amount that is tax |
applied | boolean | Yes | Whether the gateway charged the fee |
reason | enum | Yes | Why the gateway applied or dropped the fee |
reasonDetail | string | No | The raw text from the gateway. Set when reason is UNKNOWN |
The amount you send and the amount you read use different units. You send minor units. You read major units, the same as every other amount on the result.
Why the fee was applied or dropped
| Name | Meaning | What you do |
|---|---|---|
APPLIED | The gateway charged the fee | Print the fee line. Settle the total that the result carries |
NOT_ELIGIBLE_DEBIT | The customer presented a debit card, and the program does not allow one | Print no fee line. Tell the customer the fee was removed |
NOT_ELIGIBLE_PREPAID | The customer presented a prepaid card, and the program does not allow one | Print no fee line. Tell the customer the fee was removed |
PROGRAM_NOT_ENABLED | The merchant is not configured for this program | Print no fee line. Ask the merchant to enable the program |
PROGRAM_NOT_SUPPORTED | The gateway does not carry this program | Print no fee line. Handle the program in your own application |
UNKNOWN | The gateway returned a reason this version of the SDK does not recognise | Read reasonDetail. Treat the fee as dropped unless applied is true |
The current build returns APPLIED or NOT_ELIGIBLE_DEBIT. Handle all six values, because the
other four arrive with the per-program rules and need no change on your side.
Errors
The gateway refuses these requests before the authorization. The transaction never reaches the acquirer.
| Code | Meaning | Recovery |
|---|---|---|
4258 SURCHARGE_NOT_ENABLED | The merchant is not configured to surcharge, and the request carries a surcharge | Ask the merchant to enable surcharging, or send no fee |
4268 FEE_SURCHARGE_AMOUNT_CONFLICT | The request carries both fee and the deprecated surchargeAmount, and they disagree | Send one field or the other. If you send both, match the amounts and set mitigationProgram to surcharge |
4269 FEE_TAX_ON_FEE_EXCEEDS_L2_TAX | fee.taxOnFee is greater than taxInformation.taxAmount | Lower taxOnFee, or raise the tax you declare |
FIELD_REQUIRED | fee.amount or fee.mitigationProgram is missing | Send both fields. Neither one is optional |
AMOUNT_FORMAT | fee.amount or fee.taxOnFee is malformed or negative | Send a positive amount in the documented format |
MITIGATION_PROGRAM_INVALID | fee.mitigationProgram is not one of the four values | Send surcharge, adminFee, cashDiscount or dualPricing |
The gateway refuses a request that carries two different fee amounts. It never picks one for you.
Migrate a call site completely, in one change, from surchargeAmount to fee.
The Android SDK checks the same three rules before it sends, and it raises a verification error instead of a transaction. See Error handling.
Tax on the fee
A merchant can calculate the fee before the tax, or after it. Both orders reach the same total. They do not reach the same tax.
taxOnFee names the part of fee.amount that is tax. It sits inside the fee. The platform
never adds it on top of the transaction.
Example. The base is 100.00. The tax is 20%, so the tax on the base is 20.00. The merchant surcharges 3%, and the merchant configuration applies the surcharge to the tax as well.
| Value | Amount | What you send |
|---|---|---|
| Base | 100.00 | The amount of the operation, 10000 |
| Fee on 120.00 at 3% | 3.60 | fee.amount, 360 |
| The tax part inside that fee | 0.60 | fee.taxOnFee, 60 |
| What the customer pays | 123.60 | The SDK adds the base and the fee |
You send the part of the tax that belongs to the fee, because only you know how you calculated it. When the fee applies, that figure changes nothing.
When the gateway drops a fee, it removes the fee. The matching reduction of the declared tax is
still in development. Until it ships, reconcile the tax yourself when applied is false.
What reaches the card networks
Only a surcharge. The acquirer protocol has one field for a fee, and its specification calls that field the surcharge. It has no representation for the other three programs.
An admin fee and a dual price travel inside the transaction amount, and nothing else. Reporting either one as a surcharge would misstate a fee to the card networks.
The receipt
The receipt is the only record a customer keeps, and the only one a regulator inspects. Each program has its own rule.
| Program | The receipt shows |
|---|---|
| Surcharge | A separate line, clearly labelled. The card networks require it. Never inside the total, never mixed with the tax |
| Admin fee | A separate line, with its own label |
| Dual pricing | No fee line. The card price is the posted price |
| Cash discount | Nothing. It is not a card transaction |
This looks like an omission. It is not. An itemised fee would present a price as a fee, and would undermine the model that makes dual pricing lawful. Do not add a fee line to a dual pricing receipt.
See Receipt Compliance for the fields that every Handpoint receipt carries.
Compliance for a surcharge
Neither the SDK nor the terminal can discharge them. Confirm them with the merchant before you enable a surcharge.
- The merchant tells the cardholder before the transaction. Signage at the entrance, and at the point of sale.
- The merchant gives the card networks and the acquirer 30 days notice before the first surcharge.
- The maximum is 3% of the transaction, or the actual cost of acceptance, whichever is lower.
- A debit card or a prepaid card never carries a surcharge, even when the customer selects "credit" at the terminal.
- Several US states and Puerto Rico prohibit a surcharge.
Reporting
Every fee is recorded, so a merchant can answer these questions:
- How much did I collect in fees this month, and under which program?
- On this transaction, did the platform charge a fee? If it did not, why not?
- Which of my terminals run which program?
- Can I export all of it?
Four values reach the transaction list and the exported report: the fee amount, the program, whether the fee applied, and the tax attributed to the fee.
A transaction with no fee shows empty values, never 0.00. A fee of zero and no fee at all are
different facts.
The four values are not yet in the merchant reports or in the Transaction Feed API. Persist the fee result in your own records until they arrive.
Scope
Supported:
- Card transactions.
- Surcharge, admin fee and dual pricing.
- The Android SDK on PAX terminals.
Not supported:
| Item | Reason |
|---|---|
| Cash and e-gift tenders | The gateway processes card transactions. Your point of sale handles cash |
| Cash discount at the gateway | It applies to a cash tender, which never reaches the gateway |
| The Cloud API | The terminal accepts a fee object, but the Cloud API does not send one yet |
| The iOS, Windows and Cordova SDKs | The Android SDK carries the feature |
| A recalculation of the fee on a partial refund | The platform does not calculate it. You do |
| Routing that depends on the fee | Out of scope |
| Rules for each BIN or each card brand | Out of scope |
Terms
| Term | Meaning |
|---|---|
| Fee | The money the merchant adds, under any of the four programs |
| Program | Which of the four rules applies |
| Applied | The gateway charged the fee |
| Dropped | The gateway removed the fee, and the transaction continued |
| Refused | The gateway rejected the request, and the transaction stopped |
Related pages
- Transaction result object — every field the result carries
- Error codes — every code the platform returns
- Receipt Compliance — what a receipt must show
- Glossary — the vocabulary of the platform