# Alex Reservations - External Bookings API

REST API for creating reservations from external platforms such as TheFork, OpenTable, or any integration tool like Zapier.

**Base URL:** `https://your-domain.com/wp-json/alexr/v1`

---

## Authentication

All requests must include a valid API key. Two methods are supported:

### Option 1: X-API-Key header (recommended)

```
X-API-Key: your-api-key-here
```

### Option 2: Authorization Bearer header

```
Authorization: Bearer your-api-key-here
```

API keys are 64-character hex strings tied to a specific restaurant. They can be revoked individually by setting them as inactive.

### Authentication errors

| HTTP Code | Error Code | Description |
|-----------|-----------|-------------|
| 401 | `rest_missing_api_key` | No API key provided in the request headers |
| 401 | `rest_invalid_api_key` | The API key does not exist or has been deactivated |

---

## Endpoints

### Create Booking

```
POST /wp-json/alexr/v1/bookings
```

Creates a new reservation in Alex Reservations.

#### Request

**Content-Type:** `application/json`

#### Parameters

| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `first_name` | string | Yes | - | Guest first name |
| `last_name` | string | No | `""` | Guest last name |
| `email` | string | Yes | - | Guest email address |
| `phone` | string | No | `""` | Guest phone number (e.g. `"+34612345678"`) |
| `date` | string | Yes | - | Reservation date in `YYYY-MM-DD` format |
| `time` | string | Yes | - | Reservation time in `HH:MM` format (24h) |
| `party` | integer | Yes | - | Number of guests (must be >= 1) |
| `restaurant_id` | integer | No | API key's restaurant | Restaurant ID. If omitted, uses the restaurant associated with the API key |
| `service_id` | integer | No | Auto-resolved | Shift or Event ID. If omitted, the system automatically finds the active service for the given date and time |
| `platform` | string | No | API key's platform or `"API"` | Source platform name (e.g. `"TheFork"`, `"OpenTable"`). Stored in the booking's `source` field and `platform` column (max 20 chars) |
| `platform_booking_id` | string | No | `null` | External booking ID from the source platform (e.g. TheFork reservation ID). Max 50 chars |
| `status` | string | No | `"booked"` | Booking status. Allowed values: `"pending"`, `"booked"` |
| `notes` | string | No | `null` | Guest notes or special requests |
| `send_notifications` | boolean | No | `false` | Whether to send confirmation email/SMS to the guest |

#### Example request

```bash
curl -X POST https://your-domain.com/wp-json/alexr/v1/bookings \
  -H "Content-Type: application/json" \
  -H "X-API-Key: a1b2c3d4e5f6..." \
  -d '{
    "first_name": "John",
    "last_name": "Doe",
    "email": "john.doe@example.com",
    "phone": "+34612345678",
    "date": "2025-04-15",
    "time": "20:30",
    "party": 4,
    "platform": "TheFork",
    "notes": "Window table if possible",
    "status": "booked",
    "send_notifications": false
  }'
```

#### Successful response

**HTTP 201 Created**

```json
{
  "success": true,
  "booking_id": 456,
  "uuid": "bo_a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "status": "booked"
}
```

#### Duplicate response

