# Conventions

> Times and time zones, IDs, paging, errors, rate limits and AI credits.

## Times

LifeButler lives in **Europe/Berlin** (Germany, Austria, Switzerland), and so does the API.

- **Responses** use ISO 8601 with the Berlin offset: `"2026-11-20T19:30:00+01:00"` (winter), `"2026-07-01T10:00:00+02:00"` (summer). Dates are `"2026-11-20"`.
- **Requests** accept any ISO 8601 instant (`…Z`, `…+01:00`). A time **without** offset (`"2026-11-20T19:30"`) is Berlin time. A plain date is the start of that day in Berlin.
- **All-day appointments** take dates: `start` is the first day, `end` the last day (inclusive).
- **Repeating appointments** keep their wall-clock time across daylight-saving changes, like in the app. `GET …/events` expands them into single occurrences.

## IDs

IDs are opaque strings (letters, digits, `_`, `-`). Workspace IDs come from `GET /workspaces`, member IDs from `GET /workspaces/{id}/members` – use those for `attendeeIds` and `assigneeIds`.

## Paging

Lists that can grow without end (documents) are paged:

```json
{ "data": [ … ], "nextCursor": "Qm9x1…" }
```

Pass `cursor=<nextCursor>` for the next page; `nextCursor` is `null` on the last one. `limit` sets the page size. Other lists return everything at once, or a capped list with `"truncated": true`.

## Writing

- `POST` creates and answers `201` with the new object.
- `PATCH` changes only the fields you send. Send `null` to clear a field where the reference says so.
- `DELETE` answers `{ "id": "…", "deleted": true }`.
- Unknown fields are ignored.
- Requests aren't deduplicated. If a `POST` times out, check (e.g. list the items) before you send it again.

## Errors

Errors use the HTTP status and a stable `code`:

```json
{ "error": { "code": "workspace_not_found", "message": "No such workspace for this API key.", "requestId": "4c1e…" } }
```

| Status | `code` | Meaning |
|---|---|---|
| 400 | `invalid_request`, `invalid_cursor` | Something in the request is wrong – see `message` |
| 401 | `missing_key`, `invalid_key`, `key_expired` | No usable key |
| 402 | `no_credits` | The workspace owner has no AI credits left this month |
| 403 | `plan_required` | The key's account has no Pro/Premium plan (any more) |
| 403 | `insufficient_scope` | The key lacks the scope |
| 403 | `admin_required` | You are not owner/admin of the workspace (any more) |
| 403 | `workspace_limit` | More shared workspaces than the plan allows |
| 404 | `workspace_not_found`, `not_found` | Doesn't exist – or isn't yours |
| 409 | `read_only`, `duplicate`, `ai_disabled`, `cannot_analyze` | Not possible in the current state |
| 413 / 415 | `too_large`, `unsupported_type` | Upload too big / wrong file type |
| 429 | `rate_limited` | Too many requests – wait `Retry-After` seconds |
| 503 | `ai_unavailable` | The AI is busy – try again shortly |
| 507 | `storage_full` | The workspace owner's storage is full |

Quote the `requestId` (also in the `X-Request-Id` header) when you contact support.

## Rate limits

**120 requests per minute and key.** Every response tells you where you stand:

```http
X-RateLimit-Limit: 120
X-RateLimit-Remaining: 117
X-RateLimit-Reset: 42
```

Over the limit you get `429` with `Retry-After`. Back off and retry after that many seconds; for `5xx` use exponential backoff.

## AI credits

`ask_butler` and document analysis cost **AI credits of the workspace owner**, exactly like in the app (butler: usually 1–2 per question; documents: 1 per page). Smart search is free. AI features only work where the workspace admins turned AI on (*Workspace settings → AI*); otherwise you get `409 ai_disabled`.

## Versioning

The API is versioned in the path (`/v1`). Within v1 we only add things: new endpoints, new optional fields, new values in fields documented as open lists. Build your client to ignore unknown fields. Breaking changes would come as `/v2`, announced well ahead.
