# Okkre Public API - LLM Integration Brief

Use this file when an LLM must generate code that integrates a club website with
the Okkre public API.

## Core Contract

Base URL:

```text
/api/v1/clubs/{clubId}
```

Authentication:

```http
Authorization: Bearer <api_key>
```

Every request needs both values: `{clubId}` in the URL identifies the club and
the API key authorizes the request. The key must belong to that club.

Scopes:

- `read`: read courts, availability, API-created reservations.
- `book`: all `read` permissions plus create/cancel reservations.

Never expose a `book` key in browser code. Prefer server-side calls.

## Endpoints

| Method | Path | Scope | Purpose |
| --- | --- | --- | --- |
| `GET` | `/courts` | `read` | List active courts. |
| `GET` | `/availability?from=YYYY-MM-DD&to=YYYY-MM-DD&sport=padel&court_id=...` | `read` | List available slots using the club's configured duration and price. |
| `POST` | `/reservations` | `book` | Create an idempotent reservation. |
| `GET` | `/reservations/{externalId}` | `read` | Read one API-created reservation. |
| `DELETE` | `/reservations/{externalId}` | `book` | Cancel one API-created reservation. |
| `POST` | `/reservations/{externalId}/cancel` | `book` | Cancel one API-created reservation. |

## Data Shapes

Court response:

```json
{
  "courts": [
    {
      "id": "uuid",
      "name": "Terrain 1",
      "sport": "padel",
      "environment": "indoor",
      "display_order": 1
    }
  ]
}
```

Availability response:

```json
{
  "availability": [
    {
      "court": "Terrain 1",
      "date": "2026-09-12",
      "start_time": "18:00",
      "end_time": "20:00",
      "price_cents": 2400
    }
  ]
}
```

Create reservation request:

```json
{
  "external_id": "site-order-123",
  "court": "Terrain 1",
  "date": "2026-09-12",
  "start_time": "18:00",
  "end_time": "19:30",
  "person": {
    "first_name": "Ada",
    "last_name": "Lovelace",
    "email": "ada@example.com"
  },
  "notes": "Optional"
}
```

Create/read/cancel reservation response:

```json
{
  "reservation": {
    "id": "uuid",
    "external_id": "site-order-123",
    "court_id": "uuid",
    "starts_at": "2026-09-12T16:00:00.000Z",
    "ends_at": "2026-09-12T17:30:00.000Z",
    "price_cents": 2400,
    "status": "confirmed",
    "cancellation_reason": null
  },
  "idempotent_replay": false
}
```

`GET /reservations/{externalId}` returns the same `reservation` object without
`idempotent_replay`.

Error shape:

```json
{
  "error": {
    "code": "slot_unavailable",
    "message": "Ce terrain est deja occupe sur ce creneau."
  }
}
```

## Validation Rules

- `from`, `to`, `date`: `YYYY-MM-DD`.
- Availability range: 31 days maximum.
- Times: `HH:MM`, 24-hour format.
- Reservation start must be on the hour or half-hour.
- Reservation duration must be positive and a multiple of 30 minutes.
- `court`: readable court name is preferred; UUID also works.
- `person.email` or `person.phone` is required.
- `person.email`: basic email format.
- `person.phone`: 6 to 30 characters.
- `person.first_name` and `person.last_name`: required, max 80 characters each.
- `notes`: optional, truncated to 500 characters.
- Cancellation `reason`: optional, truncated to 300 characters.

## Integration Algorithm

1. On page load, call `GET /courts` or directly call `GET /availability`.
2. Show availability slots to the user. Slots are returned in local club date and
   time fields, with `price_cents` for the complete configured slot. Send the
   returned start and end times unchanged when booking.
3. When the user confirms, call `POST /reservations` from the backend with a
   stable `external_id`.
4. If response is `201` or `200` with `idempotent_replay: true`, treat the
   reservation as created.
5. If response is `409` and `error.code` is `slot_unavailable`, refresh
   availability and ask the user to choose another slot.
6. To cancel, call either `DELETE /reservations/{externalId}` or
   `POST /reservations/{externalId}/cancel`.

## TypeScript Helper

```ts
type OkkreReservationInput = {
  external_id: string;
  court: string;
  date: string;
  start_time: string;
  end_time: string;
  person: {
    first_name: string;
    last_name: string;
    email?: string;
    phone?: string;
  };
  notes?: string;
};

async function okkreFetch<T>(
  baseUrl: string,
  apiKey: string,
  path: string,
  init: RequestInit = {},
): Promise<T> {
  const response = await fetch(`${baseUrl}${path}`, {
    ...init,
    headers: {
      Authorization: `Bearer ${apiKey}`,
      "Content-Type": "application/json",
      ...init.headers,
    },
  });

  const data = await response.json().catch(() => null);
  if (!response.ok) {
    const code = data?.error?.code ?? "unknown_error";
    const message = data?.error?.message ?? `Okkre API error ${response.status}`;
    throw new Error(`${code}: ${message}`);
  }
  return data as T;
}

export async function createOkkreReservation(
  baseUrl: string,
  apiKey: string,
  input: OkkreReservationInput,
) {
  return okkreFetch(baseUrl, apiKey, "/reservations", {
    method: "POST",
    body: JSON.stringify(input),
  });
}
```

## Common Error Handling

- `missing_api_key`, `invalid_authorization_header`, `api_key_not_found`,
  `api_key_revoked`: ask the admin to check the API key.
- `api_key_wrong_club`: check the `{clubId}` in the URL.
- `insufficient_scope`: use a `book` key for create/cancel.
- `validation_error`: fix request fields.
- `invalid_period`: fix availability date range.
- `reservation_not_found`: unknown or non-API reservation.
- `slot_unavailable`: refresh availability and retry with another slot.
