Skip to main content

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​

MethodRoutePermissionSince
GET/order-typesPublic1.6.0
PUT/order-types/{slug}/force-disableManagement1.11.0
GET/schedule/{order_type}/hoursPublic1.6.0
GET/schedule/{order_type}/datesPublic1.6.0
GET/schedule/{order_type}/slotsPublic1.6.0
GET/schedule/{order_type}/statusPublic1.6.0
GET/delivery/configPublic1.7.0
POST/delivery/check-addressPublic1.7.0
POST/delivery/feePublic1.7.0
GET/delivery/minimumPublic1.7.0
POST/delivery/check-minimumPublic1.7.0
POST/cart/validatePublic1.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 whose end is earlier than start crosses 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 }. reason can be null, not_enabled, emergency_closed, force_disabled or out_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

FieldNotes
order_typeRequired.
distance_kmIf sent, it takes precedence over the address.
addressThe distance is calculated with Google.
cart_totalCart 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=delivery responds { order_type, amount, currency, currency_symbol, applies }.
  • POST /delivery/check-minimum with order_type and cart_subtotal responds { 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.

FieldRequiredNotes
order_typeYes
items[]No{product_id, variation_id, quantity, extra_selections[{category_id, extra_ids[]}], additional_comments}
addressNoTo check coverage.
distance_kmNoPreview only: avoids the Google call. POST /orders ignores it.
cart_subtotalNoUsed only if you do not send items.
fulfillment_date, fulfillment_timeNoSame 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​

CodeMeaning
schedule_selection_invalidThe 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_minimumThe minimum order is not reached.
out_of_delivery_rangeThe address is outside the coverage area.
product_unavailableProduct 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_exceededThe 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_extraThe extra is not active, does not belong to that category, the category does not apply to the product, or it is repeated.