# SpotDrop public booking API

Read a business's services and free slots, and create a booking. No API key, token
or registration — these are the same endpoints the public booking page calls, rate
limited per IP the same way.

- Base URL: `https://www.spotdrop.si`
- OpenAPI 3.1: https://www.spotdrop.si/openapi.json
- Time zone: Europe/Ljubljana. Dates `YYYY-MM-DD`, times `HH:MM`.
- `{slug}` is the last path segment of a business page: `https://www.spotdrop.si/frizerstvo-ana`
  has slug `frizerstvo-ana`.

## GET /api/company/{slug}/appointment-services

Services the business has enabled for online booking.

```json
{ "services": [ { "id": 12, "name": "Moško striženje", "duration": 30, "price": "18" } ] }
```

`name` must be sent back verbatim when booking. `duration` is in minutes and belongs
in the slots request. `price` is a string exactly as the business typed it.

## GET /api/company/{slug}/slots

Query: `date` (required), `duration` (minutes, default 30), `employeeId` (optional).

```json
{ "slots": [ { "time": "09:00", "available": true }, { "time": "09:30", "available": false } ] }
```

Only `available: true` is bookable. An empty array is a normal answer — closed day,
blocked date, or outside the business's booking window (each sets a minimum notice
and a maximum number of days ahead). Businesses that book per employee need
`employeeId`, because availability differs per person.

## POST /api/company/{slug}/appointments

```json
{
  "serviceName": "Moško striženje",
  "guestName": "Ana Novak",
  "guestPhone": "+38641123456",
  "guestEmail": "ana@example.com",
  "appointmentDate": "2026-09-01",
  "appointmentTime": "09:00",
  "notes": "Prvič pri vas.",
  "source": "api"
}
```

Required: `serviceName`, `guestName` (min 2 chars), `guestPhone` (min 7 digits),
`appointmentDate`, `appointmentTime`. Optional: `guestEmail` (recommended — it carries
the confirmation and the cancellation link), `employeeId`, `notes`, `lang` (`sl`/`en`),
`source` (send `api`).

```json
{ "success": true, "appointment": { "id": 4821, "date": "2026-09-01", "time": "09:00", "status": "confirmed" } }
```

When the service requires a deposit the response instead carries
`"requiresPayment": true` with a `checkoutUrl` and status `pending_payment`. The slot
is held only until that Stripe checkout is paid — pass the URL to the customer and do
not report the appointment as confirmed.

| Status | Meaning |
| --- | --- |
| 400 | Missing or malformed field; `error` says which |
| 403 | Online booking not enabled for this business |
| 404 | Unknown business, or booking not enabled |
| 409 | Slot taken in the meantime — re-read availability |
| 429 | Rate limited — back off and retry |

## GET /api/health

`{ "status": "ok" | "degraded" | "error", "timestamp": "...", "checks": { ... } }`
200 while serving traffic, 503 when not.

## What is not public

Dashboard endpoints (appointments list, customers, schedule, billing, analytics)
authenticate with a first-party session cookie issued to a human after an email magic
link. They are not machine-callable and there is no API key or OAuth flow for them
today. See https://www.spotdrop.si/auth.md.

## Rules of use

- Book only on behalf of a person who asked for that slot and gave you their details.
- No speculative or duplicate bookings — a slot is a real slot in a real calendar.
- Do not poll availability on a schedule; read it when someone asks.
- Identify your client in the `User-Agent` header.

Questions: info@spotdrop.si
