Integration steps

How the Agoda partner.js solution works

The partner.js solution enables partners to associate each booking attempt with a verified set of frontend session and device context signals. It supports fraud analysis, risk assessment, and performs 3DS execution when required, and optionally tokenizes card details. Partner-SDK generates a sdk_context_token which must be passed unchanged in the Affiliate Book API as part of the booking request.

In summary, the integration consists of three steps, followed by a conditional step for 3DS challenge flow. Refer to the detailed step-by-step instructions in subsections to learn more.

  1. Include Partner-SDK on your website: You must include Partner-SDK on the checkout or booking page at a minimum. For optimal session continuity and stronger fraud detection, we recommend including Partner-SDK on every page of the booking journey through checkout, starting with the home or landing page when possible.

  2. Initialize a session in the browser: After Partner-SDK is loaded, you must initialize it to start or resume a session. During this step, Partner-SDK collects eligible browser and device context signals in the background. Partner-SDK supports common partner security configurations, including Content Security Policy constraints.

  3. Generate and submit the sdk_context_token with your Book request: Before submitting the booking to your backend, you must invoke the SDK checkout method to generate the sdk_context_token. Your frontend must forward the obtained sdk_context_token to your backend, and your backend must pass it unchanged in the Book API request.

  4. [Conditional] Complete the 3DS challenge when required: If the Book API response status returns 202, and the requiredAction indicates payment_challenge, pass the statusToken back to the client browser, and have your website call window.partnerSDK.handleChallenge(statusToken) to complete the 3DS challenge. Your backend must also poll the Booking Status API to obtain the final booking status.

Integration flow

Follow the steps below to integrate the Partner-SDK into your website and enable token generation for fraud detection.

Step 1 - Installation & setup

Load partner.js on your home and checkout pages at minimum.

  • Required: Checkout page (where the booking is submitted)
  • Recommended: Every page of the user's booking journey (to establish the user session early) through checkout, beginning with the home or landing page when possible (where the user starts their booking journey)
  • (Global - Preferred): Add the script to your base layout/template. This ensures the SDK and its security beacons load and are ready before the user transitions to the payment page.

Example:

Place the following script in the <head> or before the closing <body>tag.

<script src="https://cdn6.agoda.net/s-cdn-partner-sdk/v1/`partner.js`"></script>

The Partner SDK (partner.js) may dynamically load additional scripts and send browser beacons used for fraud detection and telemetry. To ensure full functionality (and avoid blocking required network calls), update your site’s Content Security Policy (CSP) to explicitly allow the SDK CDN, fraud vendor endpoints, and the Bento analytics endpoints provided during onboarding.

Optional CSP directives and example values

DirectivePurpose / DescriptionAllowed Sources / Notes
script-srcAllow loading of partner.js and any secondary script loaders/beacons- Agoda SDK CDN: https://cdn6.agoda.com - Fraud provider beacon/loader: https://beacon.riskified.com, https://*.forter.com, https://*.cloudfront.net - Bento analytics scripts: https://bento.agoda.com - If you use nonces: Include 'nonce-YOUR_NONCE_VALUE' for each script tag that uses a nonce.
connect-srcAllow XHR/fetch/beacon network calls that the SDK will make- Partner SDK backend endpoints: https://*.agoda.com and https://*.travelwebservice.com - Fraud provider endpoints: https://beacon.riskified.com, https://*.forter.com, https://*.cloudfront.net - Bento analytics collection endpoint(s): (https://bento.agoda.com/) (covered with https://*.agoda.com)
worker-srcAllow telemetry- Agoda telemetry: https://www.agoda.com
img-srcAllow pixel image beacons (if used by fraud/analytics vendors)- Fraud provider image pixel: https://img.riskified.com

Example CSP header:

