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 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:
- The payment service has DCC enabled in the dpay panel.
- The cardholder's card was issued in a currency other than the shop currency.
- 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
| Field | Type | Description |
|---|---|---|
currencyConversionId | string | Offer identifier (useful when correlating with dpay support) |
originalAmount | float | Amount in the shop currency |
originalCurrency | string (ISO 4217) | Shop currency |
convertedAmount | float | Amount in the card currency (markup included) |
convertedCurrency | string (ISO 4217) | Card currency |
exchangeRate | float | Conversion rate (markup included) |
validUntil | string (ISO 8601) | Expiry timestamp of the offer |
declarationText | string | PSD2 regulatory text - show to the cardholder verbatim, do not translate |
markup | array | Array of { rate, additionalInfo } objects with the card network markup |
europeanEconomicArea | bool | Whether 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:
- Both amounts side-by-side -
originalAmount+originalCurrencyvsconvertedAmount+convertedCurrency. declarationTextin full - the regulatory text returned by the gateway. Do not shorten or translate it.- Card network markup (
markup) - e.g. "Conversion includes a 6% markup (Mastercard)". The field is an array - handle multiple entries. - Countdown to
validUntil- disable buttons after expiry or force a return to the payment method screen. - Two action buttons:
- "Pay in {originalCurrency}" → sends
dccDecision: "reject" - "Pay in {convertedCurrency}" → sends
dccDecision: "accept"
- "Pay in {originalCurrency}" → sends
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"
}
| Field | Type | Values | Description |
|---|---|---|---|
dccDecision | string | "accept" / "reject" | Cardholder decision |
encryptedCardData | string | - | The same encrypted payload as in the first call |
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 withthreeDsConfirmed: trueand the sameencryptedCardData
DCC business errors
Message (message) | HTTP | Cause | Frontend action |
|---|---|---|---|
DCC_OFFER_EXPIRED | 400 | Decision sent after validUntil | Show "Offer expired, please try again", return to the payment method screen (start a new flow from scratch) |
INVALID_FLOW_STATE | 400 | dccDecision sent without an active offer (e.g. on a finalized transaction) | Technical error - log + redirect to error screen |
DCC_PROVIDER_ERROR | 502 | Card processor error while updating the DCC decision | Technical 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:
| PAN | Scenario |
|---|---|
5346930000008110 | Standard DCC offer (EUR→PLN, 30-minute validity) |
5346930000008128 | Card does not qualify for DCC - frontend receives SUCCESS immediately |
5346930000008136 | DCC offer with 60-second validity - expiration test |
5346930000008144 | DCC offer with an additional 3DS after the decision - cascade DCC_OFFER → FORM → SUCCESS |
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
FORMshape encryptedCardData,deviceInfo,email,channelId- unchanged- Endpoint, authorization, RSA key format - unchanged