Skip to main content

Dynamic Currency Conversion (DCC)

DCC lets a foreign cardholder pay in their card's currency instead of the shop currency. The gateway computes a conversion offer with the current rate (with the card network markup baked in, e.g. 6% Mastercard) and returns it for presentation to the cardholder before the payment is confirmed.

DCC is only available for the Server-to-Server (S2S) integration. The iframe SDK handles DCC automatically inside the frame - merchants do not need to implement anything extra.

DCC activation

DCC requires per-service activation. Contact dpay if you want to enable this mechanism on your account.

When does the cardholder see a DCC offer?

A DCC offer appears only when all of the following are true:

  1. The payment service has DCC enabled in the dpay panel.
  2. The cardholder's card was issued in a currency other than the shop currency.
  3. The gateway determined that the transaction qualifies for conversion.

If any condition is not met, the transaction proceeds normally - the frontend receives a regular SUCCESS or FORM (3DS) right away. No changes to the existing card flow are required.

State machine

1. POST /pay/card-otp (encryptedCardData)

├── SUCCESS ──────────────────────────────────► done (payment captured)

├── FORM (3DS) ──► render HTML ──► return from ACS
│ POST /pay/card-otp (threeDsConfirmed: true) ──► SUCCESS

└── DCC_OFFER ──► currency selection UI ──► cardholder decision
POST /pay/card-otp (dccDecision: "accept" | "reject")

├── SUCCESS ───────────────────────────────► done

└── FORM (3DS after DCC) ──► render HTML ──► return from ACS
POST /pay/card-otp (threeDsConfirmed: true) ──► SUCCESS

First call - inspecting the response

The first call to POST /api/v1_0/cards/payment/{transactionId}/pay/card-otp is unchanged. The response may include a new redirectType:

{
"success": true,
"status": "success",
"message": {
"redirectType": "DCC_OFFER",
"redirectText": null,
"dccOffer": {
"currencyConversionId": "00509166251006151007",
"originalAmount": 3.00,
"originalCurrency": "EUR",
"convertedAmount": 13.52,
"convertedCurrency": "PLN",
"exchangeRate": 4.507968,
"validUntil": "2026-05-03T17:40:07+00:00",
"declarationText": "Make sure you understand the costs of currency conversions...",
"markup": [
{ "rate": 6.0, "additionalInfo": "Mastercard" }
],
"europeanEconomicArea": true
}
}
}

dccOffer fields

FieldTypeDescription
currencyConversionIdstringOffer identifier (useful when correlating with dpay support)
originalAmountfloatAmount in the shop currency
originalCurrencystring (ISO 4217)Shop currency
convertedAmountfloatAmount in the card currency (markup included)
convertedCurrencystring (ISO 4217)Card currency
exchangeRatefloatConversion rate (markup included)
validUntilstring (ISO 8601)Expiry timestamp of the offer
declarationTextstringPSD2 regulatory text - show to the cardholder verbatim, do not translate
markuparrayArray of { rate, additionalInfo } objects with the card network markup
europeanEconomicAreaboolWhether the card was issued in the EEA

UI requirements - what to display to the cardholder

Presentation of a DCC offer is regulated by PSD2 and CBPR2. Required elements:

  1. Both amounts side-by-side - originalAmount + originalCurrency vs convertedAmount + convertedCurrency.
  2. declarationText in full - the regulatory text returned by the gateway. Do not shorten or translate it.
  3. Card network markup (markup) - e.g. "Conversion includes a 6% markup (Mastercard)". The field is an array - handle multiple entries.
  4. Countdown to validUntil - disable buttons after expiry or force a return to the payment method screen.
  5. Two action buttons:
    • "Pay in {originalCurrency}" → sends dccDecision: "reject"
    • "Pay in {convertedCurrency}" → sends dccDecision: "accept"
Do not decide on the cardholder's behalf

The decision must be made consciously by the cardholder - regulators require both amounts to be shown and the cardholder to explicitly select a currency before completing the payment.

Second call - sending the decision

After the cardholder picks a currency, call the same endpoint again with the original payload plus the new dccDecision field:

POST /api/v1_0/cards/payment/{transactionId}/pay/card-otp
Content-Type: application/json

{
"channelId": 31,
"encryptedCardData": "<the same payload as in the first call>",
"deviceInfo": { ... },
"email": "...",
"dccDecision": "accept"
}
FieldTypeValuesDescription
dccDecisionstring"accept" / "reject"Cardholder decision
encryptedCardDatastring-The same encrypted payload as in the first call
encryptedCardData between calls

The frontend keeps encryptedCardData in memory between the first and the second call - exactly as during a 3DS challenge, where after returning from ACS you send threeDsConfirmed: true with the same encrypted payload.

Possible responses after the decision

  • SUCCESS - the payment is captured (most common case)
  • FORM - the gateway requires an additional 3DS challenge after DCC. Render the HTML, after returning from ACS call the endpoint with threeDsConfirmed: true and the same encryptedCardData

DCC business errors

Message (message)HTTPCauseFrontend action
DCC_OFFER_EXPIRED400Decision sent after validUntilShow "Offer expired, please try again", return to the payment method screen (start a new flow from scratch)
INVALID_FLOW_STATE400dccDecision sent without an active offer (e.g. on a finalized transaction)Technical error - log + redirect to error screen
DCC_PROVIDER_ERROR502Card processor error while updating the DCC decisionTechnical error - error screen, retry possible

Full example - handling all branches (Node.js)

async function handleCardPaymentResponse(response, retryWith) {
const { redirectType, redirectText, dccOffer } = response.message;

switch (redirectType) {
case 'SUCCESS':
window.location.href = '/success';
return;

case 'FORM':
renderHtmlAndAutoSubmit(atob(redirectText));
return;

case 'DCC_OFFER':
const decision = await showDccOfferUI(dccOffer);
const next = await retryWith({ dccDecision: decision });
return handleCardPaymentResponse(next, retryWith);

case 'URL':
window.location.href = redirectText;
return;

default:
throw new Error(`Unknown redirectType: ${redirectType}`);
}
}

Testing

A full list of test PANs covering DCC scenarios is available on the Test environment page. In short:

PANScenario
5346930000008110Standard DCC offer (EUR→PLN, 30-minute validity)
5346930000008128Card does not qualify for DCC - frontend receives SUCCESS immediately
5346930000008136DCC offer with 60-second validity - expiration test
5346930000008144DCC offer with an additional 3DS after the decision - cascade DCC_OFFERFORMSUCCESS

What does NOT change

  • Standard success path (local card, no DCC, no 3DS) - unchanged, works as before
  • 3DS challenge without DCC - unchanged, same flow, same FORM shape
  • encryptedCardData, deviceInfo, email, channelId - unchanged
  • Endpoint, authorization, RSA key format - unchanged