View as Markdown

llms.txt

This guide isn't available right now

This instance couldn't load its guide catalog. The guide returns as soon as the catalog is readable again.

Back to the guides

No such guide

This instance publishes no guide under that address. It may have been renamed, or it may belong to a feature this installation hasn't enabled.

Back to the guides

That guide is part of the product documentation

This guide is written for someone operating WinkPG through its screens rather than integrating against it, so it lives in the application's own help section instead of here. Sign in to WinkPG and open Help to read it.

Back to the guides

Guides Payments

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

{
  "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

Reconnecting to the server

Could not reconnect

This session has ended

Attempt 1

Your work on this page is still here. Retrying keeps it; reloading starts the page again.

The server no longer holds this page's state, so it has to be loaded again.