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. |
send_notifications |
bool | no | true | If false, no email/SMS/WhatsApp is sent. |
widget_id |
int | no | API key default | Rarely needed. |
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"
}
}
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?phone=... — search by phone
Useful when the user identifies themselves by phone instead of a reservation code.
| Param | Required | Default | Description |
|---|---|---|---|
phone |
yes | — | Phone number, matched exactly against booking.phone |
limit |
no | 5 | 1–20 results |
include_past |
no | false | If true, also returns past bookings |
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", ... }
]
}
}
Returns only non-deleted, non-selected bookings. Sorted by date desc.
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 | |
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.
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.".
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_availability, bot_availability_month, bot_create_booking, bot_get_booking, bot_search_bookings, bot_modify_booking, 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.