**HTTP 200 OK** - Returned when a booking with the same data already exists (see [Idempotency / Duplicate Detection](#idempotency--duplicate-detection))

```json
{
  "success": true,
  "booking_id": 456,
  "uuid": "bo_a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "status": "booked",
  "duplicate": true
}
```

The response contains the original booking data plus a `"duplicate": true` flag. No new booking is created.

#### Error responses

**HTTP 400 Bad Request** - Validation errors

```json
{
  "success": false,
  "error": "Validation failed",
  "details": {
    "email": "email is required",
    "party": "party is required and must be >= 1"
  }
}
```

**HTTP 400 Bad Request** - Invalid time format

```json
{
  "success": false,
  "error": "Invalid time format. Use HH:MM (e.g. 20:30)"
}
```

**HTTP 400 Bad Request** - Invalid status

```json
{
  "success": false,
  "error": "Invalid status. Allowed values: pending, booked"
}
```

**HTTP 401 Unauthorized** - Missing or invalid API key

```json
{
  "code": "rest_missing_api_key",
  "message": "API key is required. Send it via X-API-Key header or Authorization: Bearer <key>.",
  "data": { "status": 401 }
}
```

**HTTP 404 Not Found** - Restaurant not found

```json
{
  "success": false,
  "error": "Restaurant not found"
}
```

---

## Get Booking Status

`GET /wp-json/alexr/v1/bookings/{uuid}/status`

Retrieve the current status of a booking created via the API.

#### Parameters

| Parameter | Location | Required | Description |
|-----------|----------|----------|-------------|
| `uuid` | URL path | Yes | The booking UUID returned when the booking was created |

#### Example request

```bash
curl -X GET https://your-domain.com/wp-json/alexr/v1/bookings/bo_a1b2c3d4-e5f6-7890-abcd-ef1234567890/status \
  -H "X-API-Key: a1b2c3d4e5f6..."
```

#### Successful response

**HTTP 200 OK**

```json
{
  "success": true,
  "booking_id": 456,
  "uuid": "bo_a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "status": "booked",
  "platform": "TheFork",
  "platform_booking_id": "TF-123456"
}
```

#### Error responses

**HTTP 404 Not Found** - Booking not found or does not belong to the API key's restaurant

```json
{
  "success": false,
  "error": "Booking not found"
}
```

**HTTP 401 Unauthorized** - Missing or invalid API key (same as Create Booking)

---

## Cancel Booking

`POST /wp-json/alexr/v1/bookings/{uuid}/cancel`

Cancel a booking that was created from an external platform. Only bookings with a non-empty `platform` field can be cancelled via this endpoint.

#### Parameters

| Field | Type | Location | Required | Description |
|-------|------|----------|----------|-------------|
| `uuid` | string | URL path | Yes | The booking UUID returned when the booking was created |
| `send_notifications` | boolean | JSON body | No | Whether to send cancellation email/SMS to the guest. Default: `false` |

#### Example request

```bash
curl -X POST https://your-domain.com/wp-json/alexr/v1/bookings/bo_a1b2c3d4-e5f6-7890-abcd-ef1234567890/cancel \
  -H "Content-Type: application/json" \
  -H "X-API-Key: a1b2c3d4e5f6..." \
  -d '{
    "send_notifications": false
  }'
```

#### Successful response

**HTTP 200 OK**

```json
{
  "success": true,
  "booking_id": 456,
  "uuid": "bo_a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "status": "cancelled",
  "platform": "TheFork",
  "platform_booking_id": "TF-123456"
}
```

If the booking is already cancelled, the response includes a message:

```json
{
  "success": true,
  "message": "Booking is already cancelled",
  "booking_id": 456,
  "uuid": "bo_a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "status": "cancelled",
  "platform": "TheFork",
  "platform_booking_id": "TF-123456"
}
```

#### Error responses

**HTTP 403 Forbidden** - Booking was not created from an external platform

```json
{
  "success": false,
  "error": "Only bookings created from an external platform can be cancelled via API"
}
```

**HTTP 404 Not Found** - Booking not found or does not belong to the API key's restaurant

```json
{
  "success": false,
  "error": "Booking not found"
}
```

**HTTP 401 Unauthorized** - Missing or invalid API key (same as Create Booking)

---

## How it works

### Service auto-resolution

When `service_id` is not provided, the API automatically finds the active shift or event for the given `date` and `time`:

1. Searches all active shifts for the restaurant
2. Checks if the shift operates on the booking's day of the week
3. Checks day-specific seating hours first, then falls back to the shift's default hours
4. If no shift matches, searches active events
5. The first matching service is used

If no service is found, the booking is still created but without a linked service.

### Duration calculation

The booking duration is automatically calculated from the matched service configuration:

- **Fixed duration mode:** Uses the service's configured `duration_time` (in seconds)
- **Per-covers mode:** Duration depends on the party size, using the service's `duration_covers` mapping
- **Fallback:** 5400 seconds (1.5 hours) if no service is found

### Customer management

- If a customer with the same email already exists for the restaurant, the booking is linked to that customer
- If no customer exists, a new customer record is created automatically

### Platform tracking

The `platform` value is stored in the booking's `source` field. This allows filtering bookings by origin in the dashboard:

- `source = "TheFork"` - Booking came from TheFork
- `source = "OpenTable"` - Booking came from OpenTable
- `source = "API"` - Generic API booking (default when no platform is specified)
- `source = NULL` - Booking created from the widget or dashboard

### Notifications

By default, no email or SMS notifications are sent when creating a booking via API. This avoids duplicating notifications that the external platform (TheFork, OpenTable) already sends.

Set `send_notifications: true` if you want Alex Reservations to send its own notifications to the guest. When enabled, the following notifications are sent:

- **Email:** Booking confirmation email
- **SMS:** SMS notification
- **WhatsApp:** WhatsApp notification (if configured)
- **Dashboard notification:** The restaurant receives a `BookingOnlineReceived` notification

---

## Idempotency / Duplicate Detection

The API includes built-in protection against duplicate bookings. When a request is received, the system generates a unique idempotency key based on the combination of:

- `restaurant_id`
- `email` (case-insensitive)
- `date`
- `time`
- `party`

If a previous successful booking (HTTP 201) already exists with the same idempotency key, the API returns the original booking data with an additional `"duplicate": true` flag and an HTTP 200 status code. **No new booking is created.**

This is useful for scenarios where:

- **Zapier retries:** If a webhook delivery fails and Zapier retries the same request, the booking won't be duplicated
- **Network issues:** If the client doesn't receive the response and resends the request
- **Platform sync:** If an external platform sends the same reservation twice

### How it works

1. A SHA-256 hash is generated from `restaurant_id|email|date|time|party`
2. The hash is checked against previous successful API logs
3. If a match is found, the original response is returned with `"duplicate": true`
4. If no match is found, the booking is created normally
5. The idempotency key is stored in the `api_logs` table for future checks

### Important notes

- The idempotency check only considers **successful** bookings (HTTP 201). Failed requests do not block future attempts
- Two bookings for the **same person, date, time, and party size** at the same restaurant are considered duplicates
- If you need to create a second booking for the same person at the same time (e.g., different party size), change the `party` value
- The duplicate detection is logged as a separate action (`create_booking_duplicate`) in the API logs

---

## API Logging

Every API request is logged in the `api_logs` table with:

- Restaurant ID
- API key used
- Action performed (`create_booking` or `create_booking_duplicate`)
- HTTP status code
- Full request body (sensitive data excluded)
- Full response body
- Client IP address
- Idempotency key (SHA-256 hash for duplicate detection)
- Timestamp

---

## Setting up API keys

### From the dashboard (recommended)

API keys can be managed from the Alex Reservations dashboard:

1. Go to **Settings > Integrations > API Keys**
2. Click **Generate New API Key**
3. Enter a name (e.g. `"TheFork production key"`) and platform (e.g. `"TheFork"`)
4. The key is generated automatically and shown once. Copy it and store it securely
5. You can toggle keys active/inactive, edit their name/platform, or delete them

Access to the API Keys settings page is controlled by the **API Keys** permission in the Roles section. By default, only the Super Manager role has this permission enabled.

### Manual setup via SQL

API keys are stored in the `api_keys` database table. You can also insert them manually:

```sql
INSERT INTO {prefix}_api_keys
  (uuid, restaurant_id, api_key, platform, name, is_active, date_created, date_modified)
VALUES
  ('ak_unique_id', 1, 'your-64-char-hex-key', 'TheFork', 'TheFork via Zapier', 1, NOW(), NOW());
```

| Column | Description |
|--------|-------------|
| `restaurant_id` | The restaurant this key grants access to |
| `api_key` | 64-character hex string. Auto-generated from the dashboard, or generate manually with `bin2hex(random_bytes(32))` in PHP |
| `platform` | Default platform name for bookings made with this key (e.g. `"TheFork"`) |
| `name` | Human-readable description (e.g. `"TheFork production key"`) |
| `is_active` | Set to `0` to revoke the key |

You can create multiple keys per restaurant, each with a different platform.

---

## Zapier integration example

When integrating with TheFork via Zapier:

1. **Trigger:** New reservation in TheFork (via Zapier's TheFork integration)
2. **Action:** Webhooks by Zapier > POST request
3. **Configuration:**
   - **URL:** `https://your-domain.com/wp-json/alexr/v1/bookings`
   - **Payload Type:** JSON
   - **Headers:** `X-API-Key: your-api-key-here`
   - **Body mapping:**

| Zapier field | API field |
|-------------|-----------|
| First Name | `first_name` |
| Last Name | `last_name` |
| Email | `email` |
| Phone | `phone` |
| Date | `date` (make sure format is YYYY-MM-DD) |
| Time | `time` (make sure format is HH:MM, 24h) |
| Party Size | `party` |
| Notes | `notes` |
| _(static value)_ | `platform` = `"TheFork"` |

---

## Response codes summary

| HTTP Code | Meaning |
|-----------|---------|
| 200 | Duplicate booking detected. Returns original booking data with `"duplicate": true`. No new booking created |
| 201 | Booking created successfully |
| 400 | Validation error (missing fields, invalid format, invalid status) |
| 401 | Authentication error (missing or invalid API key) |
| 404 | Restaurant not found |