<meta http-equiv="Content-Security-Policy" content="
  script-src 
    'self' 
    https://www.agoda.com 
    https://*.agoda.com 
    https://*.riskified.com 
    https://cdn6.agoda.net
    https://*.forter.com 
    https://*.cloudfront.net; 

  connect-src 
    'self' 
    https://*.agoda.com 
    https://*.travelwebservice.com 
    https://bento.agoda.com 
    https://*.riskified.com 
    https://*.forter.com 
    https://*.cloudfront.net
    https://1.1.1.1
    https://wtp.siteperformancetest.net
    https://siteperformancetest.net
    wss://cdn0.forter.com;
  
worker-src 
    'self' 
    https://www.agoda.com 
    https://*.agoda.com 


   img-src 
    'self' 
    https://img.riskified.com;

">

Step 2 — Initialize a session in the browser

After the script loads, call window.partnerSDK.init(config) to initialize a browser session and load the required fraud-detection components.

During initialization, the SDK collects eligible browser, session, and device context signals in the background. The SDK is designed to work with common partner security setups (including Content Security Policy (CSP) constraints).

  • Call window.partnerSDK.init(config) with the required siteId and environment values.
  • To enable PAN tokenization, provide the exact partnerName supplied by Agoda. The init()validates the format of partnerName and rejects invalid syntax; it does not confirm that the name is configured for tokenization.
  • You may provide cspNonce, onReady, and onError when needed.
window.partnerSDK.init(config: {
  siteId: string;
  partnerName?: string;
  environment: 'sandbox' | 'production';
  cspNonce?: string;
  onReady?: () => void;
  onError?: (error: Error) => void;
}): Promise<void>;

Partner SDK init parameters

ParameterTypeRequiredDescription
siteIdstringRequiredUnique identifier provided by Agoda to the partner
partnerNamestringProvisionalOptional when PAN tokenization is not required; required to enable PAN tokenization. Use the exact partnerName supplied by Agoda. The value must contain lowercase letters, digits, or hyphens and must start and end with a letter or digit. Invalid syntax causes init() to reject. A syntactically valid name can pass init() even if it is not configured for tokenization; checkout() rejects with TOKENIZE_FAILED if tokenization cannot be completed.
environmentenumRequiredEnvironment to use ("sandbox" or "production")
cspNoncestringOptionalAllows safe, authorized inline scripts/styles
onReadyfunctionOptionalHook to enable UI elements once the SDK is "live"
onErrorfunctionOptionalHook to catch network or configuration errors
  • You can handle the init result using either Promise or a Callback.

Option A: Using the Promise response (recommended)

Assign the response to a variable to access the token directly.

try {
 await window.partnerSDK.init({
   siteId: "12345",           // Agoda-issued site ID
   partnerName: "partner-test",  // Optional Agoda-issued partner name; provide to enable PAN tokenization
   environment: "sandbox",       // Use production only when ready
   cspNonce: "rAnd0m12345",   // Optional nonce for CSP compliance
  });
 console.log("SDK is ready")  // Optional log for Init success
} catch(e) {
//  SDK initialization fail/error handling
   console.error("SDK init failed", error) //  Optional log for Init fail
}

Option B: Using a Callback

<script>
 window.partnerSDK.init({
   siteId: "12345",           // Agoda-issued site ID
   partnerName: "partner-test",  // Optional Agoda-issued partner name; provide to enable PAN  tokenization
   environment: "sandbox",       // Use production only when ready
   cspNonce: "rAnd0m12345",   // Optional nonce for CSP compliance
   onReady: () => console.log("SDK is ready"), // optional if used as promise
   onError: (error) => console.error("SDK init failed", error)
 });
</script>
  • The init() completes asynchronously. When initialization succeeds, Partner-SDK is ready to use. If the configuration is invalid, initialization fails and the Promise/Callback returned by init() is rejected. Fraud-beacon issues do not stop initialization and are reported through onError.

Step 3 — Generate a sdk_context_token with checkout()

