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
Pick up the cart and add products.
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).
Select Shipping Method: Choose a shipment option.
Select Payment Method: Choose your configured gateway (
custom_adyen_gateway
).Complete Order:
POST /api/v2/shop/orders/{orderToken}/complete
On success, Sylius sets:
orderState
→new
checkoutState
→completed
paymentState
→awaiting_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
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 thecomplete
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
Log in to your Adyen test dashboard.
Locate the payment record for your order.
Click "See all payment actions", then Capture Payment.
Confirm the displayed amount and Reference (no changes needed), then click Confirm.
Verify in Sylius that
paymentState
transitions fromawaiting_payment
topaid
. Note: this state change can take up to a minute as Adyen processes the capture and dispatches the webhook.
Last updated
Was this helpful?