Articles in this section

3D Secure Integration Guide

Published:

3D Secure Integration Guide

for AirGateway API partners

Document version 1.7 · API version v1.2

This guide walks a partner that operates its own frontend and backend through the 3D Secure cardholder authentication flow exposed by the AirGateway API before an OrderCreate for a 3DS-enabled airline.


1. Overview

Some airlines (for example VY, IB) require EMVCo 3D Secure 2 (3DS2) authentication before accepting a payment. Rather than have every partner onboard directly with a 3DS vendor, the API wraps the full protocol behind a single HTTP endpoint. The underlying vendor is an implementation detail and may change without impact on the integration.

The flow is a classical browser-driven challenge:

  • The backend asks for a 3DS session tied to a card, amount and airline, and provides the three landing-page URLs where the cardholder will land after the challenge.
  • The frontend renders the challenge to the cardholder by loading a challenge URL inside an iframe.
  • On success the backend attaches the returned session ID to the OrderCreate payload; the API resolves the cryptographic proof of authentication and forwards it to the airline.

1.1 Key concepts

TermMeaning
Session IDOpaque identifier that represents an ongoing / completed 3DS session. Echo it back on OrderCreate.
Challenge URLOpaque URL that the browser must load to render the ACS challenge. Delivered in the X-Challenge-URL response header. Do NOT hard-code the host — it can change without notice.
ACSAccess Control Server. The card issuer’s page that prompts the cardholder for the OTP or biometric. Runs at the challenge URL.
Redirect URLsThe three URLs (successUrl, cancelUrl, errorUrl) hosted by the partner, one per outcome of the challenge. Sent on every ThreeDSSession request.
CAVV / ECI / DS Trx ID3DS2 authentication tokens produced by the ACS. Resolved server-side; the partner never sees them. They are eventually forwarded to the airline as part of OrderCreate.

2. When is 3DS required?

The response to POST /v1.2/OfferPrice carries the flag allowedPaymentMethods.threeDSecure — the single source of truth. When it is true, the airline requires 3DS on the OrderCreate and the flow must run before submitting the payment. When it is false, a plain card payment is fine and this document does not apply.

// Example fragment of an OfferPrice response
{
  "allowedPaymentMethods": {
    "card":         false,
    "threeDSecure": true,       // ← run 3DS before OrderCreate
    "agencyCash":   false,
    "agencyCard":   false,
    "none":         true
  }
}

The same flag also appears in OrderView and OrderChange responses, so post-booking flows (ancillaries, seat purchases after issuance) can require 3DS independently.


3. High-level sequence

Figure 1. End-to-end 3D Secure flow.


4. Step-by-step integration

4.1 Step 1 — Detect that 3DS is required

After pricing the offer, inspect allowedPaymentMethods.threeDSecure. If it is false, skip the 3DS flow and submit the OrderCreate with the plain card as before.

4.2 Step 2 — Create a 3DS session

Post the card details, amount and the three landing-page URLs. This endpoint is server-to-server, authenticated with the same credentials used everywhere else (JWT / Ag-Auth-Key). Do NOT expose it directly to the frontend.

POST /v1.2/ThreeDSSession
Content-Type: application/json
Authorization: Bearer <your API token>

{
  "provider": "IB",                 // AgW carrier code (offer owner)
  "cardNumber": "4000001000000042", // PAN
  "expirationMonth": "06",
  "expirationYear": "28",
  "amount": 88341,                  // minor units (883.41 EUR)
  "currency": "eur",

  // Redirect landing pages hosted by the partner (see §5).
  "successUrl": "https://partner.example.com/3ds/success",
  "cancelUrl":  "https://partner.example.com/3ds/cancel",
  "errorUrl":   "https://partner.example.com/3ds/error"
}

A successful response is a small JSON body plus a custom response header that must be forwarded to the frontend:

HTTP/1.1 200 OK
X-Challenge-URL: <opaque URL the browser must load in an iframe>
Access-Control-Expose-Headers: X-Challenge-URL

