Delivery moments module
Module id: delivery_slot. Dependencies: checkout (and through it
cart, catalog, customer).
The shopper picks the day and the time window their order is delivered in, on the checkout. For a shop that delivers itself (a caterer, a florist, a grocer with a van) and has to know who is home when. The shop sets fixed windows per weekday, a daily cut-off ("order before 14:00, delivered tomorrow"), how far ahead can be booked and the days it does not deliver. The moment picked is frozen onto the order and shown wherever the order's delivery is: the admin, the mails, the thank-you page, the customer's account and the packing slip.
The module is off on every website until it is switched on in the Modules tab. The sidebar item (Delivery moments, Commerce section, next to Holiday closure) opens its settings. Editors can use it, like the holiday closure: when a shop delivers is day-to-day work, not configuration.
Switching it on changes nothing by itself. Until the shop has set at least one window, the checkout asks nothing, so turning the module on before the schedule is filled in cannot leave a checkout nobody can complete.
What operators set
The screen has three cards: Weekly schedule, Ordering rules and Closed dates.
| Setting | Default | What it does |
|---|---|---|
| Weekly schedule | none | Time windows per day of the week, as From and Until (09:00 to 12:00). A day can have several. Windows on one day may touch (09:00-12:00 and 12:00-15:00) but not overlap. A day without windows has no delivery. At most 70 windows. |
| Order before | 14:00 | The cut-off. An order placed from this time on starts one day later. |
| Lead time (days) | 1 | 0 to 30. From an order placed before the cut-off to the first delivery day. 0 = same day. |
| Bookable ahead (days) | 14 | 1 to 90. The last day a shopper can pick, counted from today. |
| Closed dates | none | Extra days without delivery: public holidays, a day off. The holiday closure is left out already and does not need repeating here. |
Saving checks every row first. A window that ends before it starts, or overlaps another window on the same day, is refused on its own row and nothing is saved until it is put right.
The settings are one row per website in delivery_slot_settings, created
the first time the screen is saved. No row means no windows.
The cut-off, worked through
A shop delivering Monday to Friday 09:00-12:00 and 13:00-17:00, Saturday 09:00-12:00, never on Sunday. Order before 14:00, Lead time 1 (the defaults):
| Order placed | First day offered | Why |
|---|---|---|
| Monday 19 October, 13:59 | Tuesday 20 October | Before the cut-off: today + 1. |
| Monday 19 October, 14:00 | Wednesday 21 October | From the cut-off on: one day later. |
| Friday 23 October, 15:00 | Monday 26 October | After the cut-off the first day is Sunday, which has no windows. Saturday is too soon. |
| Saturday 24 October, 11:00 | Monday 26 October | Before the cut-off, but tomorrow is Sunday. |
The screen spells the pair out under the fields as you type: "Orders placed before 14:00 are delivered from tomorrow; after 14:00 from the day after."
With Lead time 0 the shop delivers the same day, in the windows that have not started yet. An order on Monday at 10:30 can still pick Monday 13:00-17:00, but not 09:00-12:00. From 14:00 the first day is Tuesday.
The clock is Europe/Amsterdam, always, whatever timezone the server runs in. A cut-off of 14:00 is 14:00 in the Netherlands and Belgium.
Which days are offered
From the first day (above) up to and including today + Bookable ahead, every day that:
- has windows on its weekday,
- is not one of the Closed dates,
- is not a day of the holiday closure (Holiday closure screen).
A closed day moves the end of the range one day further. A closed date or a day of the holiday closure does not eat into the days that can be booked: a three-week summer closure with 14 days bookable ahead still leaves two weeks to book after it, so a shop that keeps taking orders during its break has a checkout that can be completed. A weekday without windows (Sunday, above) is an ordinary day without delivery and does not move it.
Keep Bookable ahead well above Lead time. The range runs from the first day to today + Bookable ahead, and the cut-off moves only the first day. With a lead time of 3 and 3 days ahead, an order before the cut-off would get one day and an order after it none, so the screen refuses Bookable ahead that is not more than Lead time. Even one day more can land on a weekday without windows, such as a Sunday; a week more than the lead time keeps every weekday in range.
At the checkout
A Delivery moment fieldset after the shipping options: a Day list, each day with its weekday (donderdag 22 oktober 2026), and the Time windows of that day as radio buttons. No window is ticked for the shopper: a moment someone has to be home for is chosen, not defaulted.
Required. An order that is asked for a moment is not placed without one:
| Situation | What the customer sees |
|---|---|
| No window picked | "Pick a delivery moment." |
| The moment passed its cut-off while the page was open | "This delivery moment is no longer available. Pick another one." |
| The shop asks, but no day falls in the range | The fieldset says there are no delivery moments available right now; the order cannot be placed. |
The submit looks the moment up in the schedule again with its own clock, so a moment shown at 13:58 and submitted at 14:01 is refused rather than promised.
Deliveries only. An order with a pickup option (from the shipping module) chooses nothing and stores nothing, even if a moment was posted. Picking pickup hides the fieldset; switching back to a delivery brings it in. Before the shopper picks an option, the checkout goes by the option it would charge, the cheapest, which is the one the default template ticks. With the shipping module off, every order is a delivery.
Live. The fieldset is a live region
(data-cms-checkout-live="delivery-slot", see
checkout.md).
checkout_live.js redraws it when the day changes, to show that day's
windows, and when the shipping choice changes, so pickup hides it.
Without JavaScript the windows shown are those of the day selected when the page was drawn. A shopper who picks another day and a window that day does not have is refused on submit ("no longer available") and gets the page back with the windows of the day they picked. A shop with the same windows on every delivery day never sees this.
No capacity. Any number of orders can pick the same window.
On the order
The moment is frozen on the order as delivery_date,
delivery_window_start and delivery_window_end. All three are null on an
order without one: a pickup, a shop without the module, a bol.com order,
an order from before the module. Like the dispatch date of a holiday
closure it is frozen, not recomputed: changing the windows later does
not move a moment the shop already promised.
Where the moment shows up
| Where | What |
|---|---|
| Orders list (admin) | Under the order date: delivery Thu 22 Oct, 13:00-17:00. |
| Order detail (admin) | Delivery in the summary. |
| Order confirmation email (HTML and text) | Delivery on Thursday, October 22, 2026 between 13:00 and 17:00, in the order's language. |
| New-order mail to the shop | The same sentence. |
| Thank-you page | The same sentence. |
| The customer's order page (account) | The same sentence, above the delivery address. |
| Packing slip | Deliver on, with weekday, date and window, in the website's language. |
The storefront and mail strings ship in English, Dutch, German and French.
What a theme has to do
Nothing, for a theme that uses the bundled checkout page: it already calls the picker, and the bundled thank-you page, mails, account order page and packing slip already print the moment.
A theme that overrides checkout/index.html.twig must add the picker,
inside the checkout form, where it should appear (the default puts it
after the shipping options):
{{ cms_delivery_slot_picker() }}
Without it the shopper has nothing to pick, and once the shop has set its
windows every delivery order is refused with "Pick a delivery moment."
Call it unconditionally. It reads the page's own delivery_slot and
errors, prints the field error itself, and always renders the
live-region wrapper, empty when nothing is asked, so that switching from
pickup to a delivery has somewhere to draw the picker.
A theme that overrides the thank-you page, the order mails, the account order page or the packing slip prints the moment with:
{% if order.hasDeliverySlot %}
<p>{{ cms_delivery_moment(order) }}</p>
{% endif %}
In a mail pass the order's locale: cms_delivery_moment(order, locale).
For its own markup, a theme has the checkout's delivery_slot variable
and the order getters hasDeliverySlot, deliveryDate,
deliveryWindowStart and deliveryWindowEnd. Signatures are in
twig-helpers.md.
Overridable templates
| Template | What |
|---|---|
checkout/_delivery_slot.html.twig |
The picker. Inputs: delivery_slot (null when the order is not asked) and errors. Keep the wrapper with data-cms-checkout-live="delivery-slot" even when delivery_slot is null, and the field names delivery_date (2026-10-22) and delivery_window (09:00-12:00). |
The moment in the mails, the thank-you page, the account page and the packing slip is part of those templates; see checkout.md.
Admin API
| Route | Method | What |
|---|---|---|
/api/admin/delivery-slots/settings |
GET, PATCH | The active website's settings. Editors and above; 404 while the module is off. |
{
"windows": [
{"weekday": 1, "start": "09:00", "end": "12:00"},
{"weekday": 1, "start": "13:00", "end": "17:00"}
],
"cutoff_time": "14:00",
"lead_days": 1,
"horizon_days": 14,
"closed_dates": ["2026-12-25", "2026-12-26"]
}
weekday is ISO: 1 is Monday, 7 is Sunday. PATCH answers with the
settings as stored (sorted, duplicates dropped); a field left out is kept.
A refusal is a 422 that names the field, down to one row of the schedule
(windows.3.end).
Things this module does NOT do
- No capacity per window. Every window takes any number of orders.
- No surcharge per window. An evening window costs what a morning one does; shipping is priced as before.
- No schedule per carrier. One schedule per website, for every delivery option.
- No "deliver on" filter in the Orders list. The moment is on each row, but the list cannot be narrowed to one day.
- No timezone per website. The clock is Europe/Amsterdam for every shop.
- No changing the moment afterwards. The admin shows it but cannot move it.