Hours, delivery and cart
All these routes are public, except the per-mode shutdown. Use them in the store or in an app to show the customer what they can order and when.
Routes
| Method | Route | Permission | Since |
|---|---|---|---|
| GET | /order-types | Public | 1.6.0 |
| PUT | /order-types/{slug}/force-disable | Management | 1.11.0 |
| GET | /schedule/{order_type}/hours | Public | 1.6.0 |
| GET | /schedule/{order_type}/dates | Public | 1.6.0 |
| GET | /schedule/{order_type}/slots | Public | 1.6.0 |
| GET | /schedule/{order_type}/status | Public | 1.6.0 |
| GET | /delivery/config | Public | 1.7.0 |
| POST | /delivery/check-address | Public | 1.7.0 |
| POST | /delivery/fee | Public | 1.7.0 |
| GET | /delivery/minimum | Public | 1.7.0 |
| POST | /delivery/check-minimum | Public | 1.7.0 |
| POST | /cart/validate | Public | 1.8.0 |
{order_type} is delivery or pickup. An unknown service mode responds 400 rest_invalid_param. Hours always use the time zone from WordPress Settings → General.
Service modes
GET /order-types returns a list with the status of each service mode:
{
"slug": "delivery", "label": "Delivery", "enabled": true, "force_disabled": false,
"open_now": true, "in_hours": true, "next_open": null,
"prep_time_minutes": 30, "slot_size_minutes": 30, "advance_days": 7,
"allow_now": true, "allow_asap": true, "max_per_slot": 8,
"date_selection_enabled": true, "time_selection_enabled": true
}
PUT /order-types/{slug}/force-disable with { "disabled": true } temporarily disables a service mode, like the app's "Temporarily disable" switch. Responds { slug, force_disabled }.
Hours and time slots
/hours: weekly hours,{ order_type, hours: { monday: [{start, end}], ..., sunday: [] } }. A range whoseendis earlier thanstartcrosses midnight./dates?days=14: upcoming days (1 to 90) with{ date, weekday, open, ranges_count }./slots?date=YYYY-MM-DD: the day's time slots, with capacity./status:{ open, reason, force_disabled, in_hours, next_open, order_type }.reasoncan benull,not_enabled,emergency_closed,force_disabledorout_of_hours.
curl "https://tienda.example.test/wp-json/notiwoo/v1/schedule/delivery/slots?date=2026-10-01"
{
"success": true,
"data": {
"date": "2026-10-01", "weekday": "thursday",
"ranges": [ { "start": "11:00", "end": "15:00" } ],
"slots": [
{ "time": "11:00", "label": "11:00 am", "starts_at": "2026-10-01T11:00:00-05:00", "available": true, "taken": 0, "max": 8 },
{ "time": "11:30", "label": "11:30 am", "starts_at": "2026-10-01T11:30:00-05:00", "available": false, "taken": 8, "max": 8 }
],
"asap_available": false, "now_available": false, "order_type": "delivery"
}
}
A slot is full when taken reaches max. If max is null, there is no limit. Orders in pending, on hold, accepting, processing and completed status count toward capacity.
Delivery configuration
GET /delivery/config returns the public configuration: store address and location, maximum distance, fee mode (fixed, per_km or distance by tiers), fee values, tiers, minimum order per service mode, currency, and the has_api_key and has_browser_map_key flags. It never returns the Google keys.
Check an address
POST /delivery/check-address with { "address": "..." }. The server calculates the driving distance with the Google Routes API and caches the result for one hour.
{ "deliverable": true, "reason": null, "distance_meters": 3200, "distance_km": 3.2, "fee": 5000, "fee_mode": "distance", "store_address": "Carrera 7 # 1-1, Bogotá", "max_distance_km": 15 }
It always responds 200. If there is no coverage, deliverable is false and reason explains why: out_of_range, empty_address, no_api_key, no_store_address, request_failed, invalid_response, api_error or http_<code>.
Calculate the fee
POST /delivery/fee
| Field | Notes |
|---|---|
order_type | Required. |
distance_km | If sent, it takes precedence over the address. |
address | The distance is calculated with Google. |
cart_total | Cart total. |
Responds { order_type, fee, mode, distance_meters, distance_km, distance_source, applies }. distance_source is param, geocode or none.
Fee rules:
- Fixed: always the same amount.
- Per kilometer: kilometers rounded up × amount per km, with a minimum of 1 km.
- By tiers: the first tier that contains the distance. A limit shared by two tiers belongs to the next one.
- Pickup: 0.
Errors: 422 notiwoo_geocode_failed (the address could not be located) and 400 notiwoo_invalid_order_type.
Minimum order
GET /delivery/minimum?order_type=deliveryresponds{ order_type, amount, currency, currency_symbol, applies }.POST /delivery/check-minimumwithorder_typeandcart_subtotalresponds{ order_type, valid, minimum, current, missing }.
Validate the cart
POST /cart/validate runs the same checks as the checkout in a single call: prices and extras, hours, minimum, coverage and availability. It always responds 200; problems arrive in data.errors.
| Field | Required | Notes |
|---|---|---|
order_type | Yes | |
items[] | No | {product_id, variation_id, quantity, extra_selections[{category_id, extra_ids[]}], additional_comments} |
address | No | To check coverage. |
distance_km | No | Preview only: avoids the Google call. POST /orders ignores it. |
cart_subtotal | No | Used only if you do not send items. |
fulfillment_date, fulfillment_time | No | Same as in POST /orders. |
curl -X POST https://tienda.example.test/wp-json/notiwoo/v1/cart/validate \
-H "Content-Type: application/json" \
-d '{
"order_type": "delivery",
"address": "Calle 10 # 20-30, Bogotá",
"fulfillment_date": "2026-10-01",
"fulfillment_time": "12:30",
"items": [ { "product_id": 120, "quantity": 2,
"extra_selections": [ { "category_id": 18, "extra_ids": [91] } ] } ]
}'
{
"success": true,
"data": {
"valid": false,
"errors": [
{ "code": "below_minimum", "message": "Cart total below the minimum.",
"details": { "minimum": 30000, "current": 28000, "missing": 2000 } }
],
"checks": {
"pricing": { "valid": true, "subtotal": "28000.00", "items": [ { "index": 0, "unit_total": "14000.00", "line_total": "28000.00" } ] },
"schedule": { "valid": true, "reason": null },
"minimum": { "valid": false, "minimum": 30000, "current": 28000, "missing": 2000 },
"delivery": { "deliverable": true, "distance_km": 3.2, "fee": 5000 },
"products": [ { "product_id": 120, "available": true, "reasons": [] } ]
},
"canonical_subtotal": 28000
}
}
canonical_subtotal is the subtotal calculated by the server. Display it instead of a total calculated on the client.
errors[] codes
| Code | Meaning |
|---|---|
schedule_selection_invalid | The day or time cannot be chosen. details.reason gives the reason: out_of_hours, emergency_closed, date_required, time_required, slot_unavailable, date_out_of_range, asap_not_allowed… |
below_minimum | The minimum order is not reached. |
out_of_delivery_range | The address is outside the coverage area. |
product_unavailable | Product not found, not published, disabled or out of stock. |
notiwoo_product_out_of_stock, notiwoo_invalid_quantity, notiwoo_product_not_purchasable… | Problems with an item. details.item_index says which one. |
notiwoo_extra_minimum_not_met, notiwoo_extra_maximum_exceeded | The chosen extras do not meet their category's minimum or maximum. |
notiwoo_extra_disabled, notiwoo_extra_wrong_category, notiwoo_extra_category_not_allowed, notiwoo_duplicate_extra | The extra is not active, does not belong to that category, the category does not apply to the product, or it is repeated. |