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. |
INVALID_TABLE |
400 | table_ids contains tables from another restaurant. |
AREA_NOT_FOUND |
404 | area does not match any area of this restaurant. |
AREA_AMBIGUOUS |
400 | Several areas share that name — use the area id. |
AREA_NOT_SUPPORTED |
400 | The service does not let guests choose an area. |
AREA_NOT_BOOKABLE |
409 | The area is not open to online bookings. |
AREA_FULL |
409 | Not enough free seats (or no free table) in that area. Returns the area's free_seats. |
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) |
area |
no | area id or name. Only returns slots where the party fits in that area. |
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 }
]
}
}
Areas
When the service shares its capacity between areas (availability type volume total with "The user can select Area"), every slot also carries the free seats per area — the same numbers the web widget shows:
{
"time": "20:00", "time_seconds": 72000,
"service_id": 102, "service_name": "Dinner", "service_type": "shift",
"duration_minutes": 90,
"areas": [
{ "area_id": 6, "area_name": "Terrace", "max": 36, "occupied": 30, "free": 6 },
{ "area_id": 13, "area_name": "Cellar", "max": 40, "occupied": 40, "free": 0 }
]
}
This lets the bot answer "at 20:00 we still have room on the Terrace, but the Cellar is full" without a second request.
Passing area narrows the result instead:
curl "https://your-domain.com/wp-json/alexr-bot/v1/availability?date=2026-06-15&party_size=4&area=Terrace" \
-H "X-API-Key: ...."
- Only slots where
party_size <= freein that area are returned, and each slot'sareasholds just that area. - The resolved area is echoed back at the top level as
"area": { "id": 6, "name": "Terrace" }. - On a table based service the area does not hold seats of its own, so no
areasbreakdown is returned;areathen filters the slots down to those with a free table in that area. - A service that does not allow choosing an area contributes no slots when
areais used. - An unknown area responds
404 AREA_NOT_FOUND.
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. |
status |
string | no | service-resolved | Force the initial status. One of pending, booked, confirmed. If omitted, the service's own rules decide (usually booked, or pending if the service is configured to require review). Explicit value bypasses the "force pending for specific tables" rule — respect what the caller asks. Any other value → 400 VALIDATION_FAILED. |
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. |
area |
int|string | no | none | Area the guest chooses — id or name. Validated against the area's capacity. See note below. |
send_notifications |
bool | no | true | If false, no email/SMS/WhatsApp is sent. Notifications sent match the final status: pending/booked/confirmed each have their own email + WhatsApp templates. |
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.
area. The same choice the guest makes in the web widget when the service has "The user can select Area" enabled — stored in the booking and returned in every payload. Accepts the area id or its name (case-insensitive; if two areas share a name the request fails with400 AREA_AMBIGUOUS, use the id).What gets validated depends on the service:
- Volume total with area selection — the area has its own seat limit (Terrace 36, Cellar 40). If the party does not fit, the request fails with
409 AREA_FULLand the response details carrymax_covers,occupiedandfree_seatsso the bot can offer a different area.- Table based services — the area has no seats of its own; it restricts where the table is taken from. If no table is free in that area,
409 AREA_FULL.- Service without area selection →
400 AREA_NOT_SUPPORTED. Unknown area →404 AREA_NOT_FOUND. Area not open to online bookings →409 AREA_NOT_BOOKABLE.An area with no capacity configured counts as zero seats, not as unlimited — exactly as the widget treats it. Configure a limit for every area you let guests choose.
areais ignored whentable_idsis used: the tables already decide where the guests sit.
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"
}'
Force the booking to be created in pending state (e.g. bot flow that requires restaurant review before confirming):
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_phone": "+56912345678",
"status": "pending"
}'
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" }
],
"area": { "id": 6, "name": "Terrace" }
}
}
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).
areais the area the guest chose (nullwhen none was requested). Do not confuse it with thearea_idinside each entry oftables: that one is where the assigned table physically is, whileareais the guest's choice — which on a volume total service may carry no table at all.
Error — area full (409):
{
"success": false,
"error": {
"code": "AREA_FULL",
"message": "Not enough free seats in this area for the requested party size.",
"details": {
"area_id": 6,
"area_name": "Terrace",
"max_covers": 36,
"occupied": 34,
"free_seats": 2
}
}
}
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. |
area |
int|string | Move the booking to another area (id or name), validated the same way as on create. null or "" clears it. On a table based service the table is reassigned inside the new area. |
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.
The area is only touched when you send area. Changing party_size or time does not re-check the area on its own, so a booking can end up over its area's limit — send area together with the change when you want it re-validated.
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
Change a booking's status. Supports both reservation-lifecycle transitions (pending → booked → confirmed) and table-lifecycle transitions (seated → finished, or no-show), so the same endpoint is used by the bot when the restaurant confirms a pending reservation and by the POS / host app when guests are seated or the table is closed.
| Param | Required | Default | Description |
|---|---|---|---|
status |
yes | — | One of the values below |
send_notifications |
no | false |
If true and the new status is pending or booked, sends the same customer email/SMS/WhatsApp that a fresh booking would send. Ignored for seated, finished, no-show, confirmed. |
Allowed values
| Status | Meaning |
|---|---|
pending |
Awaiting restaurant confirmation. |
booked |
Restaurant has confirmed the booking. |
confirmed |
Customer has re-confirmed the booking. |
seated |
Guests have been seated. |
finished |
Table closed / paid out. |
no-show |
Guests did not show up (immediately frees the slot). |
Cancellations should go through POST /bookings/{uuid}/cancel (with reason), not through this endpoint.
# Restaurant confirms a pending booking, sending the confirmation email
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": "booked", "send_notifications": true }'
# POS marks guests as seated (no notifications, floor plan updated)
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
- Customer notifications are off by default to avoid noise for frequent POS updates. Opt in with
send_notifications: true(only meaningful forpending/booked). - An audit-log entry is always recorded ("Automation Bot") via the standard status-changed event.
-
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.