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.


1. Basic Checkout Steps

  1. Pick up the cart and add products.

  2. Addressing: 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).

  5. Complete Order:

    • POST /api/v2/shop/orders/{orderToken}/complete

    • On success, Sylius sets:

      • orderStatenew

      • checkoutStatecompleted

      • paymentStateawaiting_payment

    • Note: If Drop-In completes the payment immediately after selection, the order will automatically transition to completed, so calling this endpoint is optional.


2. Headless Payment with Adyen Drop-In

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.

Now that the order is completed but payment is pending, use the Drop-In to collect and submit payment details.

2.1. 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' => 'my_adyen_code',
        ]);
    }
}

2.2. 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>

Open the Drop-In test view by navigating to /test/adyen/{orderToken}. The Drop-In component will load and prompt you to enter test card details – see the full list of Adyen test card numbers here: https://docs.adyen.com/development-resources/testing/test-card-numbers/

2.3. 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.

2.4. 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"
  }
}

3. Manual Capture in Adyen Dashboard

  1. Log in to your Adyen test dashboard.

  2. Locate the payment record for your order.

  3. Click "See all payment actions", then Capture Payment.

  4. Confirm the displayed amount and Reference (no changes needed), then click Confirm.

  5. Verify in Sylius that paymentState transitions from awaiting_payment to paid. Note: this state change can take up to a minute as Adyen processes the capture and dispatches the webhook.

Last updated

Was this helpful?