The Book API request requires the sdk_context_token value generated by the checkout() method. Therefore, before submitting the booking from your frontend, you must invoke the SDK checkout() method.

  • The checkout() resolves on success with { success: true, sdk_context_token }. Forward the generated sdk_context_token unchanged to the partner backend.
  • Any checkout failure rejects with { success: false, error: { code, message } }. Handle the rejection with try/catch and do not submit the booking.

Configuration parameters

type PaymentCardDetails = {
  creditCardNumber: string;
  cardHolderName: string;
  expiryDate: string;
  cvc: string;
};

type CheckoutErrorCode =
  | 'INVALID_CONFIG'
  | 'TOKENIZE_FAILED';

type CheckoutError = {
  code: CheckoutErrorCode;
  message: string;
};

type CheckoutSuccessResult = {
  success: true;
  sdk_context_token: string;
};

type CheckoutFailureResult = {
  success: false;
  error: CheckoutError;
};

type CheckoutCallback = (
  result: CheckoutSuccessResult | undefined,
  error?: CheckoutError,
) => void;

window.partnerSDK.checkout(
  details?: Partial<PaymentCardDetails>,
  cb?: CheckoutCallback,
): Promise<CheckoutSuccessResult>;

SDK checkout parameters

ParameterTypeRequiredDescription
cardDetailsPartial<PaymentCardDetails>ConditionalAn object containing the sensitive payment information (number, expiry, CVV). When partnerName is configured, all four card fields are required; missing or incomplete fields cause checkout() to reject with TOKENIZE_FAILED. When partnerName is not configured (or when the partnerName is not syntactically valid ), omit this value to skip tokenization; checkout() can still return sdk_context_token without tokenized card information.
callbackCheckoutCallbackOptionalA function triggered upon completion, providing the CheckoutResult to the caller. An optional notification callback. It receives the success result as its first argument or the checkout error as its second argument.

PaymentCardDetails

ParameterTypeRequiredDescription
creditCardNumberstringRequiredThe credit card number will consist of 15 or 16 digits. Each digit can be 0-9
expiryDatestringRequiredExpiry Date in the format MMYYYY. Each digit can be 0-9
cvcstringRequiredCVC code consisting of 3-4 digits. Each digit can be 0-9
cardHolderNamestringRequiredCardholder name

CheckoutSuccessResult and CheckoutFailureResult

ParameterTypeDescription
successbooleanIndicates if the verification was successful. Returns true on a fulfilled CheckoutSuccessResult; false on a rejected CheckoutFailureResult.
sdk_context_tokenstringConditional. Only present if success is true (fulfilled CheckoutSuccessResult). This is the opaque verification token that must be passed to your backend.
errorobjectConditional. Only present if success is false (rejected CheckoutFailureResult). Contains error details.
error.codeCheckoutErrorCodeA machine-readable error identifier (e.g., FRAUD_FAIL, NETWORK_FAILURE). Use this value to determine how to handle the failure (see the table below for error codes).
error.messagestringA human-readable description of the error for logging purposes.

CheckoutErrorCode

  • When checkout() rejects, you must handle the failure and stop the booking submission in your frontend implementation.
Error codeWhen is it returnedPartner frontend action
INVALID_CONFIGcheckout() was called before init(), or the stored SDK configuration is invalid.Treat this as an SDK-integration configuration issue. Do not submit the booking. Show a generic technical error and log the details; the customer is not expected to correct this issue.
TOKENIZE_FAILEDThis rejection occurs only when PAN tokenization is enabled.
The SDK was expected to tokenize the card but could not complete tokenization. This includes missing or invalid card details, card-validation failures, and tokenization request failures. No sdk_context_token is returned.
Do not submit the booking. Suggest that the customer review their card details and try again. If the issue persists, show a generic error and offer a retry.

Usage

  • For the checkout() method, first prepare your payment card details object.
