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-payment request 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 the init-payment request 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

{
	"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-payment endpoint 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-tokens endpoint 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-tokens endpoint 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_action is present and has a value, 3DS flow should be initiated, the same as explained for the guest flow. In that case, you would send the orders request 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