{
  "sessionId": "<opaque session id>",  // echo back on OrderCreate
  "status": ""
}

The X-Challenge-URL value is fully opaque; copy it verbatim into the iframe’s src. Its host, path and query string are implementation details and may change without notice.

The Access-Control-Expose-Headers hint is critical: without it the browser hides custom headers from JavaScript. The server already sets it, but a partner backend that proxies the response must forward X-Challenge-URL to the frontend (as a body field, another header, or however fits the architecture).

4.3 Step 3 — Render the challenge

Mount an iframe pointing at the challengeUrl. The card issuer’s ACS runs inside: it fingerprints the browser, negotiates 3DS2 with the Directory Server, and (for challenge flows) prompts the cardholder for an OTP.

<!-- Minimum viable markup -->
<iframe
  src="{{ challengeUrl }}"
  frameborder="0"
  allow="payment *"
  style="width:520px; height:640px; border:0; background:#fff;">
</iframe>

For a good UX, render the iframe as a modal-style overlay (centered, dimmed backdrop, above the rest of the page). When the challenge finishes, the ACS redirects the iframe to one of the three URLs sent in step 2. Those landing pages notify the host page via window.postMessage, and the host page then tells the backend to continue with OrderCreate.

4.4 Step 4 — Attach the session on OrderCreate

Once the challenge resolves successfully, submit the OrderCreate as usual, adding a payment.threeDSecure block that carries the session ID. Do not attempt to fill in any authentication tokens; they are resolved server-side.

POST /v1.2/OrderCreate

{
  "query": { ...standard fields... },
  "payment": {
    "method":     "3ds",
    "cardType":   "CC",
    "cardCode":   "VI",
    "cardNumber": "4000001000000042",
    "expiration": "0628",
    "cardHolderName":    "JANE",
    "cardHolderSurname": "DOE",
    "cardHolderEmail":   "jane@example.com",
    "threeDSecure": {
      "evervaultSessionId": "<sessionId from step 2>"
    }
  }
}

The field is named evervaultSessionId for historical reasons in the contract; the name is purely conventional and does not imply anything about how the session was obtained. Put here the exact sessionId string that POST /v1.2/ThreeDSSession returned.


5. Redirect landing pages

The challenge iframe ends by redirecting itself (not the top window) to one of the three URLs passed on the ThreeDSSession request. Host these three pages on your own domain, and each must post a message to the parent window with the outcome:

URL kindPurposepostMessage payload
successUrlLanding after a successful authentication. May carry vendor-specific query-string parameters; forward the whole set verbatim to the parent.{ type: '3ds-outcome', outcome: 'success', params }
cancelUrlLanding when the cardholder abandoned the challenge (e.g. closed the OTP prompt).{ type: '3ds-outcome', outcome: 'cancel' }
errorUrlLanding when the ACS or the network failed.{ type: '3ds-outcome', outcome: 'error' }

5.1 Example landing page

<!-- /3ds/success (analogous for /3ds/cancel and /3ds/error) -->
<!DOCTYPE html>
<html>
<body>
  <p>Redirecting…</p>
  <script>
    // Forward every query-string param the ACS appended.
    // Field names may vary and are considered opaque.
    const params = Object.fromEntries(new URLSearchParams(location.search));
    window.parent?.postMessage({
      type: "3ds-outcome",
      outcome: "success",   // or "cancel" | "error"
      params                // raw query-string, verbatim
    }, "*");
  </script>
</body>
</html>

Read every query-string parameter into a plain object and pass it to the parent. Do not assume a specific parameter name; the exact keys depend on the underlying provider and may change — treat them opaquely and log the whole set for auditing if needed.

5.2 Host-page listener

In the host page (the one that owns the challenge iframe), add a single message listener that dispatches on the outcome:

window.addEventListener("message", (ev) => {
  const d = ev.data || {};
  if (d.type !== "3ds-outcome") return;
  closeChallengeOverlay();
  if (d.outcome === "success") {
    // Backend submits OrderCreate with the sessionId already held.
    submitOrderCreate(sessionId);
  } else if (d.outcome === "cancel") {
    showRetry("Authentication was cancelled.");
  } else {
    showRetry("Authentication could not be completed.");
  }
});

