# 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.

**Category:** Payments

**Last reviewed:** 30 October 2026

# Cash and card prices over the API

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.

```bash
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:

```json
{
  "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](#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.cardPrice` out** 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.cardPrice`** only 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 `cardPriceDisclosed` to `false`, 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 neither `Recurring` nor `Installment`, or for a merchant whose program doesn't cover recurring billing, although `fixedAmount` is 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`, or `EmptyCandidateFallback`.
- `cardholderPresence` set to `CardPresent`.
- 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 | Print | 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`, `totalRequestedAmount` is the order's cash price, and `totalApprovedAmount` and `remainingAmount` count each payment charged the card price at its cash price. `dualPricingCardPricedAmount` is present on such a group, and is the part of `totalApprovedAmount` that 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/continuations` with `invoiceData.amounts.base` set to the cash price, up to `remainingAmount`. A card is charged the card price on it, and `invoiceData.amounts.cardPriceAdjustment` and `total` read back the difference and the card price total. An amount above `remainingAmount` is refused with `Transactions:SplitTenderAmountExceedsRemaining` (`409`), whatever the payment method.
- **A planned split tender** declares the order total with `requestedSplitTenderTotal` at 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 `cardPriceAdjustment` then 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 `total` without it.** A `total` you send is checked against the other amounts, not counting `cardPriceAdjustment`.

## 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:

```json
{
  "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](https://devportal.signapay-sbx.winkpg.io/docs/guides/refunds-voids-and-reversals.md) for which undo applies at each point in the lifecycle.
- [Webhook integration](https://devportal.signapay-sbx.winkpg.io/docs/guides/webhook-integration.md) for the envelope, the signature, and retry behavior.

## See also

- [All documentation](https://devportal.signapay-sbx.winkpg.io/llms.txt): the machine-readable index of every public page on this site.