// First, prepare your cardDetails object:
// partnerName in init() enables PAN tokenization;
// when enabled, all four fields below are required.
const cardDetails = {
  creditCardNumber: "4111...",   // Full card number
  cardHolderName: "John Doe",
  expiryDate: "122028",            // MMYYYY format
  cvc: "123"
};
  • The checkout() returns a Promise. Callbacks are optional notifications and do not consume or replace the Promise. The Promise resolves on success and rejects on failure.
  • You can handle the checkout result using either the Promise or Callback.

Option A: Using the Promise response (recommended)

  • Assign the response to a variable to access the token directly.
try {
  const response = await window.partnerSDK.checkout(cardDetails);
  await submitBookingToBackend(response.sdk_context_token);
} catch (failure) {
  handleCheckoutError(
    failure.error.code,
    failure.error.message,
  );
}

Option B: Using a Callback

  • Pass a callback function as the second argument.
const callback = (result, error) => {
  if (result) {
    console.log("Token received:", result.sdk_context_token);
    // Proceed with your backend submission logic
  } else if (error) {
    console.error(`Error [${error.code}]: ${error.message}`);
  }
};

window.partnerSDK.checkout(cardDetails, callback).catch(() => {
  // The callback already handled the checkout failure.
});
⚠️

Important:

The sdk_context_token is an opaque string. Your backend must include it unchanged in the Affiliate Book API request to ensure the transaction is correctly validated and attributed

Step 4 — Pass the sdk_context_token to backend

Send the sdk_context_token to your backend to be passed in the Book API as part of your booking request payload.

  • Treat the token as opaque: do not decode, parse, or modify.
  • Do not persist in any storage: not even temporarily (avoid localStorage, sessionStorage, cookies, or in-memory caches).
  • Avoid logging the token. If logging is unavoidable, mask it.

Step 5 — Send sdk_context_token and customer details to Book API

  • Pass the sdk_context_token received from SDK in the Book API request.
  • Pass raw credit card details only if PAN tokenization is not implemented in your integration, i.e., if SDK checkout() method is not being used to tokenize the card, include the raw cardNumber, expiryDate, and cvc fields in the Book API request.
    To ensure better fraud detection/screening accuracy, pass the customer's IP address, customer email address, customer phone number, and customer billing address details to the Book API.
 {
  "waitTime": 120,
  "bookingDetails": {
    "userCountry": "US",
    "searchId": 1629183207719830000,
    "tag": "00000000-0000-0000-0000-000000000000",
    "allowDuplication": false,
    "checkIn": "2022-12-23",
    "checkOut": "2022-12-24",
    "property": {
      "propertyId": 12157,
      "rooms": [
        {
          "blockId": "blockid_v3",
          "rate": {
            "inclusive": 798.58
          },
          "surcharges": [
            {
              "id": 278,
              "rate": {
                "inclusive": 12
              }
            },
            {
              "id": 279,
              "rate": {
                "inclusive": 12
              }
            }
          ],
          "guestDetails": [
            {
              "title": "Mr.",
              "firstName": "John",
              "lastName": "Doe",
              "countryOfResidence": "US",
              "gender": "Male",
              "age": 30,
              "primary": true
            }
          ],
          "currency": "USD",
          "paymentModel": "Merchant",
          "count": 1,
          "adults": 2,
          "children": 2,
          "childrenAges": [
            5,
            6
          ],
          "specialRequest": "high floor"
        }
      ]
    }
  },
  "customerDetail": {
    "language": "en-us",
    "title": "Mr.",
    "firstName": "Test",
    "lastName": "Test",
    "email": "[email protected]",
    "phone": {
      "countryCode": "66",
      "areaCode": "2",
      "number": "22222222222"
    },
    "newsletter": false,
    "ipAddress": "<customer IP Address>"
  },
  "paymentDetails": {
    "contextToken": "<sdk_context_token>",
    "creditCardInfo": {
      "number": "<PAN>",  // Not required if PAN Tokenization is enabled
      "expiryDate": "<EXPIRY (MMYYYY)>",  // Not required if PAN Tokenization is enabled
      "cvc": "<CVC>",  // Not required if PAN Tokenization is enabled
      "holderName": "<CARD HOLDER NAME>",  // Not required if PAN Tokenization is enabled
      "billingAddress": {
         "addressLine1":"123 Main St",
         "addressLine2": "Apt 4B",
         "city": "New York",
         "state": "New York",
         "postalCode": "10001",
         "countryCode": "US"
      }
    }
  }
}

