Adyen - Web implementation guide
Guest payments
For using the Adyen payment processor, we suggest you implement your custom payment form that will have empty HTML div tags acting as placeholder where Adyen SDK will inject iframe elements. Because of PCI Complience, this is the only way to avoid directly collecting payment card and security code information.
You will also need an empty container for 3D Secure dialog, also triggered by Adyen SDK.
<template
v-if="isAdyenPaymentProcessor && isAdyenSDKLoaded"
>
<v-dialog
v-model="isAdyen3DS"
@click:outside="(isAdyen3DS = false), (currentPaymentStep = 2)"
>
<div id="adyen-3ds-component" />
</v-dialog>
<payments-adyen-form
:selected-processor-id="selectedProcessor"
:selected-payment-method="selectedMethod"
:adyen-init-token="adyenPublicKey"
:available-credit-card-brands="availableCreditCardBrands"
@send-payment-data="sendAdyenPaymentData"
/>
</template>
The main difference compared to our other supported integrations is that you should not send an
init-paymentrequest when the user chooses a payment method! You should send it a bit later when the user populates the payment information and clicks on “Place order” button. We have all the information we need for the Adyen initialization, thus theinit-paymentrequest is skipped.
Regarding our implementation, in order for the PaymentsAdyenForm to load, you need need to make sure the venue supports Adyen payment processor:
const venueHasMethod = this.selectedPaymentMethod
? this.venue?.available_payment_methods?.some(
(method) => method.payment_method_id === this.selectedPaymentMethod.payment_method_id,
)
: null;
const venueHasAdyen = this.venue?.available_payment_methods?.some(
(method) => method.payment_processor_type_id === PaymentProcessorIds.ADYEN_ID,
);
if (venueHasMethod && venueHasAdyen) {
this.selectedProcessor = PaymentProcessorIds.ADYEN_ID;
}
AdyenSDK should be injected as soon as isAdyenPaymentProcessor becomes true:
@Watch('isAdyenPaymentProcessor', { immediate: true })
protected watchIsAdyenPaymentProcessor(val: boolean): void {
if (val) {
this.injectAdyenSDK();
this.adyenPublicKey = this.selectedMethodBrandInfo
?.client_authentication_public_key as string;
}
}
At this step, you will need to store locally client_authentication_public_key that you receive from the response of init-application endppoint. The API key will be used to initialize Adyen SDK.
The init-application call should be made as soon as the white label app loads, as the first endpoint to be called.
POST {{MENU_API_URL}}/api/init-application
For explanation of the request and all response data visit Init Application API reference
Response example for payment related data
{
"status": "OK",
"code": 200,
"data": {
// ...
"payment_processors": [
{
"id": "5bab16ce-bb27-4e62-aff4-124266647eaf",
"type_id": 11,
"properties": {
"client_encryption_public_key": "10001|A3795C2E0A78E5FF639AB006428D5EC19166AF82C402828476442E44476AE3DB9BE22468C15D8744574080DE5697FB81FBC4A0E0AB27B3B33A2739F20B1A514C6DCCBA3414E36F8056D4E1C007B6BF9ED5579A47313BDB651A3A984864E927B3A5D47CDA068E6A5C3AD76FB88A4173BC57EE672D421B13B3434F2D4B03FC250AAD86D64121A1760C83289EE7097A4643E493333ADE8373E9FB36A24F156C4B42D404879BBD8896705E0E91CD4F8BEC0E02A3F38D6EE275B6440F40B40E88B3D1B3292ABB331F9CB10E11D5AC81977ADCD0C22B7ECF009D608C651CC1FD7D4AA114B2130C6E82272224248B29CE4529DE93E5D010BD3976557067FD48E090B653",
"client_authentication_public_key": "test_YTPZMKLO2JGCHJWC2CFBULMURAULV7DG",
"environment": "test"
}
}
],
"fraud_detection_processors": [],
"available_payment_methods": [
{
"id": "73f393ba-14f3-42b5-b959-e681cef5e914",
"payment_method_id": "293c4959-4d85-4fe1-8367-f8c3ebeac9b6",
"payment_method_type_id": 1,
"payment_method_brand": "Visa",
"payment_processor_type_id": 0,
"payment_processor_id": 0,
"properties": null
},
{
"id": "e947b37b-dc8f-4719-82bc-76050ca146de",
"payment_method_id": "7c29b469-344c-413d-8d28-94bcce9cbd0d",
"payment_method_type_id": 1,
"payment_method_brand": "MasterCard",
"payment_processor_type_id": 0,
"payment_processor_id": 0,
"properties": null
},
{
"id": "633b02f1-34e5-45dd-a4e5-a26a8c5beeae",
"payment_method_id": "363362c7-f4bc-4a44-b694-87c508c6306b",
"payment_method_type_id": 1,
"payment_method_brand": "Amex",
"payment_processor_type_id": 0,
"payment_processor_id": 0,
"properties": null
}
]
}
}
| Attribute | Type | Example Value | Description |
|---|---|---|---|
payment_processors |
array[PaymentProcessor] | Array of payment processor results for tokenization - if multiple card payment processors are configured for the Brand, multiple results will be present | |
payment_processors[i].id |
string | "5bab16ce-bb27-4e62-aff4-124266647eaf" |
UUID of Payment Processor |
payment_processors[i].type_id |
int | 11 | Payment Processor Type ID. See Payment Processors & Payment Methods reference for more information |
payment_processors[i].properties |
object | Payment integration-specific properties | |
payment_processors[i].client_encryption_public_key |
string | "10001A3795C2E0A78E5FF639AB006428D5EC1916..." |
Public encryption key used by white label apps for encrypting credit card data before sending it to the server |
payment_processors[i].client_authentication_public_key |
string | "test_YTPZMKLO2JGCHJWC2CFBULMURAULV7DG" |
An API key that should be used to initialize Adyen SDK |
payment_processors[i].environment |
string | "test" | Represents sandbox (test) or production environments |
fraud_detection_processors |
array[FraudPaymentProcessor] | Array of fraud detection payment processors configured on tbe brand, such as Forter. Not tipically used with Adyen integration | |
available_payment_methods |
array[PaymentMethod] | Array of Payment Methods enabled for a payment processor configured on a Brand level configurations, on the CMS. |
Once you have the API key token, you can inject the Adyen SDK into the DOM:
protected injectAdyenSDK(): void {
const isAdyenScript = !!document.querySelector('script#adyen-sdk-script');
if (isAdyenScript || this.isAdyenSDKLoaded) {
this.isAdyenSDKLoaded = true;
return;
}
const script = document.createElement('script');
const cssLink = document.createElement('link');
script.src = 'https://checkoutshopper-live.adyen.com/checkoutshopper/sdk/5.1.0/adyen.js';
script.integrity = 'sha384-MSSWlGReMOvq1c7XJ2+w4ndYk2dGvQIwY5KZZ8FiSsFd8SyySH52G54o0/wxuVT+';
script.crossOrigin = 'anonymous';
script.id = 'adyen-sdk-script';
cssLink.rel = 'stylesheet';
cssLink.href = 'https://checkoutshopper-live.adyen.com/checkoutshopper/sdk/5.1.0/adyen.css';
cssLink.integrity = 'sha384-XN0c1CgugymoLluXz9O4h5yZilh197E8065oXNe5SgK9TSm6JL2eBdEM7IETwboE';
cssLink.crossOrigin = 'anonymous';
document.head.appendChild(cssLink);
document.body.appendChild(script);
script.addEventListener(
'load',
() => {
this.isAdyenSDKLoaded = true;
},
{ once: true },
);
}
Now, our PaymentsAdyenForm will load and receive all the necessary data that we pass it. Mentioned component is responsible for presenting the payment form and processing card information through Adyen SDK.
HTML form example:
<template>
<div ref="customCardContainer">
<form @submit.stop.prevent="submit">
<div>
<label for="encryptedCardNumber">Card number</label>
<div class="input_field" data-cse="encryptedCardNumber"></div>
<span v-show="fieldType === 'encryptedCardNumber'" class="error-message">
{{ errorText }}
</span>
</div>
<div>
<div>
<label for="encryptedExpiryDate">Expiration date</label>
<div class="input_field" data-cse="encryptedExpiryDate"></div>
<span v-show="fieldType === 'encryptedExpiryDate'" class="error-message">
{{ errorText }}
</span>
</div>
<div>
<label for="encryptedSecurityCode">
CVV/CVC:
</label>
<div class="input_field" data-cse="encryptedSecurityCode"></div>
<span v-show="fieldType === 'encryptedSecurityCode'" class="error-message">
{{ errorText }}
</span>
</div>
</div>
<button
ref="payButton"
type="submit"
>
Place order
</button>
</form>
</div>
</template>
We have empty div container where Adyen SDK will inject iframes for Card number and Security code (CVV) fields and additionally encrypt the values. We never have a chance to get and decrypt values since all input fields are in separate iframes. Here, we also have a chance to play with form validation and styles.
Example of how we configure and initialize Adyen
protected async initAdyen(): Promise<void> {
const handleOnChange = ({ isValid, data: { paymentMethod } }) => {
if (isValid) {
this.encryptedCardData = { ...paymentMethod };
this.errorText = '';
this.errorType = ''
}
};
const configuration = {
locale: 'en_US',
onChange: handleOnChange,
clientKey: this.adyenInitToken, // client_authentication_public_key
};
this.shouldShowLoader = true;
const checkout = await initializeAdyen(configuration);
const customCard = checkout?.['create']('securedfields', {
// Optional configuration
type: 'card',
brands: this.availableCreditCardBrands,
styles: {
base: { color: 'black' },
error: { color: 'red' },
validated: { color: 'green' },
},
onLoad: () => {
// when the form is loaded
this.shouldShowLoader = false;
},
onError: (error) => {
// do the error handling here
this.errorType = error?.fieldType;
this.errorText = error?.errorI18n;
},
});
customCard.mount(this.$refs.customCardContainer);
}
async function initializeAdyen(config: AdyenConfiguration): Promise<unknown> {
const configuration = {
locale: window.navigator.language,
environment: environment.production ? 'live' : 'test',
...config
};
return await window.AdyenCheckout(configuration);
}
When the user populates and submits the payment form, if there are no errors you should start preparing data for the init-payment request. As you can see, Adyen provide us with the encrypted values for card number and CVV, in case we need to send them to the API, which we do.
Encrypted data, as well as the chosen payment method ID notify the parent component of the data.
protected submit(): void {
if (!this.errorText && !this.errorType) {
const cardData = {
encrypted_card_number: this.encryptedCardData?.encryptedCardNumber,
encrypted_expiry_year: this.encryptedCardData?.encryptedExpiryYear,
encrypted_expiry_month: this.encryptedCardData?.encryptedExpiryMonth,
encrypted_security_code: this.encryptedCardData?.encryptedSecurityCode,
};
this.$emit('send-payment-data', {
payment_method_id: this.selectedPaymentMethod,
...cardData,
});
}
}
Back in the parent component, we react to that event by calling the sendAdyenPaymentData method:
protected async sendAdyenPaymentData(data: AdyenGuestPaymentData): Promise<void> {
this.adyenGuestData = data;
await this.initializePayment(this.selectedMethod, PaymentProcessorIds.ADYEN_ID);
}
Init payment payload preparation:
case PaymentProcessorIds.ADYEN_ID: {
initPaymentData = {
...initPaymentData,
payment_info: {
...initPaymentData.payment_info,
...this.adyenGuestData,
},
device_info: {
userAgent: window.navigator.userAgent,
acceptHeader:
'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8',
language: window.navigator.language,
colorDepth: window.screen.colorDepth,
screenHeight: window.screen.height,
screenWidth: window.screen.width,
timeZoneOffset: new Date().getTimezoneOffset(),
javaEnabled: true,
},
urls: {
redirect_success: `${window.location.origin}${this.$route.fullPath}&type=order&pp-id=${PaymentProcessorIds.ADYEN_ID}`,
},
};
break;
}
POST {{MENU_API_URL}}/api/payment-processors/init-payment
Request example
{
"amount": 735,
"venue_id": "f3c304e3-b48e-42aa-bc0a-5b15552c7cf6",
"payment_info": {
"amount": 735,
"payment_method_id": "354bc214-ab45-4857-890d-9b7c25e304d0",
"encrypted_card_number": "adyenjs_0_1_25$XjViC8cm9k1HrvrbkjF+7yE0a1+UlkP0lJX3z8umMuTHiT5QIMUIb/n7np0bVI9fchtTYygd0MsvxWG...",
"encrypted_expiry_year": "adyenjs_0_1_25$FfeLWbOLh/inhIQE/RmYK6UxE9c3ACz0tKBCaML43f7cLvl1o6Bxlty7+GmldsD0RJyqtRzIlxt0R76EZEPb5eFzobNS95zTniIiKzkCECH7M0BxJqYfn4NB9j2zLA9tSwjE8C00k7Q8v/Emczo3AwMgR...",
"encrypted_expiry_month": "adyenjs_0_1_25$N0cs5RzHtsjjtrvAKjpTVbS9oSH22QevrqrxyYZ869oECGTO9XsHHcfb5FpKyoPyZ9D+WQ/GDlDlwMMQSEmcI6edp1Dr...q",
"encrypted_security_code": "adyenjs_0_1_25$A8FLzFC7iLRfWU+mxdiYif+6flGENUxK9tLaZwZPEwrijFodcsMVBZhoX8g0SjrLocl2K0q/INE0QaoOHxioDyALOV11xv34vQNWkaybkxq9gp+RqWVavoYusUujUSe0sb7cr..."
},
"order_info": {
"singular_point_id": "02a0b125-da44-4164-b4d2-4f24133a6781",
"order_type": {
"id": 6,
"pickup_asap": true,
"properties": {},
"pickup_at": "2023-09-26 22:29:47"
},
"menu_items": [
{
"id": "23023f14-764d-4188-ad4c-f402b797b2a7",
"price_level_id": "ae342e22-0c79-44fa-89a0-6b34cf69ddd6",
"quantity": 3,
"modifiers": [],
"comment": ""
}
],
"combo_meals": [],
"discounts": [],
"discount_cards": [],
"customer_info": {
"first_name": "Isabela",
"last_name": "Companys",
"email": "Isabela.Companys@gmail.com",
"phone_number": "+49381601234567",
"demographics": []
}
},
"device_info": {
"userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36",
"acceptHeader": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8",
"language": "en-GB",
"colorDepth": 30,
"screenHeight": 1120,
"screenWidth": 1792,
"timeZoneOffset": -120,
"javaEnabled": true
},
"urls": {
"redirect_success": "https://api-public-playground.menu.app/venue/?id=f3c304e3-b48e-42aa-bc0a-5b15552c7cf6&order-type=6&time=IIZWAUg&items=KQVgQsDsEEwwlgE2HYBmAgnAjAFlwBwoDCKMAhgLYD2ArgHYAuZ6GaJZAxtZZQKZMWmFqhik4AZz4AbPp0Z9EABQBO8TnwAyfAG4yAkslSYYANlynTIDpJlyFiALLVE8AGbw%2BKiUIygIIAAiUMFBQA&screen=checkout&type=order&pp-id=11"
}
}
| Attribute | Type | Example Value | Description |
|---|---|---|---|
amount |
number | Math.round(+amountToPay * 100) | Total order amount for payment |
venue_id |
string | f3c304e3-b48e-42aa-bc0a-5b15552c7cf6 |
Store UUID |
order_info |
object | Describes all the order-specific items including menu items, combo meals, discounts, discount cards, customer information, order type, singular_point_id and tips information. See Place order for Dine-in (QS) or Takeout for more information | |
payment_info |
object | Contains payment specific info - total order amount and payment_method_id. See Payment Processors & Payment Methods for more info |
|
payment_info.encrypted_card_number |
string | Specific for Adyen integration, where credit card number is encrypted by Adyen SDK | |
payment_info.encrypted_expiry_year |
string | Specific for Adyen integration, where expiration year is encrypted by Adyen SDK | |
payment_info.encrypted_expiry_month |
string | Specific for Adyen integration, where expiration month is encrypted by Adyen SDK | |
payment_info.encrypted_security_code |
string | Specific for Adyen integration, where security code is encrypted by Adyen SDK | |
device_info |
object | Represents device information where payment is initiated | |
urls.redirect_success |
string | The full URL which API will set as the redirection URL when payment is successful |
Response example
{
"status": "OK",
"code": 200,
"data": {
"payment_processor_type_id": 11,
"payment_init_hash": "b4358c766caafa40b2d47d93d59f137d",
"allows_webhooks": false,
"expires_in": 899,
"status_polling_interval": 5,
"additional_info": {
"approval_url": null,
"approval_action": "eyJwYXltZW50RGF0YSI6IkFiMDJiNGMwIUJRQUJBZ0FNd1hQVjFKMEFoZUZlV0E3TUczckZSY244WkhOdXdDZ0xwelpETGhKaDFSeXI3YXI2Z1FoUW9wOGN2TXlTVkN5cFRtOVZhNmxxY2ZtdTZVdmNhcElhVkVJaD...",
"client_encryption_public_key": "10001|A3795C2E0A78E5FF639AB006428D5EC19166AF82C402828476442E44476AE3DB9BE22468C15D8744574080DE5697FB81FBC4A0E0AB27B3B33A2739F20B1A514C6DCCBA3414E36F8056D4E1C...",
"client_authentication_public_key": "test_YTPZMKLO2JGCHJWC2CFBULMURAULV7DG"
}
}
}
| Attribute | Type | Example Value | Description |
|---|---|---|---|
payment_processor_type_id |
int | 11 |
Payment Processor identifier (See Payment Processors for more information) |
payment_init_hash |
string | "b4358c766caafa40b2d47d93d59f137d" |
Payment hash uniquely identifying the initiated transaction |
allows_webhooks |
bool | false |
Boolean flag identifying whether the payment processor supports webhook updates for payment status. If allows_webhooks is true, then expires_in and status_polling_interval are used for short polling. Adyen does not support Webhooks. |
expires_in |
int | 899 |
Transaction validity time in seconds. This applies to all payment processors. |
status_polling_interval |
bool | 5 |
Recommended status poll interval in seconds |
additional_info |
object | additional_info object contains the URLs specific to some of the payment processors. For Adyen, the next step is decided based on this object |
|
additional_info.approval_url |
string / null | Base64 encoded string passed usually together with approval_action to payment processor SDK for initializing 3DS dialog. For Adyen payment processor, this field is not needed, |
|
additional_info.approval_action |
string / null | Base64 encoded string that can to be decoded using atob global DOM API function and then parsed into object. The object is then passed to Adyen SDK for initializing 3DS dialog. If we receive this property in the response, 3DS flow must be initiated. |
|
additional_info.client_encryption_public_key |
string | "10001A3795C2E0A78E5FF639AB06428D5EC1916..." |
Public encryption key used by white label apps for encrypting credit card data before sending it to the server |
additional_info.client_authentication_public_key |
string | "test_YTPZMKLO2JGCHJWC2CFBULMURAULV7DG" |
An API key that is be used to initialize Adyen SDK |
Webhooks are not supported for Adyen so we don’t do the short polling. Before init-payment, we do not know if webhooks are even supported, so we need to send order_info via init-payment request always. If webhooks are supported, order_info would be used to create an order by the backend side, if webhooks are faster than the client side.
3D Secure flow
When this flow is triggered, we have an additional API endpoint to call: the auth-payment endpoint.
The rest of the actions are depending on Adyen SDK, which will trigger an additional 3DS modal to be shown.
protected async handleAdyen3DS(
approval_action: string,
payment_init_hash: string,
): Promise<void> {
const callAuthPayment = async (state) => {
try {
const { data } = await axios({
method: 'POST',
headers: this.headers,
url: `${this.baseUrl}/api/payment-processors/auth-payment`,
data: {
payment_init_hash: this.initPaymentHash,
auth_info: {
action_result: window.btoa(JSON.stringify(state.data)),
},
},
});
return data?.data;
} catch (err) {
console.log(err);
}
};
let checkout = null;
const initAdyen3DS = async (action: string) => {
const actionObject = JSON.parse(window.atob(action));
const handleAdyenResponse = async (state, component) => {
const result = await callAuthPayment(state);
if (result?.additional_info?.approval_action) {
checkout.remove(component);
return await initializeAdyen(result?.additional_info?.approval_action);
}
if (result?.additional_info?.approval_action === null) {
return this.submitPayment(true);
} else {
this.isAdyen3DS = false;
return this.showGenericModal();
}
};
if (!checkout) {
checkout = await initAdyen({
onAdditionalDetails: handleAdyenResponse,
});
}
checkout
.createFromAction(actionObject, {
challengeWindowSize: '05',
})
.mount('#adyen-3ds-component');
checkout?.update();
};
if (approval_action) {
this.isAdyen3DS = true;
initAdyen3DS(approval_action);
}
if (payment_init_hash && !approval_action) {
this.submitPayment();
}
}
The approval_action will have a value and will trigger initAdyen3DS recursively until it’s called again where it becomes null.
Read more about the
auth-paymentendpoint and 3DS Authentication in the 3DS Authentication reference
At that point, you can assume that there is no next action to be done, so finally, you are at the submitPayment stage.
if (result?.additional_info?.approval_action === null) {
return this.submitPayment(true);
}
Regular flow
If the 3D Secured card was not used, then we wouldn’t get approval_action at all and this part will call the submitPayment, which will also trigger an init-payment request.
if (payment_init_hash && !approval_action) {
this.submitPayment();
}
Guest flow order creation
From the init-payment response, we expect just the payment_init_hash so that we can send it for the order request.[Create Order Flow].
Order request payload:
{
"singular_point_id": "0b8b3ccc-b56a-4926-bc72-eebdbc9b9b82",
"order_type": {
"id": 6,
"pickup_asap": true,
"properties": {},
"pickup_at": "2023-09-26 23:26:33"
},
"menu_items": [
{
"id": "3c11ab9d-835f-4546-ac33-19d375b6259d",
"price_level_id": "78ce182a-7fa4-46ec-a7d1-755fe2ca7e43",
"quantity": 3,
"modifiers": [],
"comment": ""
}
],
"combo_meals": [],
"discounts": [],
"discount_cards": [],
"customer_info": {
"first_name": "Isabella",
"last_name": "Companys",
"email": "Isabella.Companys@gmail.com",
"phone_number": "+49381601234567",
"demographics": []
},
"payment_info": {
"payment_init_hash": "be98c8a5aacd4fb9057f872dc1562bea"
},
"is_reorder": false
}
Order request is then sent and
- If you get 200 from the backend, you should inform the user about successful order creation
- If you get any error, you should inform the user that an error has happend. Some of the rejected reasons can be:
- Payment has failed for any reason
- Venue is offline
- Pickup time is not available
- Fraud Prevention tool caused the rejection
Card tokenization
When a user wants to tokenize a card, we show a modal, asking the user which payment method he/she wants to add. When the modal component is mounted, we immediately send a generate-tokens request so that we can get a payment token.
GET {{MENU_API_URL}}/api/payment-processors/generate-tokens
Response example
{
"status": "OK",
"code": 200,
"data": {
"tokens": [
{
"payment_processor_id": "5bab16ce-bb27-4e62-aff4-124266647eaf",
"payment_processor_type_id": 11,
"token": "mt-card-20230926233714-514125c5e9e0",
"additional_info": {
"client_encryption_public_key": "10001|A3795C2E0A78E5FF639AB006428D5EC19166AF82C402828476442E44476AE3DB9BE22468C15D8744574080DE5697FB81FBC4A0E0AB27B3B33A2739F20B1A514C6DCCBA3414E36F8056D4E1C007B6BF9ED5579A47313BDB651A3A984864E927B3A5D47CDA068E6A5C3AD76FB88A4173BC57EE672D421B13B3434F2D4B03FC250AAD86D64121A1760C83289EE7097A4643E493333ADE8373E9FB36A24F156C4B42D404879BBD8896705E0E91CD4F8BEC0E02A3F38D6EE275B6440F40B40E88B3D1B3292ABB331F9CB10E11D5AC81977ADCD0C22B7ECF009D608C651CC1FD7D4AA114B2130C6E82272224248B29CE4529DE93E5D010BD3976557067FD48E090B653",
"client_authentication_public_key": "test_YTPZMKLO2JGCHJWC2CFBULMURAULV7DG",
"reference": "mt-card-20230926233714-514125c5e9e0"
}
}
]
}
}
| Attribute | Type | Example Value | Description |
|---|---|---|---|
tokens |
array[Tokens] | Array of Tokens for different payment processors - if multiple payment processors are configured, you will have to follow the action of every payment processor, in order to successfully store the payment method | |
tokens[i].payment_processor_id |
string | "5bab16ce-bb27-4e62-aff4-124266647eaf" |
Payment Processor UUID |
tokens[i].payment_processor_type_id |
id | 11 |
Type ID of Payment Processor - see Payment Processors & Payment Methods reference |
tokens[i].token |
string | "mt-card-20230926233714-514125c5e9e0" |
Adyen specific token that represents a shopperReference |
tokens[i].additional_info.client_encryption_public_key |
string | "10001A3795C2E0A78E5FF639AB00683s..." |
Public encryption key used by white label apps for encrypting credit card data before sending it to the server |
tokens[i].additional_info.client_authentication_public_key |
string | "test_YTPZMKLO2JGCHJWC2CFBULMURAULV7DG" |
An API key that should be used to initialize Adyen SDK |
tokens[i].additional_info.reference |
string | "mt-card-20230926233714-514125c5e9e0" |
Same as tokens[i].token |
Once we have the token, we can initialize the component responsible for handling the payment card storing.
<payments-adyen-form
:adyen-init-token="
paymentProcessorToken.additional_info.client_authentication_public_key"
:adyen-token="paymentProcessorToken.token"
:selected-processor-id="selectedProcessor.id"
:selected-payment-method="selectedMethod.payment_method_id"
:available-credit-card-brands="availableCreditCardBrands"
@close="close()"
/>
However, notice that now we have adyen-init-token (client_authentication_public_key) and adyen-token (token), which we both receive from generate-tokens response. Those are two different tokens.
Same as for the guest payments, when you mount the modal component, Adyen SDK should be injected and initialized, in the same way as previously shown.
When the user populates the payment form and submits it, you need to pass encrypted data, as well as the token (from generate-tokens response) to the stored-payment-methods payload.
const cardData = {
encrypted_card_number: this.encryptedCardData?.encryptedCardNumber,
encrypted_expiry_year: this.encryptedCardData?.encryptedExpiryYear,
encrypted_expiry_month: this.encryptedCardData?.encryptedExpiryMonth,
encrypted_security_code: this.encryptedCardData?.encryptedSecurityCode,
};
const data: CustomerPaymentMethodRegular = {
payment_method_id: this.selectedPaymentMethod,
payment_processors: [
{
id: this.selectedProcessorId,
properties: {
token: this.adyenToken, // "token" from generate-tokens response
...cardData,
},
},
],
};
POST {{MENU_API_URL}}/api/customers/{customer_account_id}/stored-payment-methods
Request example
{
"payment_method_id": "9e546791-04bc-4f6b-9c54-0e221beca118",
"payment_processors": [
{
"id": "5bab16ce-bb27-4e62-aff4-124266647eaf",
"properties": {
"encrypted_card_number": "adyenjs_0_1_25$DlhSsLZX/4mpUFi55EDuT/VQVw2Dc9UcYKb3BVn+xtuwXmJSH82dFZCPRSpzHKU6JJV4fJ/pWmh3mJI5X8fsGt45vvs2fRtoyq02uqDOCfoSi/AL3YLu4clhqLGZyE7kZzmrlPwjhcK+MiQntszGYNFQ9WmXlop6D1eFZni0+6L1rFvZ/L5WejkSF3LlMPWGyv2kKPkkbhboqcmcuCaZb6epm5gY08HALOR9yThgkjbXJ+TZGZ0F5zD1n77S+x0tdLT6TLawUKWrvaCo...",
"encrypted_expiry_month": "adyenjs_0_1_25$XOuakw+ya5ATaUosH4EI0FavVcgmkodweS+6Ny4/9zvcmILO5249Kf+9vzUM1Gil+OhXs2eNE16PCmg5ShAHEhqe/SUQjv/IAtVdHkTYOmGDMgB/duoOFrcc3svbvnJLZrEeDhN77PyxUakuLq2uJTprbiIvl9Ryr5tMvF7qmPYV7htCRP1soX0G+zpe5YSc3pUEvEhEZD...",
"encrypted_expiry_year": "adyenjs_0_1_25$DhZUPW2WeWjKG27V65JQifPYAPuJHedcWeX7YFse75p/OvsFlOmXhRvPXLQL9RFyzcH+pkbAbGurDLiJ7Co5aMMXlb9wD8ugpicwrcEa+cUDbZ0+SUWoJGtfBudtYVIUS7GtKj26DLdy+lt/CPyaMvOAdRK6jqf2UFFqnPs1ID5iBCYpyRPHfcLUTlNlCM2DfdUG9zdUv2BMDqBTIvMT+fAR3GGVDZcEUs9gz6KzUizSw...",
"encrypted_security_code": "adyenjs_0_1_25$QLNBvXljhDz4WurIptWvei3xPV0jjyJYWI0ssXFZTtOsqU0/5SQTCKdO1qbdCGIwWsaXvpoizW1HQbkqPuNwZB9v6kxNZZQg3N7Z0CGG1KVQqpSmcnBdZ8mfHuMQQKkpnsVar3WhvDQRukgkySYjdJye4/3L688UIcO9RXBHqJYjNSBEOgVQEaGk/Wdub5gwlZfZFq5dfOdqsIZXeOJ3PM9x...",
"token": "mt-card-20230926233714-514125c5e9e0"
}
}
]
}
| Attribute | Type | Example Value | Description |
|---|---|---|---|
payment_method_id |
string | 9e546791-04bc-4f6b-9c54-0e221beca118 |
UUID of Payment Method you are trying to register - always 1 Credit Card - see Payment Processors & Payment Methods for more info |
payment_processors |
array[PaymentProcessor] | Array of payment processor results for tokenization - if multiple card payment processors are configured for the Brand, multiple results will be present | |
payment_processors.[i].id |
string | "5bab16ce-bb27-4e62-aff4-124266647eaf" |
UUID of Payment Processor - from /payment-processors/generate-tokens API call |
payment_processors[i].properties.encrypted_card_number |
string | Specific for Adyen integration, where credit card number is encrypted by Adyen SDK | |
payment_processors[i].properties.encrypted_expiry_year |
string | Specific for Adyen integration, where expiration year is encrypted by Adyen SDK | |
payment_processors[i].properties.encrypted_expiry_month |
string | Specific for Adyen integration, where expiration month is encrypted by Adyen SDK | |
payment_processors[i].properties.encrypted_security_code |
string | Specific for Adyen integration, where security code is encrypted by Adyen SDK |
Response example
{
"status": "OK",
"code": 200,
"data": {
"stored_payment_methods": [
{
"id": "326d59ef-12cc-408f-964d-bd40a2c23f91",
"parent_id": "6dc3b0a3-8717-4fe3-8b36-88060f7b1039",
"parent_type": "CustomerAccount",
"payment_method_id": "384hf836-38xa-34jd-2is9-cs83hf8zja9e",
"preselected": false,
"usage": "personal",
"properties": {
"card_type": "American Express",
"masked_number": "************8431",
"expiration_date": "03/2030",
"last_four_digits": "8431"
},
"proxy_info": null,
"billing_country_code": "",
"billing_zip_code": ""
},
{
"id": "4dcafcbc-f0a4-4788-9278-1d079dd39ca6",
"parent_id": "0d6393c1-02b3-4a8f-885e-11db5fa18476",
"parent_type": "CustomerAccount",
"payment_method_id": "027b59de-97cb-48a0-9404-37f426659bad",
"preselected": true,
"usage": "personal",
"properties": {
"card_type": "Visa",
"masked_number": "************0006",
"expiration_date": "03/2030",
"last_four_digits": "0006"
},
"proxy_info": null,
"billing_country_code": "",
"billing_zip_code": ""
}
]
}
}
| Attribute | Type | Example Value | Description |
|---|---|---|---|
stored_payment_methods |
array[StoredPaymentMethods] | Array of Stored Payment Methods that belong to the customer account | |
stored_payment_methods.[i].id |
string | "85688223-b4ee-4c0f-8593-513c182ef7b1" |
ID of the stored payment method - will be used to initialize payment |
stored_payment_methods[i].properties.card_type |
string | "visa" |
String ID for Card Type |
stored_payment_methods[i].properties.masked_number |
string | "901010******0004" |
First 6 digits and last 4 digits of credit card number (PAN) - some payment processors may only return last 4 digits |
stored_payment_methods[i].properties.expiration_date |
string | "12/2024" |
Month / Year in which credit card will expire |
stored_payment_methods[i].properties.last_four_digits |
string | "0004" |
Last 4 digits of credit card number (PAN) |
stored_payment_methods[i].preselected |
bool | true |
Defines which payment method should be pre-selected for the customer on checkout - based on last-touched (last-used) payment method for the customer |
stored_payment_methods[i].payment_method_id |
string | "027b59de-97cb-48a0-9404-37f426659bad" |
UUID of a registered payment method |
In the response, we receive a fresh state of the stored payment methods, which we use to update the UI. The newly added payment card should be presented as the chosen payment method.
We additionally call again the
generate-tokensendpoint so that the customer can place an order with a fresh token.
If the error had occurred and we had received the failure message inside of a listener, or the stored-payment-method endpoint had broken, we do:
- Show the error dialog
- Log into the event service that the error has occurred
- Reset the component state
- Call
generate-tokensendpoint so that the customer can try again using a fresh token
Vault payments
In order to pay as an authenticated user, the customer must have had stored a payment card previously.
We would know that a customer has stored a card by doing a GET stored-payment-methods API call. We do this when the component is mounted in the DOM. From the response, we see which card has preselected: true — which is the one that will be shown as a default payment card. The newly stored payment card will be presented as the new default one.
<v-dialog v-model="isAdyen3DS">
<div id="adyen-3ds-component"></div>
</v-dialog>
<button @click="submitPayment()">
Place order
</button>
Once the customer clicks on "Place order" button, firstly the payment needs to be initiated, and as soon as the response arrives, you should immediately send the orders request.
To initiate the payment, the init-payment endpoint should be called.
Init payment payload preparation:
const data = {
payment_info: {
amount: Math.round(Number(amountToPay) * 100),
stored_payment_method_id: paymentMethodId,
},
urls: {
redirect_success: `${window.location.origin}${this.$route.fullPath}&type=order&pp-id=${PaymentProcessorIds.ADYEN_ID}`,
},
device_info: {
userAgent: window.navigator.userAgent,
acceptHeader:
'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8',
language: window.navigator.language,
colorDepth: window.screen.colorDepth,
screenHeight: window.screen.height,
screenWidth: window.screen.width,
timeZoneOffset: new Date().getTimezoneOffset(),
javaEnabled: true,
},
}
POST {{MENU_API_URL}}/api/payment-processors/init-payment
Request example
{
"amount": 980,
"device_info": {
"acceptHeader": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8",
"colorDepth": 30,
"javaEnabled": true,
"language": "en",
"screenHeight": 1120,
"screenWidth": 1792,
"timeZoneOffset": -120,
"userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/118.0"
},
"order_info": {
"combo_meals": [],
"customer_info": {
"demographics": [],
"email": "isabella.companys@gmail.app",
"first_name": "Isabella",
"last_name": "Companys",
"phone_number": "+52123123123"
},
"discount_cards": [],
"discounts": [],
"menu_items": [
{
"comment": "",
"id": "0b881065-6e41-4568-a13b-108ac5f28f7a",
"modifiers": [],
"price_level_id": "b9dd9dc9-a6a2-4fd9-8d78-4fa23977d0c7",
"quantity": 4
}
],
"order_type": {
"id": 6,
"pickup_asap": true,
"pickup_at": "2023-09-27 00:19:14",
"properties": {}
},
"singular_point_id": "4c518de8-7be8-4d81-868d-57ae85c62532"
},
"payment_info": {
"amount": 980,
"stored_payment_method_id": "66941bec-2c8b-426a-a8de-2298c86fc50e"
},
"urls": {
"redirect_success": "https://api-public-playground.menu.app/venue/?id=6c5bfc54-0247-40a8-8278-9872651f0d80&order-type=6&time=IIZWAUg&items=KQVgQsDsEEwwlgE2HYBmAgnAjAFlwBwoDCKMAhgLYD2ArgHYAuZ6GuJZAxtZZQKZMWmFqhik4AZz4AbPp0Z9EABQBO8TnwAyfAG4yAkslSYYANlynTIDpJlyFiALLVE8AGbw%2BKiUIygIIAAiUMFBQA&screen=checkout&type=order&pp-id=11"
},
"venue_id": "6c5bfc54-0247-40a8-8278-9872651f0d80"
}
| Attribute | Type | Example Value | Description |
|---|---|---|---|
| amount | number | Math.round(+amountToPay * 100) | Total order amount for payment |
| order_info | object | Describes all the order-specific items including menu items, combo meals, discounts, discount cards, customer information, order type, singular_point_id and tips information. See Place order for Dine-in (QS) or Takeout for more information | |
| payment_info | object | Contains payment specific info - total order amount and stored_payment_method_id (for authenticated customers). |
|
venue_id |
string | "6c5bfc54-0247-40a8-8278-9872651f0d80" |
Store UUID |
device_info |
object | Represents device information where payment is initiated | |
urls.redirect_success |
string | The full URL which API will set as the redirection URL when payment is successful |
Response example
{
"status": "OK",
"code": 200,
"data": {
"payment_processor_type_id": 11,
"payment_init_hash": "16c816607448dd02b9878e7d1a7b8dc3",
"allows_webhooks": false,
"expires_in": 899,
"status_polling_interval": 5,
"additional_info": {
"approval_url": null,
"approval_action": null,
"client_encryption_public_key": "10001|A3795C2E0A78E5FF639AB006428D5EC19166AF82C402828476442E44476AE3DB9BE22468C15D8744574080DE5697FB81FBC4A0E0AB27B3B33A2739F20B1A514C6DCCBA3414E36F8056D4E1C007B6BF9ED5579A47313BDB651A3A984864E927B3A5D47CDA068E6A5C3AD76FB88A4173BC57EE672D421B13B3434F2D4B03FC250AAD86D64121A1760C83289EE7097A4643E493333ADE8373E9FB36A24F156C4B42D404879BBD8896705E0E91CD4F8BEC0E02A3F38D6EE275B6440F40B40E88B3D1B3292ABB331F9CB10E11D5AC81977ADCD0C22B7ECF009D608C651CC1FD7D4AA114B2130C6E82272224248B29CE4529DE93E5D010BD3976557067FD48E090B653",
"client_authentication_public_key": "test_YTPZMKLO2JGCHJWC2CFBULMURAULV7DG"
}
}
}
| Attribute | Type | Example Value | Description |
|---|---|---|---|
payment_processor_type_id |
int | 11 |
Payment Processor identifier (See Payment Processors for more information) |
payment_init_hash |
string | "b4358c766caafa40b2d47d93d59f137d" |
Payment hash uniquely identifying the initiated transaction |
allows_webhooks |
bool | false |
Boolean flag identifying whether the payment processor supports webhook updates for payment status. If allows_webhooks is true, then expires_in and status_polling_interval are used for short polling. Adyen does not support Webhooks. |
expires_in |
int | 899 |
Transaction validity time in seconds. This applies to all payment processors. |
status_polling_interval |
bool | 5 |
Recommended status poll interval in seconds |
additional_info |
object | additional_info object contains the URLs specific to some of the payment processors. For Adyen, the next step is decided based on this object |
|
additional_info.approval_url |
string / null | Base64 encoded string passed usually together with approval_action to payment processor SDK for initializing 3DS dialog. For Adyen payment processor, this field is not needed, |
|
additional_info.approval_action |
string / null | Base64 encoded string that can to be decoded using atob global DOM API function and then parsed into object. The object is then passed to Adyen SDK for initializing 3DS dialog. If we receive this property in the response, 3DS flow must be initiated. |
|
additional_info.client_encryption_public_key |
string | "10001A3795C2E0A78E5FF639AB06428D5EC1916..." |
Public encryption key used by white label apps for encrypting credit card data before sending it to the server |
additional_info.client_authentication_public_key |
string | "test_YTPZMKLO2JGCHJWC2CFBULMURAULV7DG" |
An API key that is be used to initialize Adyen SDK |
If
approval_actionis present and has a value, 3DS flow should be initiated, the same as explained for the guest flow. In that case, you would send theordersrequest as soon as Adyen SDK callback confirms successful 3DS authentication.
Vault flow order creation
After initializing the payment, the order can be created. [Create Order Flow].
Order request payload:
{
"combo_meals": [],
"customer_info": {
"demographics": [],
"email": "isabella.companys@gmail.app",
"first_name": "Isabella",
"last_name": "Comapnys",
"phone_number": "+52123123123"
},
"discount_cards": [],
"discounts": [],
"is_reorder": false,
"menu_items": [
{
"comment": "",
"id": "f46b2029-1af4-4c34-a0aa-21a784610e59",
"modifiers": [],
"price_level_id": "e75a9e75-ef9f-4260-8d20-8a3132f5c8bf",
"quantity": 4
}
],
"order_type": {
"id": 6,
"pickup_asap": true,
"pickup_at": "2023-09-27 00:19:14",
"properties": {}
},
"payment_info": {
"payment_init_hash": "16c816607448dd02b9878e7d1a7b8dc3"
},
"singular_point_id": "72b1fe06-052a-4e94-a3fa-f3a25dd3b2d0"
}
Order request is then sent and
- If you get 200 from the backend, you should inform the user about successful order creation
- If you get any error, you should inform the user that an error has happend. Some of the rejected reasons can be:
- Payment has failed for any reason
- Venue is offline
- Pickup time is not available
- Fraud Prevention tool caused the rejection