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
paymentResultand 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:
- Configure Adyen Drop-in as an available payment method in Centra.
- Make sure your checkout can fetch payment methods and payment instructions.
- 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-Secretheader- Adyen plugin payload with amount, line items, and
returnUrl(your confirmation page URL in case the 3ds uses redirection flow)
Relevant API references:
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- optional rollback with
mutation updateLine
- optional rollback with
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 selectionto get the current selection state - if you are handling PDP Express Checkout and the item is not yet in the selection, call
mutation addItemto 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 paymentInstructionswithpaymentInitiateOnly: 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
onSubmitpayload
onSubmit- triggered when Adyen submits the final Google Pay payment data
- use this as the final payment step
- call
mutation paymentInstructionswith 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 selectionto get the current selection state - if you are handling PDP Express Checkout and the item is not yet in the selection, call
mutation addItemto 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 paymentInstructionswithpaymentInitiateOnly: true - if needed, call
query selectionto 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
onSubmitpayload
onSubmit- triggered when Adyen submits the final Apple Pay payment data
- use this as the final payment step
- call
mutation paymentInstructionswith the final wallet payload
Reference docs:
- Apple Pay Express Checkout (Adyen): https://docs.adyen.com/payment-methods/apple-pay/web-component/express-checkout?payment-method=Apple+Pay&integration=express
5) Handling payment
Handle the standard payment flow first.
Happy path:
onSubmitcallsmutation paymentInstructions.- If no additional action is required, Adyen triggers
onPaymentCompleted. onPaymentCompletednavigates the user to your success page.- On your success page, call the
latestOrderquery 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.
- Wallet submission starts with
paymentInstructions. - If Adyen requires 3DS,
onAdditionalDetailsis triggered. - 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();
},
});
- Navigate the user to your confirmation page, where the callback payload is parsed and the
mutation paymentResultis called. - After
paymentResultresolves:- 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:
- When initializing the selection with items
- Submitting address submission
- 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:
-
Configure shipping options in Centra.
-
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
expressCheckoutflag 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
setShippingMethodmutation with{ id: $shippingMethodId }. -
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.