Cash and card prices over the API
What an integration sends and reads back for a merchant that posts a cash price and a card price, the card price amount the gateway computes, how to tell which price a payment was charged, what a card-present terminal prints, the merchant configuration block, and the errors to handle.
This guide is for developers integrating with a merchant that posts two prices: a cash price, and a card price that's the cash price plus a configured rate or a configured fixed amount. It covers what to send on a payment, what the gateway adds to it, how to tell which price a payment was charged, the merchant configuration block, and the errors to handle.
Dual pricing is off by default. It's turned on for an account on request, and then for each merchant by the merchant's reseller, after the acquirer and legal review described in the Dual pricing guide in the help center. While a merchant isn't enrolled, nothing in this guide changes its new payments, and they read back with these fields as null. A payment made while the merchant was enrolled keeps its values, and so do the voids, captures, and refunds that follow it.
Send the cash price, and let the gateway add the card price
The amount you send is the cash price. When the payment is a card, WinkPG computes the difference between the two prices from the merchant's rate or fixed amount, adds it to the transaction as invoiceData.amounts.cardPriceAdjustment, and authorizes the card price. You don't need to know the rate or the amount, and nothing on the request asks for the tender's price.
curl -X POST "https://your-gateway-host/api/transactions" \
-H "api-key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"transactionType": "Sale",
"idempotencyKey": "order-0001",
"cardData": {
"cardNumber": "4111111111111111",
"nameOnCard": "Jane Doe",
"expirationMonth": 12,
"expirationYear": 2030
},
"invoiceData": {
"amounts": { "base": 100.00, "total": 100.00 }
}
}'
For a merchant enrolled at 4 percent, the response, and every later GET /api/transactions/{id}, carries the card price. An abridged response:
{
"id": "6f3b2c18-0a4d-4a9e-9d4f-2b71c2f0a911",
"transactionType": "Sale",
"invoiceData": {
"amounts": {
"base": 100.00,
"cardPriceAdjustment": 4.00,
"dualPricingAssessed": true,
"total": 104.00
}
},
"cardPriceAdjustmentReversedAmount": null
}
The fields to read:
| Field | What it carries |
|---|---|
invoiceData.amounts.base |
The cash price, as you sent it. Never rewritten. |
invoiceData.amounts.cardPriceAdjustment |
The difference between the card price and the cash price. Counted in total only when dualPricingAssessed is true. |
invoiceData.amounts.dualPricingAssessed |
Set by the gateway when it charged the card price. A value you send is discarded. |
invoiceData.amounts.total |
What was authorized: the card price plus tax, tip, and shipping on a card, the cash price plus those on anything else. |
The difference is the card price, not a fee, so it never appears in a processor fee field. The processor receives total as the transaction amount. Level III line items still sum to base.
The rate applies to base, plus shipping when the merchant elects it. Tax and tips are never in it. The amount is rounded to the cent, half away from zero. A merchant configured with a fixed amount instead is charged that amount on every card payment whose base is above zero, whatever its size: at a fixed 2.50, a 100.00 card sale carries a cardPriceAdjustment of 2.50. When the gateway calculates the tax, the card price is the taxable sale price, so a card's tax includes its share of the difference. A tax amount you send is never recalculated.
Which price each payment is charged
| Payment | Price charged |
|---|---|
| Credit, debit, or prepaid card, a stored payment method, or a payment token | Card price |
| A digital wallet, while the merchant's program covers wallets | Card price |
| A request with no payment method (a cash sale) | Cash price, with the discount recorded |
| A bank payment | Cash price, with the discount recorded |
An EBT SNAP, EBT Cash, or eWIC tender (cardData.cardTenderType) |
Cash price, always |
| A payment method the gateway can't classify | The amount sent |
| A merchant whose program doesn't cover the API channel | The amount sent |
A card read by a chip, contactless, or magnetic stripe reader at a point of sale, while the merchant's program covers Terminal |
Card price |
A card read by a chip, contactless, or magnetic stripe reader at a point of sale, while the merchant's program doesn't cover Terminal |
The amount sent, which is the cash price, even when the program covers the API channel |
A merchant-initiated payment with mitReason Recurring or Installment, or cardholderPresence Recurring |
Decided as a recurring payment, whatever the channel: the cash price unless the merchant's program covers recurring billing and the stored-credential consent stated the card price |
| An additional payment on a split tender, while the merchant's program covers split tender | Card price on a card for the amount sent; cash price on cash or a bank payment. See Split tender payments |
| An additional payment on a split tender, while the merchant's program doesn't cover split tender | The amount sent |
| A void, capture, adjustment, or linked refund | The original payment's prices, unchanged |
| A return | The amount returned. A return never carries a card price amount |
| A request through the version 1 API | The amount sent |
A repeated or retried payment starts from the cash price and is priced again at the rate or fixed amount in force when it runs. A repeat or retry of a recurring payment is priced as a recurring payment, at no more than the card price its consent stated.
A recurring payment is charged the card price only under a consent that stated it: one captured when a cardholder enrolls on a hosted payment page, one the merchant records in WinkPG with the card price the gateway stated, or one you send through the API with enrollmentTerms.cardPriceDisclosed set to true.
Telling the gateway you showed the card price. When you store a card with a stored-credential consent (storedCredentialConsent) that names Recurring or Installment, set enrollmentTerms.cardPriceDisclosed to true to confirm that the cardholder saw the card price of each payment before agreeing. Send the cash price of each payment as enrollmentTerms.fixedAmount; it's required, and must be greater than zero, whenever cardPriceDisclosed is true, on any consent. The gateway works out the card price from the merchant's current rate or fixed amount and records it on the consent. It never takes a rate or an amount from your request.
- Leave
enrollmentTerms.cardPriceout and the gateway records its own statement of the card price, such as "$260.00 per payment, against a cash price of $250.00" for 250.00 at 4 percent. - Send
enrollmentTerms.cardPriceonly as that statement, exactly. Any other text is kept as the record of what you showed, and no card price is recorded, so the payments under the consent are charged the cash price. - Set
cardPriceDisclosedtofalse, or leave it out, and no card price is recorded. No card price is recorded either on a bank account consent, on a consent that names neitherRecurringnorInstallment, or for a merchant whose program doesn't cover recurring billing, althoughfixedAmountis still checked.
Check the card price the consent recorded. The token create response returns it as consentedCardPrice, beside storedCredentialConsentId: rate is a decimal fraction (0.04 is 4 percent) and amount is a fixed amount in the merchant's currency, and only one of them is set. Both are read-only; the gateway prices them, and they report what the consent recorded, not what your request sent. consentedCardPrice is null when the consent recorded no card price. If you sent cardPriceDisclosed as true and get null back, the consent was still recorded, but the payments under it are charged the cash price: most often the cardPrice text you sent wasn't the gateway's statement exactly, or the merchant's program doesn't cover recurring billing. Show the cardholder the gateway's statement, then send the consent again with cardPrice left out or set to that statement exactly. When the gateway answers a repeated request with the consent it already recorded, consentedCardPrice is that consent's card price.
To add the card price to a card whose consent didn't state it, or to state a card price the merchant has raised, send the consent again with cardPriceDisclosed set to true: the gateway records a new consent when the rate or fixed amount it records differs from the one the earlier consent recorded, and otherwise answers with the consent it already recorded. A recurring contract you create on that card takes the consent's card price when it's created. A contract created before its card's consent recorded a card price keeps none, and its payments are charged the cash price.
The gateway records what a consent stated in the measure the merchant's program used: a rate with a percentage, an amount with a fixed amount. A recurring contract returns it as dualPricingConsentedRate or dualPricingConsentedAmount, and each recurring payment carries it as sourceData.consentedCardPriceRate or sourceData.consentedCardPriceAmount. All four are set by the gateway, and a value you send is discarded. A recurring payment is charged no more than the card price the consent stated, compared in dollars, even after the merchant changes how its card price is set: a 250.00 payment consented at a fixed 2.50 is charged 252.50 after the merchant raises the amount to 4.00, and one consented at 4 percent is charged 255.00 after the merchant moves to a fixed 5.00.
A payment through an invoice's payment link is priced the way the invoice is, not the way a hosted payment page is. The transaction's sourceData.source is HostedPaymentPage, and the gateway sets sourceData.invoicePayLinkPayment to true. When the payer chose how much to pay, it also sets sourceData.partialInvoicePayment to true, and that payment is charged the amount entered. Both fields are null on every other transaction, and a value you send is discarded.
Card-present sales and terminal receipts
A card read at a point of sale is a card-present sale, priced on the merchant's Terminal channel rather than the API channel, even though it arrives through the API. The gateway treats a payment as card present when its card carries any of these:
- A reader
cardData.entryMode:UnencryptedCardReaderSwipe,EncryptedCardReaderSwipe,Icc,Proximity,Fallback,TechnicalFallback, orEmptyCandidateFallback. cardholderPresenceset toCardPresent.- Track, EMV, or encrypted track data, whatever the entry mode says.
A keyed card with any other presence is an API sale, and so is a charge of a stored payment method or a payment token, whatever entry mode the card was first read with, unless you send cardholderPresence CardPresent. cardData.terminalCapability describes the device, not the sale, so it doesn't make a payment card present. A cash or bank payment is never card present.
At the terminal, the payer reads the merchant's posted signage before handing over a card, so the merchant attests that both prices are posted at every point of sale where it takes a card in person (terminalSignageAttested) before its program can cover Terminal.
Send the cash price, the same as any API sale. The terminal prints the receipt from the response:
| Receipt row | From the response | |
|---|---|---|
| Cash price | The price before the card price | invoiceData.amounts.base |
| Card price | The price the card was charged, before tax, tip, and shipping | invoiceData.amounts.base plus invoiceData.amounts.cardPriceAdjustment |
| Total | What was authorized | invoiceData.amounts.total |
Print the two price rows only when invoiceData.amounts.dualPricingAssessed is true, using the labels Cash price and Card price exactly as written. Otherwise the payment was charged one price: print total as you print any other sale. Never label the difference between the two prices a fee, an adjustment, or a surcharge.
Split tender payments
When the merchant's program covers split tender, each payment of a split tender is priced on its own share, and the group's balance is kept at the cash price:
- Read the balance at the cash price. On
GET /api/transactions/{id}/split-tender,totalRequestedAmountis the order's cash price, andtotalApprovedAmountandremainingAmountcount each payment charged the card price at its cash price.dualPricingCardPricedAmountis present on such a group, and is the part oftotalApprovedAmountthat payments charged the card price cover. It's absent on every other group. - Send the cash price you want to collect. Post an additional payment to
/api/transactions/{id}/split-tender/continuationswithinvoiceData.amounts.baseset to the cash price, up toremainingAmount. A card is charged the card price on it, andinvoiceData.amounts.cardPriceAdjustmentandtotalread back the difference and the card price total. An amount aboveremainingAmountis refused withTransactions:SplitTenderAmountExceedsRemaining(409), whatever the payment method. - A planned split tender declares the order total with
requestedSplitTenderTotalat the cash price. Its first card is charged the card price on its own share. - A fixed amount is shared across the card payments in proportion to what each one covers, so an order paid entirely by cards carries exactly the fixed amount.
For example, at 4 percent, a split tender declaring 100.00 whose first card covers 60.00 is charged 62.40, and the group then reports a remainingAmount of 40.00 and a dualPricingCardPricedAmount of 60.00. An additional card payment with a base of 40.00 is charged 41.60, and the group is paid in full.
A split tender opened on a partial approval of a payment charged the card price collects toward its cash price only when the merchant's program covered split tender as it opened. Otherwise dualPricingCardPricedAmount is absent, the group collects toward the amount the first payment asked for, and each additional payment is charged the amount sent.
Sending your own card price amount
You don't have to send cardPriceAdjustment, and most integrations shouldn't. If your own system already prices the card and you want the gateway to charge your figure, send it on the API channel:
- It's charged as sent, on a payment the card price applies to.
- It can be replaced. The platform can be set to replace a figure that disagrees with the merchant's rate or fixed amount with the configured amount, and
cardPriceAdjustmentthen reads back as the configured amount. Ask your platform administrator which way it's set. - It's dropped on a payment the card price doesn't apply to: a cash or bank payment, a benefit tender, or a channel the program doesn't cover, including a card-present sale while the program doesn't cover
Terminal. - Send
totalwithout it. Atotalyou send is checked against the other amounts, not countingcardPriceAdjustment.
The record of each decision
The gateway records every decision on the transaction: whether the card price applied and why, both prices, the rate and what it was applied to or the fixed amount, the form of payment, the merchant configuration it was made under, and, on the Virtual Terminal and hosted payment pages, what the payer was shown and when. That record is for the merchant's operators. They read it on the transaction's Dual Pricing Decision panel in the application.
The record appears in the API schema as dualPricingResult, but it isn't returned to an API key: on a request authenticated with an API key, dualPricingResult is always null. Read the money from the amounts. A payment was charged the card price when invoiceData.amounts.dualPricingAssessed is true, and cardPriceAdjustment is then the difference. Anything else was charged the amount you sent.
Refunds, voids, and reversals
A void, refund, or reversal of a payment charged the card price returns the difference in proportion to the amount returned. The share returned so far is on the original transaction as cardPriceAdjustmentReversedAmount: cumulative, rounded to the cent, never more than cardPriceAdjustment, and exactly cardPriceAdjustment once the whole payment has gone back.
For example, a 100.00 card sale at 4 percent with 8.25 tax is authorized for 112.25. A refund of 50.00 sets cardPriceAdjustmentReversedAmount to 1.78. A second refund of 62.25 sets it to 4.00.
A linked refund sent without its own invoice refunds one figure, the amount the payer paid, and carries no cardPriceAdjustment of its own.
Errors to handle
| Code | HTTP | When | What to do |
|---|---|---|---|
Transactions:CardPriceWithAnotherCharge |
400 |
A request carries cardPriceAdjustment together with a surcharge or a convenience fee. A payment carries one of the three. |
Remove one of them. |
Transactions:DualPricingNotEnabled |
403 |
A new payment carries a non-zero cardPriceAdjustment for a merchant that isn't enrolled. |
Send the cash price alone. Don't retry the same request. |
Transactions:CardPriceIsGatewayComputed |
400 |
A non-zero cardPriceAdjustment arrives on a channel the gateway renders, such as a hosted payment page. |
Remove the amount. The gateway computes it on those channels. |
Merchants:DualPricingNotEnabled |
403 |
A merchant save changes the dualPricing block while the account doesn't have the feature. |
Leave the block out, or ask for the feature. |
Surcharging:DualPricingEnabled |
409 |
A surcharge notice, enable, or force enable for a merchant with dual pricing on. | Turn dual pricing off for the merchant first, or leave surcharging off. |
An omitted cardPriceAdjustment, or a 0, is never refused. A negative one is refused with HTTP 400 as a validation error, because the card price is never below the cash price. A stored-credential consent with enrollmentTerms.cardPriceDisclosed set to true and no enrollmentTerms.fixedAmount, or one of 0, is refused the same way, and nothing is stored. A void, capture, adjustment, or linked refund of a payment charged the card price is never refused for carrying it, even after the program is turned off. A Transactions:DualPricingNotEnabled response looks like this:
{
"error": {
"code": "Transactions:DualPricingNotEnabled",
"message": "Dual pricing is not enabled for this merchant. Remove the card price amount from the request, or submit the cash price alone."
}
}
The merchant configuration block
GET /api/merchants/{id} returns the merchant's program as dualPricing, and PUT /api/merchants/{id} saves it. Leave the block out, or send it unchanged, and the stored program is kept as it is.
| Field | Type | What it does |
|---|---|---|
enabled |
boolean | The program switch. Changing it needs the Merchants.DualPricing.Enable permission, which the reseller and administrator roles hold and a merchant's own users don't. |
mode |
string | How the card price is set: Percentage, the cash price plus rate of it, or Fixed, the cash price plus fixedAmount. null reads as Percentage. |
rate |
number | The rate as a fraction, greater than 0, at most the platform ceiling (0.04 unless your administrator set another), never above 0.10, and at most four decimal places. Read only when mode is Percentage or null. |
fixedAmount |
number | The amount added to the cash price, in the merchant's currency, greater than 0, at most the platform ceiling (10.00 unless your administrator set another), never above 100.00, and in whole cents. Read only when mode is Fixed. |
includesShipping |
boolean | Whether the rate also applies to shipping. Not read when mode is Fixed. |
treatAchAsCash |
boolean | Whether a bank payment is charged the cash price. Only true is accepted. |
channels |
string | Where the program applies: VirtualTerminal, HostedPaymentPage, Api, DigitalWallet, Invoicing, RecurringBilling, SplitTender, and Terminal (card-present sales), combined as names separated by a comma and a space. SplitTender prices each payment of a split tender on its own share; a payment also needs its own channel, such as Api. |
displayCopyTemplate |
string | The price panel sentence. It must carry {cashPrice} and {cardPrice}, plus {rate} when mode is Percentage or {amount} when it's Fixed, and no other placeholder. null uses the standard wording. |
bothPricesPostedAttested |
boolean | The merchant's attestation that both prices are posted wherever it takes payment outside the platform. |
terminalSignageAttested |
boolean | The merchant's attestation that both prices are posted at every point of sale where it takes a card in person. Required to include Terminal in channels. |
bothPricesPostedAttestedAt, bothPricesPostedAttestedBy, terminalSignageAttestedAt, terminalSignageAttestedBy, enabledChangedAt, enabledChangedBy, configurationVersion |
read-only | Set by the gateway. Values you send are ignored. |
A save that turns the program on is refused with HTTP 400 when the rate (or, with mode set to Fixed, the fixed amount), a channel, or the attestation is missing, or when surcharging or the convenience fee is on for the merchant. A save that includes Terminal in channels without terminalSignageAttested is refused with HTTP 400 whether or not the program is on. Each error names the field.
The Virtual Terminal recent list
POST /api/transactions/virtual-terminal/recent lists the signed-in operator's recent Virtual Terminal payments. It returns cashPrice and cardPrice on each payment that was dual priced, and null on every other.
Webhooks
The Transaction.* lifecycle events carry the amount authorized, which is the card price total on a card payment. A webhook that carries the transaction document includes invoiceData.amounts.cardPriceAdjustment, dualPricingAssessed, and cardPriceAdjustmentReversedAmount. No webhook carries dualPricingResult.
See also
- Refunds, voids, and reversals for which undo applies at each point in the lifecycle.
- Webhook integration for the envelope, the signature, and retry behavior.