Schema fields (highlights for payment/fraud handling)

ParameterTypeDescription
paymentDetails.contextTokenstringThe token (sdk_context_token) generated from the SDK.
paymentDetails.creditCardInfoobjectThe raw credit card details (only pass if PAN tokenization is not implemented).
paymentDetails.creditCardInfo.billingAddressobjectCustomer billing address. Recommended to ensure better fraud detection/screening accuracy.
customerDetail.ipAddressstringThe customer IP Address. Recommended to ensure better fraud detection/screening accuracy.
customerDetail.emailstringCustomer email address. Recommended to ensure better fraud detection/screening accuracy.
customerDetail.phoneobjectCustomer phone number. Recommended to ensure better fraud detection/screening accuracy.

Step 6 — Book API response

Standard response (success/error)

  • For standard booking responses (success or error), follow the existing flow. Refer to the Book API documentation to learn more.
  • Example of a success response:
{
    "status": "200",
    "bookingDetails": [
        {
            "id": 20440123434,
            "itineraryID": 20437611123,
            "processing": false
        }
    ]
}

Challenge-required response (3DS)

  • When a booking requires a 3DS challenge, the Book API returns status: 202 with requiredAction set to payment_challenge.
  • In this case, pass the provided statusToken from Book response to the Partner SDK, and keep the statusToken on your backend for booking status polling.
  • Example:
{
  "status": 202,
  "bookingDetails": [],
  "requiredAction": "payment_challenge",
  "statusToken": "u9JX7vK8mW1NqA5zPz9wL3kM1bC6rX8zY4tV2bN9oE"
}

Step 7 (conditional) — Handle a challenge-required booking

📘

Note:

statusToken and sdk_context_token have different purposes. Pass the statusToken to handleChallenge(), and use the same statusToken on your backend to poll the Booking Status API. Never pass sdk_context_token or any status value to handleChallenge().

  • If you received a challenge-required response in step 6, pass that exact statusToken provided in the Book API response to your frontend application.
  • Have your frontend application call handleChallenge() from the customer's browser with the statusToken.
⚠️

Important:

The statusToken is an opaque string. Do not decode, parse, modify, log, construct, or store it in browser storage.

handleChallenge() method

type HandleChallengeFailureReason =
  | 'failed'
  | 'timeout'
  | 'cancelled'
  | 'mount_error'
  | 'network_error'
  | 'context_error'
  | 'page_changed'
  | 'decode_failed';

type HandleChallengeSuccessResult = {
  success: true;
};

type HandleChallengeFailureResult = {
  success: false;
  reason: HandleChallengeFailureReason;
  error?: Error;
};

type HandleChallengeOptions = {
  container?: string | HTMLElement;
  onSuccess?: () => void;
  onFailure?: (failure: {
    reason: HandleChallengeFailureReason;
    error?: Error;
  }) => void;
};

window.partnerSDK.handleChallenge(
  statusToken: string,
  options?: HandleChallengeOptions,
): Promise<HandleChallengeSuccessResult>;

handleChallenge() parameters

ParameterTypeRequiredDescription
statusTokenstringRequiredOpaque token from the Book response in case a challenge is required for 3DS payment.
optionsHandleChallengeOptionsOptionalConfiguration for mounting the challenge and receiving optional callbacks.

HandleChallengeOptions

