# OgaSite API reference — v1

Base URL: `https://api-ogasite.lucalmhub.com/api/v1`

**Conventions**

- JSON in and out; field names are camelCase.
- Browser calls use `credentials: 'include'` (session cookies).
- Every `POST/PATCH/PUT/DELETE` (except `/leads`) must send the header
  `X-OgaSite-Client: app`, otherwise **403**.
- Errors: `{ "message": "…", "errors": { "field": "…" } }` (`errors` only on 400 validation).
- 401 = not signed in / session expired → call `POST /auth/refresh` once, then retry.
- Money is integer **kobo**. Dates are WAT strings `YYYY-MM-DD HH:MM:SS`.

Roles: `owner`, `manager`, `technician`, `storekeeper`, `accountant`.

---

## Health

| Method | Path | Auth | Response |
| --- | --- | --- | --- |
| GET | `/health` | — | `{ ok, db }` (503 if the database is down) |

## Auth — `/auth`

| Method | Path | Body | Notes |
| --- | --- | --- | --- |
| POST | `/register` | `companyName, name, email, phone, password` | Creates company (14-day trial) + Owner; sets cookies. 201 → profile. 409 if email exists |
| POST | `/login` | `email, password` | Sets cookies → profile. 401 on wrong details |
| POST | `/refresh` | — | Rotates refresh token, new access token. 401 → log in again |
| POST | `/logout` | — | Revokes this session, clears cookies |
| GET | `/me` | — | Profile (signed in) |
| POST | `/forgot-password` | `email` | Always 200; emails a 6-digit code if the account exists |
| POST | `/reset-password` | `email, code, password` | Resets password, ends all sessions |
| POST | `/change-password` | `currentPassword, newPassword` | Signed in; ends other sessions |

**Profile shape**

```json
{
  "user":    { "id": 1, "name": "…", "email": "…", "phone": "…", "role": "owner", "mustChangePassword": false },
  "company": { "id": 1, "name": "…", "email": "…", "phone": "…", "address": "…", "city": "…", "state": "…",
               "rcNumber": "…", "logoUrl": null, "plan": "trial", "trialEndsAt": "2026-11-17 13:05:00" }
}
```

Password rule: at least 8 characters with a letter and a number.

## Company — `/company` (signed in)

| Method | Path | Roles | Body |
| --- | --- | --- | --- |
| GET | `/` | all | → `{ company }` |
| PATCH | `/` | owner | any of `name, email, phone, address, city, state, rcNumber` → `{ company }` |

## Team — `/users` (owner, manager)

| Method | Path | Body | Notes |
| --- | --- | --- | --- |
| GET | `/` | — | `{ users: [...] }` |
| POST | `/` | `name, email, role, password, phone?, hourlyRateKobo?` | Temporary password; user must change it at first login. Managers cannot create Owners |
| PATCH | `/:id` | any of `name, phone, role, status ('active'/'disabled'), hourlyRateKobo` | Cannot change own role or disable self; company keeps ≥ 1 active Owner; disabling ends their sessions |

User shape: `{ id, name, email, phone, role, status, hourlyRateKobo, lastLoginAt, createdAt }`

## Customers — `/customers` (signed in)

| Method | Path | Roles | Body / query |
| --- | --- | --- | --- |
| GET | `/` | all | `?q=search&page=1&limit=20` → `{ customers, page, limit, total }` |
| POST | `/` | owner, manager, accountant | `type ('individual'/'business'), name, contactName?, phone?, email?, address?, notes?` |
| GET | `/:id` | all | → `{ customer, sites }` |
| PATCH | `/:id` | owner, manager, accountant | any customer field |
| DELETE | `/:id` | owner, manager | Soft delete (with its sites) |
| POST | `/:id/sites` | owner, manager, accountant | `label, address, landmark?, city?, state?, lat?, lng?, contactName?, contactPhone?, accessNotes?` |
| PATCH | `/:id/sites/:siteId` | owner, manager, accountant | any site field |
| DELETE | `/:id/sites/:siteId` | owner, manager | Soft delete |

Records from another company always return **404**.

## Dashboard — `/dashboard` (signed in)

| Method | Path | Response |
| --- | --- | --- |
| GET | `/summary` | `{ counts: { customers, sites, team }, plan, trialEndsAt, checklist: [{ id, label, done, to }] }` |

## Leads — `/leads` (public, used by the website)

| Method | Path | Body | Notes |
| --- | --- | --- | --- |
| POST | `/` | `name, company, phone, email, industry, teamSize, consent: true, message?, source?, website` | No client header needed. `website` is a honeypot (must be empty). Rate limited 5/hour/IP. Emails `SALES_EMAIL` |

`industry`: `solar-installers | facility-management | hvac-electrical-plumbing | construction | other`
`teamSize`: `1-5 | 6-30 | 30+`
