3D Secure Integration Guide
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
OrderCreatepayload; the API resolves the cryptographic proof of authentication and forwards it to the airline.
1.1 Key concepts
| Term | Meaning |
|---|---|
| Session ID | Opaque identifier that represents an ongoing / completed 3DS session. Echo it back on OrderCreate. |
| Challenge URL | Opaque 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. |
| ACS | Access Control Server. The card issuer’s page that prompts the cardholder for the OTP or biometric. Runs at the challenge URL. |
| Redirect URLs | The three URLs (successUrl, cancelUrl, errorUrl) hosted by the partner, one per outcome of the challenge. Sent on every ThreeDSSession request. |
| CAVV / ECI / DS Trx ID | 3DS2 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 kind | Purpose | postMessage payload |
|---|---|---|
successUrl | Landing 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 } |
cancelUrl | Landing when the cardholder abandoned the challenge (e.g. closed the OTP prompt). | { type: '3ds-outcome', outcome: 'cancel' } |
errorUrl | Landing 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://localhostmay 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 atchrome://flags. - A simple pattern: derive the three URLs from
window.location.originon the client and pass them to the backend, which forwards them on theThreeDSSessionrequest. 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
| PAN | Flow | Outcome |
|---|---|---|
4000001000000018 | Frictionless | Authenticated |
4000001000000034 | Frictionless | Declined |
4000001000000042 | Challenge | Depends on OTP |
4000001000000026 | Challenge | Depends on OTP |
Mastercard
| PAN | Flow | Outcome |
|---|---|---|
5100001000000014 | Frictionless | Authenticated |
5100001000000030 | Frictionless | Declined |
5100001000000022 | Challenge | Depends on OTP |
5100001000000048 | Challenge | Depends on OTP |
American Express
| PAN | Flow | Outcome |
|---|---|---|
340000100000016 | Frictionless | Authenticated |
340000100000032 | Frictionless | Declined |
340000100000024 | Challenge | Depends on OTP |
340000100000040 | Challenge | Depends 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
| Situation | Action |
|---|---|
4xx on POST /v1.2/ThreeDSSession | Show 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 3DS | The 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.threeDSecurefrom everyOfferPrice/OrderViewresponse. - [ ] When
true, callPOST /v1.2/ThreeDSSessionfrom the backend, never from the browser. - [ ] Include the three redirect URLs (
successUrl,cancelUrl,errorUrl) in that call. - [ ] Read the
X-Challenge-URLheader 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
OrderCreatewith thesessionIdinsidepayment.threeDSecure.evervaultSessionId(historical field name; purely the destination for the opaquesessionId). - [ ] Never fill in CAVV / ECI / DS Transaction ID manually; they are resolved from the
sessionId. - [ ] Do not reuse a
sessionIdonceOrderCreatehas consumed it, or after any error path.