OptionTypeRequiredDescription
containerstring | HTMLElementOptionalUse an existing partner-owned element, or its selector, when you want to control where the challenge is mounted. When omitted, Partner SDK creates and manages a default mount element.
onSuccess() => voidOptionalCalled when the browser challenge completes successfully.
onFailure(failure) => voidOptionalCalled for every expected failure with reason and optional error.

HandleChallengeSuccessResult and HandleChallengeFailureResult

FieldTypeWhen presentDescription
successbooleanAlwaystrue on a resolved result; false on a rejected result.
reasonHandleChallengeFailureReasonOn rejectionStable failure value for normal control flow.
errorErrorOptional, on rejectionOptional diagnostic Error. You may use its message and other properties for diagnostics; use reason for control flow.

HandleChallengeFailureReason

ReasonMeaningPartner action
failedThe challenge was not completed successfully.Ask the backend to check booking status before recovery or retry.
timeoutThe challenge timed out or remained pending.Ask the backend to check booking status before recovery or retry.
cancelledThe customer closed the challenge modal.Ask the backend to check booking status before offering a retry.
mount_errorThe SDK could not mount the challenge.Treat as a runtime integration error. Ensure that the configured challenge container node exists and is available before calling handleChallenge(). Ask the backend to check booking status if a booking may exist.
network_errorThe challenge module or required network resource could not be loaded.Show a connectivity error and allow recovery after connectivity is restored.
context_errorRequired payment or challenge context was unavailable.Show a technical error; do not continue the current challenge without valid context.
page_changedThe customer navigated away, or the page changed during the challenge.When the customer returns, restore the booking UI from the partner backend's current state.
decode_failedThe supplied statusToken could not be processed.Do not retry the same token; obtain a valid Book-response statusToken.

Usage examples

Example of 3DS challenge modal displayed by `handleChallenge()`

Example of 3DS challenge modal displayed by handleChallenge()

📘

Note:

This is a dummy 3DS challenge page for illustration only. The actual challenge page and content may depend on the issuer bank. Perform the challenge provided by your Card Network/Bank.

Challenge lifecycle

The SDK displays the 3DS challenge in a browser modal.

  • When the customer completes the challenge, handleChallenge() resolves with { success: true }.
  • When the customer closes the modal (using the top-right close control), handleChallenge() rejects with { success: false, reason: 'cancelled' }.
  • All other unsuccessful terminal paths reject with { success: false, reason, error? }.
  • error is optional, untyped diagnostic data. Use reason for control flow; do not assume the shape of error or show it directly to the customer.

Option 1: Promise-result pattern (recommended)

// The partner backend polls booking status independently.
void window.partnerSDK.handleChallenge(statusToken).catch((failure) => {
  logChallengeFailure(failure.reason, failure.error);
  // Optional: when failure.reason is 'cancelled', your integration may stop
  // backend booking-status polling for this attempt. See the Step 8 note.
});
// Process backend booking-status results independently of this Promise.

Option 2: Callback-options pattern

Callbacks are optional notifications. Providing a callback does not consume or replace the returned Promise: it still resolves on success and rejects on failure.

// The partner backend polls booking status independently.
void window.partnerSDK.handleChallenge(statusToken, {
  container: '#payment-challenge',
  onFailure: ({ reason, error }) => {
    logChallengeFailure(reason, error);
    // Optional: when reason is 'cancelled', your integration may stop
    // backend booking-status polling for this attempt. See the Step 8 note.
  },
}).catch(() => {
  // onFailure already handled the rejected challenge result.
});
⚠️

Important:

A successful handleChallenge() result confirms only that the customer completed the 3DS/payment challenge. It does not confirm the booking. As described in Step 7, the partner backend must poll booking status and move the customer to the confirmed/thank-you page only after it receives CONFIRMED.

