Documents

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 pass table_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 (otherwise 400 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" }
    ]
  }
}

tables lists 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 (like cancelled) immediately frees the table on the floor plan and in availability.
  • seated keeps the table occupied (the guests are there).
  • finished marks the reservation as completed for statistics; the table stays reserved for the booking's time window (Alex Reservations counts finished as occupying its slot).
  • A cancelled / denied booking cannot change status → 409 BOOKING_NOT_MODIFIABLE.
  • Any other status value → 400 VALIDATION_FAILED (allowed values are returned in error.details.allowed).
  • Idempotent: setting the status it already has returns 200 with "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_type is 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}@fake where platform is the API key's platform field (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 booking source and 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 /bookings with tables_schedule services) 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.