Skip to main content

Adyen integration

This guide explains how to integrate the current AdyenExpressCheckout component from the storefront accelerator into your checkout.

What this integration provides

The reference implementation mounts both Google Pay and Apple Pay through Adyen Web Components and runs the payment flow through Storefront API payment instructions.

In your own project, create an express checkout component that follows the same flow described in this guide (wallet initialization, submit handling, and success/failure routing).

Page responsibilities

Your storefront should define pages for the final payment states:

  • Success page: payment is complete, no further action is needed, and the page should load and render the order data.
  • Confirmation page: used for the 3DS/additional details flow, where you call paymentResult and resolve the final payment outcome.
  • Failure page: payment was not processed correctly, and the page should display error handling and a clear way for the shopper to retry the payment flow.

Prerequisites

Before integrating:

  1. Configure Adyen Drop-in as an available payment method in Centra.
  2. Make sure your checkout can fetch payment methods and payment instructions.
  3. Implement success, confirmation, and failure pages in your storefront.

Install package:

npm install @adyen/adyen-web

Integration steps

1) Fetch express checkout widget configuration

Request expressCheckoutWidgets from a backend endpoint and return wallet configuration to the frontend.

Backend requirements:

  • authenticated shopper context
  • X-Shared-Secret header
  • Adyen plugin payload with amount, line items, and returnUrl (your confirmation page URL in case the 3ds uses redirection flow)

Relevant API references:

Keep amount and line items aligned

Wallet authorization can fail if the amount or line items sent to Adyen differ from your current selection totals.

2) Create the shared AdyenCheckout instance

Create one AdyenCheckout instance used by both wallets:

const adyenCheckout = await AdyenCheckout({
amount: { currency: paymentConfig.paymentAmount.currency, value: paymentConfig.paymentAmount.amount },
clientKey: paymentConfig.clientKey,
countryCode: paymentConfig.country,
environment: paymentConfig.context,
locale: paymentConfig.languageCode,
paymentMethodsResponse: paymentConfig.paymentMethodsResponse,
onPaymentCompleted: () => {
window.location.href = successPageUrl;
},
onError: async (error) => {
if (isCancel(error) && addedItemLineId) {
await updateLine({ id: addedItemLineId, quantity: 0 });
}
},
});

Shared callbacks you should handle at this stage:

  • onPaymentCompleted
    • redirect to your success page
  • onError

3) Set up Google Pay using the shared AdyenCheckout instance

Create Google Pay from the shared instance:

const googlePay = new GooglePay(adyenCheckout, {
isExpress: true,
callbackIntents: ['SHIPPING_ADDRESS', 'SHIPPING_OPTION'],
paymentDataCallbacks: {
onPaymentDataChanged: async ({ callbackTrigger, shippingAddress, shippingOptionData }) => {
if (callbackTrigger === 'INITIALIZE') {
await maybeAddItemToSelection();
return {};
}
if (callbackTrigger === 'SHIPPING_ADDRESS') {
return await recalculateForAddress(shippingAddress!);
}
if (callbackTrigger === 'SHIPPING_OPTION') {
return await recalculateForShippingOption(shippingOptionData!.id);
}
return {};
},
},
onAuthorized: (payload, actions) => {
const ok = mapAndValidateGoogleAddresses(payload.authorizedEvent);
if (!ok) return actions.reject();
actions.resolve();
},
onSubmit: (state, _component, actions) => {
void submitPayment(state, actions, currentBillingAddress, currentShippingAddress);
},
});

Google Pay event to API mapping:

  • onPaymentDataChanged(INITIALIZE)
    • triggered when Google Pay initializes the payment sheet
    • use this step to prepare the selection for Express Checkout before the shopper continues
    • call query selection to get the current selection state
    • if you are handling PDP Express Checkout and the item is not yet in the selection, call mutation addItem to add it on the fly
  • onPaymentDataChanged(SHIPPING_ADDRESS)
    • triggered when the shopper provides or changes the shipping address in the Google Pay sheet
    • use this callback to recalculate totals and available shipping methods for the selected address
    • call mutation paymentInstructions with paymentInitiateOnly: true
  • onPaymentDataChanged(SHIPPING_OPTION)
    • triggered when the shopper selects a shipping option inside the Google Pay sheet
    • use it to persist the selected shipping method in Centra and refresh totals
    • call mutation setShippingMethod
  • onAuthorized
    • triggered after the shopper authorizes the payment in Google Pay
    • no Storefront API call is needed here
    • validate the billing and shipping data, map Google Pay address payloads into the structure your payment submission expects, and save those addresses for the later onSubmit payload
  • onSubmit
    • triggered when Adyen submits the final Google Pay payment data
    • use this as the final payment step
    • call mutation paymentInstructions with the final wallet payload