Step 8 (conditional) — Poll Booking Status API on the backend

  • For a challenge-required booking, your backend should poll the Booking Status API concurrently while the browser runs the handleChallenge() method (see step 7 above) to get the final booking status. Handle the rejected Promise of the handleChallenge() before processing the Booking Status results.
  • Use the same opaque statusToken returned in the Book API response for the backend Booking Status polling and the frontend handleChallenge(). Pass the statusToken unchanged; you must not decode, modify, or construct it.
  • Continue polling with the same statusToken while the Booking Status API response shows processing: true (after the specified waitTime elapses).
  • Stop polling when the response shows processing: false.
  • Consider the terminal result as either CONFIRMED or FAILED.
    • Move the frontend to the confirmed/thank-you page only after the backend reports CONFIRMED.
    • When the backend reports FAILED, use the normal booking-failure flow.
📘

Note:

If handleChallenge() reports reason: 'cancelled', you may stop polling the Booking Status API for that attempt. This indicates that the customer closed the challenge; it does not confirm that the booking was cancelled or failed.

Booking Status API request

  • API details:
    • Method: POST
    • URL: /api/v4/book/status
{
  "waitTime": 120,
  "tag": "partner unique identifier",
  "statusToken": "<status token passed in book 3ds response>"
}
ParameterTypeRequiredDescription
waitTimeintegerrequiredSpecifies how long (in seconds) to wait for a response. Min: 5, Max: 120
tagstringoptionalPartner unique identifier
statusTokenstringrequiredThe statusToken value returned in the Book API response

Booking Status API response (same as Book API response)

Refer to the Book API documentation to learn more.

{
    "status": "200",
    "bookingDetails": [
        {
            "id": 80001230,
            "itineraryID": 70001230,
            "processing": false
        }
    ]
}

Partner SDK React end-to-end usage example

This example uses the browser CDN global, window.partnerSDK, loaded from Agoda's CDN.

Load and initialize the SDK in the root application layout

This example assumes the partner application has a persistent root AppLayout and an AppContextProvider. Load and initialize the SDK there, rather than in individual routed pages, so session context and the fraud beacon begin across the customer journey.

At a minimum, load and initialize Partner-SDK on the checkout or booking page. For stronger fraud-detection coverage, load and initialize it on every page of the booking journey through checkout, beginning with the home or landing page when possible.

const partnerSdkUrl =
  'https://cdn6.agoda.net/s-cdn-partner-sdk/v1/partner.js';

const AppLayout = () => (
  <AppContextProvider>
    <Header />
    <main>
      <AppRouter />
    </main>
  </AppContextProvider>
);

const Header = () => {
  const {
    markPartnerSdkReady,
    recordPartnerSdkWarning,
    recordPartnerSdkUnavailable,
  } = useAppContext();

  const initializePartnerSdk = () => {
    // Step 2 — initialize the Partner SDK after the script has loaded.
    void window.partnerSDK
      .init({
        siteId: 'YOUR_AGODA_SITE_ID',
        partnerName: 'your-partner-name',
        environment: 'sandbox',
        onError: (error) => {
          // Fraud-beacon errors are non-blocking. Record the warning in
          // application context so the partner can monitor or diagnose it.
          recordPartnerSdkWarning(error);
        },
      })
      .then(() => {
        markPartnerSdkReady();
      })
      .catch((error) => {
        // Invalid SDK configuration prevents initialization.
        recordPartnerSdkUnavailable(error);
      });
  };

  return (
    <header>
      <OtherHeaderComponents />

      {/* Step 1 — load partner.js from Agoda's CDN. */}
      <script
        src={partnerSdkUrl}
        onLoad={initializePartnerSdk}
      />
    </header>
  );
};

Keep AppLayout mounted across route changes so the SDK is not repeatedly initialized. The application context can then make SDK readiness available to routed pages, including the booking page.

Submit the booking and handle a required challenge

This example assumes AppLayout has already completed Steps 1 and 2. The booking page does not load or initialize the SDK again; it reads SDK readiness from application context and uses partner-owned booking state and UI components.

The browser generates sdk_context_token in Step 3 and sends it to the partner backend in Step 4. The partner backend performs the Book call in Step 5. This example intentionally does not prescribe the partner backend's Book request or status-polling implementation.

