Headless Checkout

Using Sylius in Headless Mode with Adyen Plugin (Quick Guide)

This guide assumes that the Sylius Adyen Plugin is already installed and that your Adyen gateway (configured under code custom_adyen_gateway) is fully set up. It focuses solely on the headless payment flow and order state transitions.


Sample Adyen Drop-In Setup (Concept Only)

The following example demonstrates how Adyen’s Drop-In component could be integrated in a headless Sylius environment for testing purposes.

This is not part of a production-ready integration.

In a real-world project—whether it’s a React single-page application, a native mobile app, or any other frontend—the same principle would be applied within the chosen technology stack.

The goal here is solely to illustrate the usage pattern and facilitate testing of the payment flow.

Why Drop-In?

Adyen’s Drop-In component provides a ready-made, secure UI for collecting payment details, handling 3DS, and submitting to Adyen without building your own forms. It simplifies PCI compliance and accelerates testing of the complete payment lifecycle in a headless setup.

Test Controller

Create a quick test controller to render Adyen’s Drop-In in your headless environment:

// src/Controller/AdyenTestController.php
final class AdyenTestController extends AbstractController
{
    #[Route('/test/adyen/drop-in/{orderToken}', name: 'test_adyen_dropin')]
    public function dropin(string $orderToken): Response
    {
        return $this->render('adyen/test_dropin.html.twig', [
            'orderToken' => $orderToken,
            'gatewayCode' => 'custom_adyen_gateway',
        ]);
    }
}

Test Twig View

{# templates/adyen/test_dropin.html.twig #}
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8"/>
    <title>Sylius Adyen Headless Demo</title>
    <script
        src="https://checkoutshopper-test.cdn.adyen.com/checkoutshopper/sdk/6.18.0/adyen.js"
        integrity="sha384-ZEPvk8M++Rrf/1zMUvnfdO73cZlnj/u9oAGHSeUIIgOXoW0ZrwfyB6pBcIrhDbdd"
        crossorigin="anonymous"
    ></script>
    <link
        rel="stylesheet"
        href="https://checkoutshopper-test.cdn.adyen.com/checkoutshopper/sdk/6.18.0/adyen.css"
        integrity="sha384-lCDmpxn4G68y4vohxVVEuRcbz4iZTDh1u/FLlsCV1wSbibWKs+knhLQpgzPBqap4"
        crossorigin="anonymous"
    />
</head>
<body>
<div id="dropin-container" style="max-width:400px;margin:2em auto;"></div>

<script>
    (async () => {
        const gatewayCode = '{{ gatewayCode }}';
        const orderToken = '{{ orderToken }}';

        // Fetch Drop-In configuration
        const response = await fetch(`/api/v2/shop/payment/adyen/${gatewayCode}/${orderToken}`, {
                headers: {
                    'Accept': 'application/json'
                }
            }
        );

        const config = await response.json();

        const {AdyenCheckout, Dropin} = window.AdyenWeb;
        const errorKey = 'sylius_adyen.runtime.payment_failed_try_again';
        const errorMsg = config.translations[errorKey];

        const checkout = await AdyenCheckout({
            clientKey: config.clientKey,
            environment: config.environment,
            paymentMethodsResponse: config.paymentMethods,
            amount: config.amount,
            countryCode: config.billingAddress.countryCode,
            locale: config.locale,
            onError: (err, component) => component.setStatus('error', {message: errorMsg}),
            onSubmit: (state, component) =>
                fetch(config.path.payments, {
                    method: 'POST',
                    headers: {'Content-Type': 'application/json'},
                    body: JSON.stringify(state.data)
                })
                    .then(r => r.json())
                    .then(res => {
                        if (res.resultCode === 'Authorised') {
                            window.location.href = `/${config.locale}/payment/adyen/${gatewayCode}/thanks`;
                        } else if (res.action) {
                            component.handleAction(res.action);
                        } else {
                            component.setStatus('error', {message: errorMsg});
                        }
                    })
        });

        const dropin = new Dropin(checkout);
        dropin.mount('#dropin-container');
    })();
</script>
</body>
</html>

Checkout & Payment Flow

The next section covers the live payment flow — from completing the order to processing the Drop-In response:

  1. Pick up the cart and add products.

  2. Enter addresses: Provide billing and shipping addresses for the order (we do not recommend using US or CA country codes at this time due to known handling issues).

  3. Select Shipping Method: Choose a shipment option.

  4. Select Payment Method: Choose your configured gateway (custom_adyen_gateway).

Finalize payment

You can either complete the payment directly (which will also complete the checkout after a successful transaction) or first place the order. Once the order is in the awaiting_payment state, proceed with the payment.

Access the Drop-In test view via the route defined in AdyenTestController:

GET /test/adyen/drop-in/{orderToken}

This will render the Drop-In component (using the example controller shown earlier) and prompt you to enter test card details. You can find the full list of Adyen test card numbers here: https://docs.adyen.com/development-resources/testing/test-card-numbers/

Handle Drop-In Response

  • On error, the transaction moves to failed (noted both in Sylius and the Adyen Dashboard), and the customer must retry the payment.

  • On success, Drop-In will redirect the customer to the confirmation URL provided by the plugin endpoint and Sylius will automatically transition the order to completed. In this case, you don’t need to call the complete endpoint separately.

Drop-In Configuration Data

Adyen’s Drop-In component initializes itself using the JSON returned by the headless endpoint:

GET /api/v2/shop/payment/adyen/{custom_adyen_gateway}/{orderToken}

Below is a representative example of the configuration payload:

{
  "billingAddress": {
    "firstName": "John",
    "lastName": "Doe",
    "countryCode": "NL",
    "province": null,
    "city": "Sample City",
    "postcode": "12345"
  },
  "paymentMethods": {
    "paymentMethods": [
      {
        "brands": ["visa", "mc", "amex"],
        "name": "Cards",
        "type": "scheme"
      }
    ],
    "storedPaymentMethods": []
  },
  "clientKey": "<yourClientKey>",
  "locale": "en_US",
  "environment": "test",
  "canBeStored": false,
  "amount": {
    "currency": "EUR",
    "value": 5000
  },
  "path": {
    "payments": "/en_US/payment/adyen/custom_adyen_gateway?tokenValue={orderToken}",
    "paymentDetails": "/en_US/payment/adyen/details?code=custom_adyen_gateway&tokenValue={orderToken}",
    "deleteToken": "/en_US/payment/adyen/custom_adyen_gateway/token/_REFERENCE_?tokenValue={orderToken}"
  },
  "translations": {
    "sylius_adyen.runtime.payment_failed_try_again": "Payment failed, please try again"
  }
}

Last updated

Was this helpful?