Horarios, domicilio y carrito
Todas estas rutas son públicas, salvo el cierre por modalidad. Úsalas en la tienda o en una app para mostrar al cliente qué puede pedir y cuándo.
Rutas
| Método | Ruta | Permiso | Desde |
|---|---|---|---|
| GET | /order-types | Público | 1.6.0 |
| PUT | /order-types/{slug}/force-disable | Gestión | 1.11.0 |
| GET | /schedule/{order_type}/hours | Público | 1.6.0 |
| GET | /schedule/{order_type}/dates | Público | 1.6.0 |
| GET | /schedule/{order_type}/slots | Público | 1.6.0 |
| GET | /schedule/{order_type}/status | Público | 1.6.0 |
| GET | /delivery/config | Público | 1.7.0 |
| POST | /delivery/check-address | Público | 1.7.0 |
| POST | /delivery/fee | Público | 1.7.0 |
| GET | /delivery/minimum | Público | 1.7.0 |
| POST | /delivery/check-minimum | Público | 1.7.0 |
| POST | /cart/validate | Público | 1.8.0 |
{order_type} es delivery (domicilio) o pickup (recogida). Una modalidad desconocida responde 400 rest_invalid_param. Los horarios usan siempre la zona horaria de Ajustes → Generales de WordPress.
Modalidades
GET /order-types devuelve una lista con el estado de cada modalidad:
{
"slug": "delivery", "label": "Domicilio", "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 con { "disabled": true } desactiva temporalmente una modalidad, como el interruptor «Desactivar temporalmente» de la app. Responde { slug, force_disabled }.
Horarios y franjas
/hours: horario semanal,{ order_type, hours: { monday: [{start, end}], ..., sunday: [] } }. Un tramo cuyoendes menor questartcruza la medianoche./dates?days=14: próximos días (1 a 90) con{ date, weekday, open, ranges_count }./slots?date=AAAA-MM-DD: franjas del día, con cupo./status:{ open, reason, force_disabled, in_hours, next_open, order_type }.reasonpuede sernull,not_enabled,emergency_closed,force_disableduout_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"
}
}
Una franja se llena cuando taken alcanza max. Si max es null, no hay límite. Cuentan para el cupo los pedidos en estado pendiente, en espera, aceptando, en proceso y completado.
Configuración de domicilio
GET /delivery/config devuelve la configuración pública: dirección y ubicación de la tienda, distancia máxima, modo de tarifa (fixed, per_km o distance por rangos), valores de tarifa, rangos, pedido mínimo por modalidad, moneda, y los indicadores has_api_key y has_browser_map_key. Nunca devuelve las claves de Google.
Comprobar una dirección
POST /delivery/check-address con { "address": "..." }. El servidor calcula la distancia en coche con Google Routes API y guarda el resultado en caché durante una hora.
{ "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 }
Responde siempre 200. Si no hay cobertura, deliverable es false y reason explica el motivo: out_of_range, empty_address, no_api_key, no_store_address, request_failed, invalid_response, api_error o http_<código>.
Calcular la tarifa
POST /delivery/fee
| Campo | Notas |
|---|---|
order_type | Obligatorio. |
distance_km | Si lo envías, tiene prioridad sobre la dirección. |
address | Se calcula la distancia con Google. |
cart_total | Total del carrito. |
Responde { order_type, fee, mode, distance_meters, distance_km, distance_source, applies }. distance_source vale param, geocode o none.
Reglas de tarifa:
- Fija: siempre el mismo valor.
- Por kilómetro: kilómetros redondeados hacia arriba × valor por km, con un mínimo de 1 km.
- Por rangos: el primer rango que contiene la distancia. Un límite compartido entre dos rangos pertenece al siguiente.
- Recogida: 0.
Errores: 422 notiwoo_geocode_failed (no se pudo ubicar la dirección) y 400 notiwoo_invalid_order_type.
Pedido mínimo
GET /delivery/minimum?order_type=deliveryresponde{ order_type, amount, currency, currency_symbol, applies }.POST /delivery/check-minimumconorder_typeycart_subtotalresponde{ order_type, valid, minimum, current, missing }.
Validar el carrito
POST /cart/validate hace en una sola llamada las mismas comprobaciones que el checkout: precios y extras, horario, mínimo, cobertura y disponibilidad. Responde siempre 200; los problemas llegan en data.errors.
| Campo | Obligatorio | Notas |
|---|---|---|
order_type | Sí | |
items[] | No | {product_id, variation_id, quantity, extra_selections[{category_id, extra_ids[]}], additional_comments} |
address | No | Para comprobar la cobertura. |
distance_km | No | Solo para vista previa: evita la llamada a Google. POST /orders no lo tiene en cuenta. |
cart_subtotal | No | Solo se usa si no envías items. |
fulfillment_date, fulfillment_time | No | Igual que en 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": "Total del carrito por debajo del mínimo.",
"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 es el subtotal calculado por el servidor. Muéstralo en lugar de un total calculado en el cliente.
Códigos de errors[]
| Código | Significado |
|---|---|
schedule_selection_invalid | El día u hora no se pueden elegir. details.reason precisa el motivo: out_of_hours, emergency_closed, date_required, time_required, slot_unavailable, date_out_of_range, asap_not_allowed… |
below_minimum | No alcanza el pedido mínimo. |
out_of_delivery_range | La dirección está fuera de la zona de cobertura. |
product_unavailable | Producto no encontrado, no publicado, desactivado o agotado. |
notiwoo_product_out_of_stock, notiwoo_invalid_quantity, notiwoo_product_not_purchasable… | Problemas de un artículo. details.item_index indica cuál. |
notiwoo_extra_minimum_not_met, notiwoo_extra_maximum_exceeded | Los extras elegidos no respetan el mínimo o el máximo de su categoría. |
notiwoo_extra_disabled, notiwoo_extra_wrong_category, notiwoo_extra_category_not_allowed, notiwoo_duplicate_extra | El extra no está activo, no pertenece a esa categoría, la categoría no aplica al producto, o está repetido. |