The critical guard is d.type !== "3ds-outcome" — that keeps unrelated postMessages from other libraries out of the handler.

5.3 Hosting requirements

  • HTTPS in production.
  • Must resolve to a real page (200 OK) even without a session.
  • Reachable from the browser rendering the iframe (not just from the backend). In development, http://localhost may hit Chrome’s Local Network Access restrictions when the iframe (public origin) tries to redirect to localhost. Workarounds: expose the dev server via a tunnel (ngrok, cloudflared), or disable the Chrome flag Local Network Access Checks at chrome://flags.
  • A simple pattern: derive the three URLs from window.location.origin on the client and pass them to the backend, which forwards them on the ThreeDSSession request. That way local, staging and production automatically use the correct host without per-env config.

6. Test cards (sandbox)

Use the following PANs in sandbox for end-to-end testing. Expiry is 06/28 for all. When the ACS asks for an OTP, enter 4444 to authenticate successfully, or 4009 to trigger a declined authentication.

Visa

PANFlowOutcome
4000001000000018FrictionlessAuthenticated
4000001000000034FrictionlessDeclined
4000001000000042ChallengeDepends on OTP
4000001000000026ChallengeDepends on OTP

Mastercard

PANFlowOutcome
5100001000000014FrictionlessAuthenticated
5100001000000030FrictionlessDeclined
5100001000000022ChallengeDepends on OTP
5100001000000048ChallengeDepends on OTP

American Express

PANFlowOutcome
340000100000016FrictionlessAuthenticated
340000100000032FrictionlessDeclined
340000100000024ChallengeDepends on OTP
340000100000040ChallengeDepends on OTP

Frictionless cards never show an OTP prompt — the ACS resolves automatically. For additional card schemes (JCB, Discover, Diners) ask your AirGateway account manager.


7. Error handling

SituationAction
4xx on POST /v1.2/ThreeDSSessionShow a generic “payment could not be initialised” message; do NOT retry automatically — the cardholder must correct the details or restart the flow. If the error mentions missing redirect URLs, ensure the three fields are included in the request body.
Challenge iframe never redirects (timeout / cardholder closes)Treat as error. Reset the payment form; do not reuse the sessionId — request a new one.
outcome = 'cancel'The cardholder deliberately aborted. Prompt to try again or choose a different card. Do not resubmit the same session.
outcome = 'error'Log the outcome and any parameters received; show a retry option.
OrderCreate 4xx after successful 3DSThe airline rejected the payment. The authentication tokens have been consumed and cannot be reused — repeat the 3DS flow if the cardholder retries.
Screen too small (browser innerWidth < 768 in an iframe)If the host page hides content on narrow viewports, exclude the challenge-callback routes from that guard, otherwise the landing page will not render and the postMessage will never fire.

8. Partner checklist

  • [ ] Read allowedPaymentMethods.threeDSecure from every OfferPrice / OrderView response.
  • [ ] When true, call POST /v1.2/ThreeDSSession from the backend, never from the browser.
  • [ ] Include the three redirect URLs (successUrl, cancelUrl, errorUrl) in that call.
  • [ ] Read the X-Challenge-URL header and forward it to the frontend as an opaque string.
  • [ ] Host the three redirect landing pages on the partner’s own domain, HTTPS, publicly reachable.
  • [ ] In each landing page, forward the raw query-string params to the parent with window.parent.postMessage({ type: '3ds-outcome', outcome, params }, '*').
  • [ ] In the host page, listen for that message, tear down the iframe, and submit OrderCreate with the sessionId inside payment.threeDSecure.evervaultSessionId (historical field name; purely the destination for the opaque sessionId).
  • [ ] Never fill in CAVV / ECI / DS Transaction ID manually; they are resolved from the sessionId.
  • [ ] Do not reuse a sessionId once OrderCreate has consumed it, or after any error path.

Access denied
Access denied