4) Set up Apple Pay using the shared AdyenCheckout instance

Create Apple Pay from the shared instance:

const applePay = new ApplePay(adyenCheckout, {
isExpress: true,
onClick: async (resolve, reject) => {
try {
await maybeAddItemToSelection();
resolve();
} catch {
reject(new Error('Unable to prepare Apple Pay checkout'));
}
},
onShippingContactSelected: async (resolve, reject, event) => {
try {
resolve(await recalculateForShippingContact(event.shippingContact));
} catch {
reject();
}
},
onShippingMethodSelected: async (resolve, reject, event) => {
try {
resolve(await recalculateForShippingMethod(event.shippingMethod.identifier));
} catch {
reject();
}
},
onAuthorized: (payload, actions) => {
const ok = mapAndValidateAppleAddresses(payload.authorizedEvent.payment);
if (!ok) return actions.reject();
actions.resolve();
},
onSubmit: (state, _component, actions) => {
void submitPayment(state, actions, currentBillingAddress, currentShippingAddress);
},
});

Apple Pay event to API mapping:

  • onClick
    • triggered after the shopper clicks the Apple Pay button, before the payment sheet is shown
    • use this step to prepare the selection for Express Checkout
    • call query selection to get the current selection state
    • if you are handling PDP Express Checkout and the item is not yet in the selection, call mutation addItem to add it on the fly
  • onShippingContactSelected
    • triggered when the shopper provides or changes the shipping address in the Apple Pay sheet
    • use this callback to recalculate totals and available shipping methods for the selected address
    • call mutation paymentInstructions with paymentInitiateOnly: true
    • if needed, call query selection to build an unsupported-country or invalid-address response payload
  • onShippingMethodSelected
    • triggered when the shopper selects a shipping method inside the Apple Pay sheet
    • use it to persist the selected shipping method in Centra and refresh totals
    • call mutation setShippingMethod
  • onAuthorized
    • triggered after the shopper authorizes the payment in Apple Pay
    • no Storefront API call is needed here
    • validate the billing and shipping data, map Apple Pay address payloads into the structure your payment submission expects, and save those addresses for the later onSubmit payload
  • onSubmit
    • triggered when Adyen submits the final Apple Pay payment data
    • use this as the final payment step
    • call mutation paymentInstructions with the final wallet payload

Reference docs:

5) Handling payment

Handle the standard payment flow first.

Happy path:

  1. onSubmit calls mutation paymentInstructions.
  2. If no additional action is required, Adyen triggers onPaymentCompleted.
  3. onPaymentCompleted navigates the user to your success page.
  4. On your success page, call the latestOrder query and render the placed order.

Example latestOrder query used on the success page:

query latestOrder {
order {
number
orderDate
status
totals {
type
price {
value
formattedValue
currency {
code
}
}
}
}
}

Handling 3D Secure flow

Use a dedicated callback flow for 3DS/additional details when Adyen requires an extra shopper step after paymentInstructions.

  1. Wallet submission starts with paymentInstructions.
  2. If Adyen requires 3DS, onAdditionalDetails is triggered.
  3. In onSubmit, detect whether the payment response requires an additional shopper step:
const response = await submitPaymentInstructions({
shippingAddress,
separateBillingAddress: billingAddress,
paymentMethodSpecificFields: state.data as Record<string, unknown>,
});

const formFields =
response.action?.__typename === 'JavascriptPaymentAction'
? ((response.action.formFields as Record<string, string> | undefined) ?? null)
: null;

if (formFields) {
actions.resolve(formFields as unknown as CheckoutAdvancedFlowResponse);
} else if (response.action?.__typename === 'RedirectPaymentAction') {
window.location.assign(response.action.url);
} else if (response.action?.__typename === 'FormPaymentAction') {
const formContainer = document.createElement('div');
formContainer.innerHTML = response.action.html;
document.body.appendChild(formContainer);
} else {
actions.resolve({ resultCode: 'Authorised' });
}

JavascriptPaymentAction, RedirectPaymentAction, and FormPaymentAction mean that Adyen requires an additional step such as 3DS. After that step is completed, onAdditionalDetails is called. 4. Post the callback payload from onAdditionalDetails to your confirmation page:

