Alex Reservations — Bot API
REST API designed for conversational AI assistants (Instagram DM, WhatsApp, Telegram, Messenger…). The bot can check real-time availability, create bookings honouring the restaurant's true capacity, look up bookings by phone, modify or cancel them — all in a single round-trip per operation.
Base URL: https://your-domain.com/wp-json/alexr-bot/v1
This API is separate from the platform integration API at /wp-json/alexr/v1:
Platform API (alexr/v1) |
Bot API (alexr-bot/v1) |
|
|---|---|---|
| Use case | External platforms (TheFork, OpenTable, Zapier) syncing existing reservations | Conversational bots creating reservations from scratch |
| Real availability check | No | Yes |
| Bound to a widget | No | Yes (each key has its own widget_id) |
| Notifications by default | OFF | ON |
| Modify endpoint | No | Yes |
| Module | Included | Requires the API Bot module |
| Key storage | api_keys table |
api_keys_bot table (independent) |
| Log storage | api_logs table |
api_logs_bot table (independent) |
Authentication
All requests must include a valid bot API key. Two methods are supported:
X-API-Key: your-api-key-here
or
Authorization: Bearer your-api-key-here
Each bot API key is bound to one restaurant and one widget (which determines the available services, party-size limits, online-booking rules, etc.). The bot does not need to send widget_id on every request — it is resolved from the key.
Authentication errors
| HTTP | Code | Message |
|---|---|---|
| 401 | MISSING_API_KEY |
No API key provided. |
| 401 | INVALID_API_KEY |
Key not found or inactive. |
Response envelope
All responses follow a consistent JSON envelope.
Success:
{ "success": true, "data": { ... } }
Error:
{ "success": false, "error": { "code": "SLOT_UNAVAILABLE", "message": "...", "details": { ... } } }
The code field is machine-readable; the message is human-readable. details is optional and may include validation errors, alternative dates, etc.
Error codes
| Code | Typical HTTP | Meaning |
|---|---|---|
MISSING_API_KEY |
401 | No API key header. |
INVALID_API_KEY |
401 | Key not found or deactivated. |
RESTAURANT_NOT_FOUND |
404 | Restaurant inactive or missing. |
WIDGET_NOT_FOUND |
400 / 404 | Widget not configured or doesn't belong to the restaurant. |
SERVICE_NOT_FOUND |
404 | Shift/Event not found or not part of the widget. |
BOOKING_NOT_FOUND |
404 | UUID does not match any booking for this restaurant. |
VALIDATION_FAILED |
400 | Missing or invalid fields. details lists them. |
INVALID_DATE |
400 | Date not in YYYY-MM-DD format or unreal date. |
INVALID_TIME |
400 | Time not in HH:MM 24h format. |
DATE_CLOSED |
409 | Restaurant fully closed on that date. |
SLOT_CLOSED |
409 | Service has the slot blocked. |
SLOT_UNAVAILABLE |
409 | No real availability for the requested combination. Returns alternative_dates. |
BOOKING_NOT_MODIFIABLE |
409 | Booking is cancelled, finished, paid, denied or no-show. |
Endpoints
1. GET /restaurant — bot context
Returns metadata useful for priming the bot's prompt: restaurant name, timezone, language, services offered through the widget, party-size limits, reservation policy and upcoming closed dates.
curl https://your-domain.com/wp-json/alexr-bot/v1/restaurant \
-H "X-API-Key: ...."
Response (200):
{
"success": true,
"data": {
"restaurant": {
"id": 1,
"name": "Raffaella Cucina Napoletana",
"timezone": "America/Santiago",
"language": "es",
"phone": "+56...",
"address": "...",
"reservation_policy": "Free cancellation up to 2h before..."
},
"widget": {
"id": 42,
"name": "Instagram Bot Widget",
"guests_min": 1,
"guests_max": 12
},
"services": [
{
"id": 101,
"name": "Lunch",
"type": "shift",
"public_notes": null,
"min_guests": 1,
"max_guests": 8,
"availability_type": "volume_total"
}
],
"closed_dates": ["2026-06-15", "2026-06-22"]
}
}
2. GET /availability — slots for a date
Real-time availability for a single date, taking into account current bookings, table assignments and the service rules.
| Param | Required | Description |
|---|---|---|
date |
yes | YYYY-MM-DD |
party_size |
yes | integer ≥ 1 |
service_id |
no | numeric ID, or all (default) |
widget_id |
no | override the API key's widget (rarely needed) |
curl "https://your-domain.com/wp-json/alexr-bot/v1/availability?date=2026-06-15&party_size=4" \
-H "X-API-Key: ...."
Success (200) — slots found:
{
"success": true,
"data": {
"date": "2026-06-15",
"party_size": 4,
"available": true,
"slots": [
{ "time": "13:00", "time_seconds": 46800, "service_id": 101, "service_name": "Lunch", "service_type": "shift", "duration_minutes": 90 },
{ "time": "13:30", "time_seconds": 48600, "service_id": 101, "service_name": "Lunch", "service_type": "shift", "duration_minutes": 90 },
{ "time": "20:00", "time_seconds": 72000, "service_id": 102, "service_name": "Dinner", "service_type": "shift", "duration_minutes": 90 }
]
}
}
Success (200) — no slots, with alternatives:
{
"success": true,
"data": {
"date": "2026-06-15",
"party_size": 4,
"available": false,
"slots": [],
"alternative_dates": [
{ "date": "2026-06-14", "slots_count": 8 },
{ "date": "2026-06-13", "slots_count": 12 },
{ "date": "2026-06-16", "slots_count": 6 },
{ "date": "2026-06-17", "slots_count": 9 }
]
}
}
Success (200) — restaurant closed that day:
{
"success": true,
"data": {
"date": "2026-06-15",
"party_size": 4,
"available": false,
"reason": "DATE_CLOSED",
"slots": [],
"alternative_dates": [ ... ]
}
}
3. GET /availability/month — days with availability in a range
Useful when the user asks "what days do you have open this week/month?".
| Param | Required | Description |
|---|---|---|
start_date |
yes | YYYY-MM-DD |
end_date |
yes | YYYY-MM-DD (must be ≥ start_date) |
service_id |
no | numeric ID, or all (default) |
widget_id |
no | override |
curl "https://your-domain.com/wp-json/alexr-bot/v1/availability/month?start_date=2026-06-01&end_date=2026-06-30" \
-H "X-API-Key: ...."
Success (200):
{
"success": true,
"data": {
"start_date": "2026-06-01",
"end_date": "2026-06-30",
"days_available": ["2026-06-01", "2026-06-02", "2026-06-05", "..."],
"days_with_services": {
"2026-06-01": [101, 102],
"2026-06-02": [101]
}
}
}
days_with_services maps each available date to the IDs of the services that offer slots that day. days_available is a flat sorted list of dates.
4. POST /bookings — create a reservation (with real availability check)
Performs an atomic create-with-availability-check. If the slot is no longer free (or the date is closed, or the service is blocked online), responds with 409 SLOT_UNAVAILABLE and a details.alternative_dates array.
Body (JSON):
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
date |
string | yes | — | YYYY-MM-DD |
time |
string | yes | — | HH:MM (24h) |
party_size |
int | yes | — | ≥ 1 |
customer_name |
string | yes | — | First name (display name) |
customer_phone |
string | yes | — | National or international phone |
customer_last_name |
string | no | "" | Surname |
customer_email |
string | no | synthetic | If omitted, generated as {platform}+{phone}@fake |
customer_dial_code |
string | no | "" | Optional dial-code prefix (e.g. +56) |
service_id |
int | no | auto-detect | Shift or event ID. If omitted, the API matches the slot to one of the widget's services. |
notes |
string | no | null | Free text — allergies, occasion, etc. |
table_ids |
int[] | no | auto-assign | Specific table id(s) to seat the booking on (from GET /tables). For POS walk-ins already seated. See note below. |
send_notifications |
bool | no | true | If false, no email/SMS/WhatsApp is sent. |
widget_id |
int | no | API key default | Rarely needed. |
table_ids(walk-ins). When you passtable_ids, Alex Reservations does not auto-assign a table and the availability/cover check is skipped — the POS asserts the guests are physically seated, so the booking is created on exactly those tables even if the shift looks "full". The floor plan then matches the room. Ids must belong to the restaurant (otherwise400 INVALID_TABLE); table conflicts are not blocked (the floor plan shows the overlap). Accepts a JSON array ([12,13]) or a comma-separated string.
curl -X POST https://your-domain.com/wp-json/alexr-bot/v1/bookings \
-H "Content-Type: application/json" \
-H "X-API-Key: ...." \
-d '{
"date": "2026-06-15",
"time": "20:00",
"party_size": 4,
"customer_name": "Juan",
"customer_last_name": "Pérez",
"customer_phone": "+56912345678",
"notes": "Allergic to nuts"
}'
Success (201):
{
"success": true,
"data": {
"reservation_id": "bo_abc...",
"booking_id": 456,
"uuid": "bo_abc...",
"status": "booked",
"restaurant_id": 1,
"widget_id": 42,
"service_id": 102,
"service_name": "Dinner",
"date": "2026-06-15",
"time": "20:00",
"time_seconds": 72000,
"party_size": 4,
"duration_minutes": 90,
"customer_name": "Juan Pérez",
"customer_first_name": "Juan",
"customer_last_name": "Pérez",
"customer_email": "instagram+56912345678@fake",
"customer_phone": "+56912345678",
"customer_dial_code": "",
"notes": "Allergic to nuts",
"source": "instagram",
"language": "es",
"created_at": "2026-05-26 15:42:01",
"tables": [
{ "id": 12, "name": "7", "area_id": 2, "area_name": "Interior" }
]
}
}
tableslists the table(s) assigned to the booking (with their area), or[]if none is assigned yet. Present in every booking payload (create, read, search and the by-date list).
Idempotency: if the same restaurant + email + date + time + party_size was already booked successfully, the original booking is returned with "duplicate": true in data. No second booking is created.
Error — no availability (409):
{
"success": false,
"error": {
"code": "SLOT_UNAVAILABLE",
"message": "No availability for the requested date, time and party size.",
"details": {
"alternative_dates": [
{ "date": "2026-06-14", "slots_count": 8 },
{ "date": "2026-06-16", "slots_count": 6 }
]
}
}
}
5. GET /bookings/{uuid} — read a booking
curl https://your-domain.com/wp-json/alexr-bot/v1/bookings/bo_abc... \
-H "X-API-Key: ...."
Returns the same payload shape as the create response. 404 BOOKING_NOT_FOUND if the UUID is unknown or belongs to a different restaurant.
6. GET /bookings — search by phone or list a whole day
The /bookings endpoint serves two read modes depending on the query string:
| Param | Required | Default | Description |
|---|---|---|---|
phone |
one of phone/date |
— | Phone number, matched exactly against booking.phone |
date |
one of phone/date |
— | YYYY-MM-DD. Lists all bookings of that day |
limit |
no | 5 | (phone mode only) 1–20 results |
include_past |
no | false | (phone mode only) If true, also returns past bookings |
If date is present it takes precedence and phone is ignored. If neither is sent → 400 VALIDATION_FAILED.
6a. Search by phone
Useful when the user identifies themselves by phone instead of a reservation code.
curl "https://your-domain.com/wp-json/alexr-bot/v1/bookings?phone=%2B56912345678&limit=3" \
-H "X-API-Key: ...."
Response (200):
{
"success": true,
"data": {
"count": 2,
"bookings": [
{ "reservation_id": "bo_...", "date": "2026-06-15", "time": "20:00", "status": "booked", ... },
{ "reservation_id": "bo_...", "date": "2026-06-22", "time": "13:00", "status": "pending", ... }
]
}
}
Sorted by date desc.
6b. List a whole day — ?date=YYYY-MM-DD
Returns every booking of the day, in all statuses (except the internal selected and deleted), each with its assigned tables. Use this to see which tables are already taken/reserved before seating a walk-in, so you don't hand out a table that belongs to a pending or confirmed reservation.
curl "https://your-domain.com/wp-json/alexr-bot/v1/bookings?date=2026-06-15" \
-H "X-API-Key: ...."
Response (200):
{
"success": true,
"data": {
"date": "2026-06-15",
"count": 2,
"bookings": [
{
"reservation_id": "bo_...", "date": "2026-06-15", "time": "13:00",
"status": "pending", "party_size": 2, "customer_name": "Ana López",
"tables": [ { "id": 12, "name": "7", "area_id": 2, "area_name": "Interior" } ]
},
{
"reservation_id": "bo_...", "date": "2026-06-15", "time": "20:00",
"status": "booked", "party_size": 4, "customer_name": "Juan Pérez",
"tables": []
}
]
}
}
Each booking has the same structure as GET /bookings/{uuid}. Sorted by time asc. Invalid date → 400 VALIDATION_FAILED.
7. PATCH / PUT /bookings/{uuid} — modify
Partial update. Send only the fields the user wants to change. Both PATCH and PUT are accepted.
| Field | Type | Description |
|---|---|---|
date |
string | New YYYY-MM-DD |
time |
string | New HH:MM |
party_size |
int | New party size |
notes |
string | Replace notes |
customer_name |
string | Update first name |
customer_last_name |
string | |
customer_phone |
string | |
customer_email |
string | |
table_ids |
int[] | Reassign the booking to specific table(s) — e.g. a waiter moving the reservation to another table in the POS. Overrides auto-assignment; applied even when date/time/party don't change. Empty array [] clears the tables. |
send_notifications |
bool | Default true — sends a modification email |
If date, time or party_size changes, availability is re-validated against the same service (excluding the current booking from the count). Conflicts respond with 409 SLOT_UNAVAILABLE and alternative_dates.
When table_ids is sent, those tables are assigned directly (no availability re-check for the table move) and ids must belong to the restaurant (400 INVALID_TABLE). A tables changed entry is recorded in the audit log.
Bookings in status cancelled, denied, no-show, finished or paid can't be modified (409 BOOKING_NOT_MODIFIABLE).
curl -X PATCH https://your-domain.com/wp-json/alexr-bot/v1/bookings/bo_abc... \
-H "Content-Type: application/json" \
-H "X-API-Key: ...." \
-d '{ "time": "20:30", "party_size": 5 }'
Response (200):
{
"success": true,
"data": {
"reservation_id": "bo_abc...",
"status": "booked",
"date": "2026-06-15",
"time": "20:30",
"party_size": 5,
...,
"old_date": "2026-06-15",
"old_time": 72000,
"old_party": 4
}
}
8. POST /bookings/{uuid}/cancel — cancel
| Field | Type | Description |
|---|---|---|
reason |
string (optional) | Stored in booking.cancelReason |
send_notifications |
bool (optional, default true) | If false, no cancellation emails |
curl -X POST https://your-domain.com/wp-json/alexr-bot/v1/bookings/bo_abc.../cancel \
-H "Content-Type: application/json" \
-H "X-API-Key: ...." \
-d '{ "reason": "Customer requested cancellation via Instagram" }'
Response (200):
{
"success": true,
"data": {
"reservation_id": "bo_abc...",
"status": "cancelled",
"date": "2026-06-15",
"time": "20:00",
...
}
}
If the booking was already cancelled, the response still has success: true and includes "message": "Booking is already cancelled.".
9. PATCH /bookings/{uuid}/status — change status (seated / finished / no-show)
Operational status change driven by your POS / host app, so the floor plan and statistics stay in sync — e.g. when guests are seated, when the table is closed/paid out, or when nobody showed up.
| Param | Required | Description |
|---|---|---|
status |
yes | One of seated, finished, no-show |
curl -X PATCH https://your-domain.com/wp-json/alexr-bot/v1/bookings/bo_abc.../status \
-H "X-API-Key: ...." \
-H "Content-Type: application/json" \
-d '{ "status": "seated" }'
Response (200): the updated booking (same shape as GET /bookings/{uuid}), now with the new status.
Notes
- No customer notifications are sent — this is an operational change. An audit-log entry is recorded ("Automation Bot").
-
no-show(likecancelled) immediately frees the table on the floor plan and in availability. -
seatedkeeps the table occupied (the guests are there). -
finishedmarks the reservation as completed for statistics; the table stays reserved for the booking's time window (Alex Reservations countsfinishedas occupying its slot). - A
cancelled/deniedbooking cannot change status →409 BOOKING_NOT_MODIFIABLE. - Any other
statusvalue →400 VALIDATION_FAILED(allowed values are returned inerror.details.allowed). - Idempotent: setting the status it already has returns
200with"message": "Booking already has this status.".
10. GET /tables — list tables
Returns every table of the restaurant with its id, name, area/zone and capacity. Use it to keep a mapping between your POS table names and the Alex Reservations table IDs (the same IDs returned in each booking's tables field).
curl "https://your-domain.com/wp-json/alexr-bot/v1/tables" \
-H "X-API-Key: ...."
Response (200):
{
"success": true,
"data": {
"count": 3,
"tables": [
{ "id": 12, "name": "7", "area_id": 2, "area_name": "Interior", "min_seats": 2, "max_seats": 4 },
{ "id": 13, "name": "EXT-1", "area_id": 5, "area_name": "Terrace", "min_seats": 2, "max_seats": 4 },
{ "id": 14, "name": "16", "area_id": 2, "area_name": "Interior", "min_seats": 3, "max_seats": 5 }
]
}
}
| Field | Description |
|---|---|
id |
Internal table ID (matches the IDs in a booking's tables) |
name |
Table name/label as shown on the floor plan |
area_id / area_name |
Area/zone the table belongs to (null if none) |
min_seats / max_seats |
Capacity range of the table |
Returns all physical tables of the restaurant (table combinations are not included).
How it works under the hood
Real availability
POST /bookings calls the same Service::isAvailable() method used by the public reservation widget. This means:
- Closed days are honoured.
- Closed slots within an open day are honoured.
- The service's
availability_typeis honoured (volume_total,tables,specific_tables,tables_schedule,tables_layouts). - Tables are assigned automatically when the availability type requires it.
- The service's online-bookings configuration is honoured.
Service auto-detection
If you don't send service_id in POST /bookings, the API walks the widget's services and picks the one whose bookable slots include the requested time. Sending service_id explicitly is faster and unambiguous.
Status of new bookings
The status (pending or booked) is decided by the service's getBookingStatusForNewReservation() method — the same rule the widget applies. Some services force pending for specific tables, which is honoured.
Customer matching
- If a customer with the same email exists for the restaurant, the booking is linked to it.
- If no email is supplied, a synthetic one is built as
{platform}+{phone}@fakewhereplatformis the API key'splatformfield (instagram,whatsapp_bot, …). This mirrors the convention already used by WhatsApp-created customers. - If still no customer exists, one is created automatically.
Notifications
By default, all create / modify / cancel operations send the configured emails, SMS, WhatsApp and dashboard notifications. Pass send_notifications: false to suppress them — useful when the bot confirms in its own chat thread.
Idempotency
A SHA-256 hash of restaurant | email | date | time | party_size is stored in api_logs_bot.idempotency_key. A second identical request returns the original booking with "duplicate": true in data (HTTP 200).
Tenant isolation
Every read / modify / cancel verifies that the booking's restaurant_id matches the API key's restaurant_id. Mismatches respond 404 BOOKING_NOT_FOUND to avoid leaking existence of bookings from other restaurants.
Source tracking
Bookings created by this API get booking.source = <api_key.platform> (e.g. instagram, whatsapp_bot) and booking.widget_id = <api_key.widget_id>. This lets the dashboard segment statistics by origin.
API key setup
Bot API keys live in the api_keys_bot table. They are managed from the dashboard under Settings → Integrations → API Keys (Bot) when the API Bot module is enabled.
Required to create a key:
- Restaurant
- Widget — determines which services, party limits and rules apply to the bot
- Platform name —
instagram,whatsapp_bot,telegram_bot… Used both as the bookingsourceand as the prefix for synthetic emails. - A human-readable name (e.g.
"RaffaellIA Instagram")
The key itself is a 64-char hex string generated on creation. Store it as an environment variable in the bot's runtime (e.g. Railway, Cloudflare Workers). Never commit it.
Manual SQL example
INSERT INTO {prefix}_api_keys_bot
(uuid, restaurant_id, widget_id, api_key, platform, name, is_active, date_created, date_modified)
VALUES
('akb_unique_id', 1, 42, 'your-64-char-hex', 'instagram', 'RaffaellIA Instagram', 1, NOW(), NOW());
| Column | Notes |
|---|---|
restaurant_id |
Restaurant the key authorizes |
widget_id |
The widget whose services/rules the bot uses |
api_key |
64-char hex. Generate with bin2hex(random_bytes(32)) in PHP |
platform |
Free-form, alphanumeric. Used in source and synthetic emails |
is_active |
Set to 0 to revoke |
Rate limits and timeouts
- The plugin does not impose hard rate limits at the moment. A conversational bot is expected to make at most ~1 call per user message.
- Recommended client-side timeout: 10 seconds. Operations that touch large floorplans (
POST /bookingswithtables_scheduleservices) can take a couple of seconds.
Logging
Every request lands in api_logs_bot:
| Column | Content |
|---|---|
restaurant_id, api_key_id, widget_id |
Resolved from the auth step |
action |
bot_get_restaurant, bot_get_tables, bot_availability, bot_availability_month, bot_create_booking, bot_get_booking, bot_search_bookings, bot_modify_booking, bot_update_status, bot_cancel_booking |
status_code |
HTTP status returned |
request_body, response_body |
Full JSON (API key stripped from request) |
ip_address |
First non-empty of X-Forwarded-For, X-Real-IP, REMOTE_ADDR |
idempotency_key |
SHA-256 hash (only on successful creates) |
Conversational flow example
User: "Quiero reservar para el sábado para 4 personas"
Bot: [GET /availability?date=2026-05-30&party_size=4]
Bot: "Tenemos 13:00, 14:30 y 20:00. ¿Cuál prefieres?"
User: "20:00"
Bot: "¿A nombre de quién y con qué teléfono?"
User: "Juan Pérez, +56912345678"
Bot: [POST /bookings { date, time, party_size, customer_name, customer_phone }]
Bot: "¡Reserva confirmada! Código bo_abc..."
If the POST returns 409 SLOT_UNAVAILABLE (someone took the slot meanwhile), the bot reads details.alternative_dates and offers them to the user.