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.
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_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_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.