const adyenCheckout = await AdyenCheckout({
// ...
onAdditionalDetails: state => {
function flattenForPost(data: Record<string, unknown>, prefix: string): Record<string, string> {
return Object.entries(data).reduce<Record<string, string>>((acc, [key, value]) => {
const flatKey = prefix.length ? `${prefix}[${key}]` : key;

if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
return {
...acc,
...flattenForPost(value as Record<string, unknown>, flatKey),
};
}

return {
...acc,
[flatKey]: String(value),
};
}, {});
}

const form = document.createElement('form');
form.setAttribute('action', confirmationPageUrl);
form.setAttribute('method', 'post');

const flattenedItems = flattenForPost(state.data as Record<string, unknown>, '');

Object.entries(flattenedItems).forEach(([name, value]) => {
const input = document.createElement('input');
input.setAttribute('type', 'hidden');
input.setAttribute('name', name);
input.setAttribute('value', value);
form.appendChild(input);
});

form.setAttribute('style', 'position:absolute;left:-100px;top:-100px;');
document.body.appendChild(form);
form.submit();
},
});
  1. Navigate the user to your confirmation page, where the callback payload is parsed and the mutation paymentResult is called.
  2. After paymentResult resolves:
    • redirect to your success page on success
    • redirect to your failure page on error

Example paymentResult mutation used on the confirmation page:

mutation paymentResult($paymentMethodFields: Map!) {
paymentResult(paymentMethodFields: $paymentMethodFields) {
type
... on PaymentResultSuccessPayload {
...paymentResultFragment
}
userErrors {
message
path
}
}
}

Operational hints

Logging and debugging

The component uses debugLog(...) throughout initialization, submit, and callbacks. Keep those logs enabled in non-production environments to troubleshoot wallet and callback issues quickly.

Wallet availability

Apple Pay is mounted only when isAvailable() resolves. If Apple Pay does not render, confirm:

  • browser/device supports Apple Pay
  • merchant domain validation is complete
  • Adyen account and payment method are enabled for the market

Payment method selection

Express checkout config depends on finding PaymentMethodKind.AdyenDropin. If that method is not returned by checkoutPaymentMethodsQuery, wallet buttons will not initialize.

Validation checklist

Before going live, verify:

  • Google Pay can authorize and complete an order
  • Apple Pay can authorize and complete an order
  • cancellation removes any temporarily added line items
  • your success page is reachable and correctly loads the placed order
  • your confirmation page is reachable and correctly mapped for 3DS/additional details callbacks
  • your failure page is reachable and handles retry/recovery correctly

Ingrid compatibility

Unfortunately, in its current state the Ingrid plugin is not usable with Express Checkout. Ingrid has its own frontend widget that the customer needs to interact with in order to get the shipping options. There is no way to make it compatible with the Express Checkout widget. This means that orders placed via the Express Checkout widget can only utilize the standard Centra shipping options. Moreover, if the plugin is enabled, it will interfere with the Express Checkout orders. It will communicate with Ingrid and also update the selection with default shipping option and other relevant attributes. There are three places where such interference happens:

  1. When initializing the selection with items
  2. Submitting address submission
  3. Finalizing the payment

Because we cannot disable this behaviour by default, a workaround was required. These are the necessary steps you will need to perform in order for Express Checkout to not be negatively impacted by an enabled Ingrid v2 plugin:

  1. Configure shipping options in Centra.

  2. When initializing Express Checkout, fetch checkout data through the Ingrid-aware path:

    If the Ingrid workaround is disabled, use the standard checkout and selection queries:

    query checkout {
    selection {
    checkout {
    ...checkoutFragment
    }
    }
    }

    query selection {
    selection {
    ...selectionFragment
    }
    }

    If the Ingrid workaround is enabled, initialize checkout through the widget event mutation so checkout data is read with the expressCheckout flag applied:

    mutation widgetEvent($payload: Map!) {
    handleWidgetEvent(payload: $payload) {
    selection {
    ...selectionFragment
    checkout {
    ...checkoutFragment
    }
    }
    userErrors {
    message
    path
    }
    }
    }

    With variables:

    {
    "payload": {
    "expressCheckout": true
    }
    }

    After initialization, fetch the first shipping method from the selection response and perform the setShippingMethod mutation with { id: $shippingMethodId }.

  3. After initialization, keep the Express Checkout flow outside of Ingrid and use only the standard Centra shipping methods for the order.

With these steps in place the Express Checkout order will be placed outside of Ingrid. Clients that have a hard dependency on third-party Delivery Experience systems for order fulfilment can consider building custom logic that receives the order data from Centra and in Ingrid case uses the Ingrid API to book the shipping.