import { useState } from 'react';

export function BookingPage() {
  const [isSubmitting, setIsSubmitting] = useState(false);
  const [errorMessage, setErrorMessage] = useState<string | null>(null);

  // Partner-owned context, hook, and components used for illustration.
  const { isPartnerSdkReady } = useAppContext();
  const {
    bookingFields,
    cardDetails,
    isBookingReady,
  } = useBookingState();

  const handleFinalBookingStatus = (finalStatus) => {
    if (finalStatus.bookingStatus === 'CONFIRMED') {
      navigateToThankYouPage(finalStatus);
      return;
    }

    setErrorMessage('Your booking could not be confirmed.');
  };

  const handleBook = async () => {
    setIsSubmitting(true);
    setErrorMessage(null);

    try {
      // Step 3 — without partnerName, card details may be omitted.
      // With partnerName, all four card fields are required.
      let checkoutResult;

      try {
        checkoutResult = await window.partnerSDK.checkout(cardDetails);
      } catch (failure) {
        logCheckoutFailure(failure.error);
        setErrorMessage('We could not prepare your booking request.');
        return;
      }

      // Step 4 — send sdk_context_token to the partner backend.
      //
      // Step 5 — the backend calls Affiliate Book API. bookingFields represents
      // the partner's existing booking data; it must not contain PAN, CVC,
      // expiry, or raw paymentDetails.
      const bookingResponse = await submitBookingToPartnerBackend({
        ...bookingFields,
        sdk_context_token: checkoutResult.sdk_context_token,
      });

      if (
        bookingResponse.status === 202 &&
        bookingResponse.requiredAction === 'payment_challenge' &&
        bookingResponse.statusToken
      ) {
        // Step 6 returned requiredAction as payment_challenge, status as 202 and statusToken from Affiliate Book.
        //
        // Step 7 — start the browser 3DS/payment challenge with only the
        // opaque statusToken returned by Affiliate Book.
        //
        // Step 8 — concurrently wait for the partner backend's authoritative
        // status result. The backend owns polling and its polling identifier.
        const finalStatusPromise =
          waitForFinalBookingStatusFromPartnerBackend();

        void window.partnerSDK.handleChallenge(
          bookingResponse.statusToken,
        ).catch((failure) => {
          logChallengeFailure(failure.reason, failure.error);
          // Optional: when failure.reason is 'cancelled', your integration
          // may stop backend status polling for this attempt. See Step 8.
        });

        const finalStatus = await finalStatusPromise;
        handleFinalBookingStatus(finalStatus);
        return;
      }

      if (!bookingResponse.bookingId) {
        setErrorMessage(
          'The booking response did not include a booking ID or challenge token.',
        );
        return;
      }

      // A non-challenge booking also gets its final result from Step 8.
      const finalStatus = await waitForFinalBookingStatusFromPartnerBackend();
      handleFinalBookingStatus(finalStatus);
    } catch (error) {
      // Application or partner-backend failure.
      reportUnexpectedBookingFailure(error);
      setErrorMessage('We could not complete your booking request.');
    } finally {
      setIsSubmitting(false);
    }
  };

  return (
    <>
      <Loader isLoading={isSubmitting} />

      <ErrorModal
        isOpen={errorMessage !== null}
        message={errorMessage}
        onClose={() => setErrorMessage(null)}
      />

      <BookingForm />

      <BookButton
        disabled={!isPartnerSdkReady || !isBookingReady || isSubmitting}
        isLoading={isSubmitting}
        onClick={handleBook}
      />
    </>
  );
}

AppContextProvider, useAppContext, useBookingState, OtherHeaderComponents, AppRouter, BookingForm, Loader, ErrorModal, BookButton, logging functions, and backend functions represent examples of what your application code may look like; adapt as required. bookingFields contains your normal booking-request fields only; it must not include raw card data or paymentDetails.


Did this page help you?