# CrispHive Backend Integration Skill

You are integrating a backend with the **CrispHive Developer API** — a REST API
for field-service businesses (customers, job bookings, technicians, fleet).
This document is the authoritative quick-reference; the complete
machine-readable contract is the OpenAPI 3.0.3 spec served next to this file at
`/developers/openapi.json`. Read both before writing code.

## Base URL & authentication

| Environment | Base URL | Key prefix |
|---|---|---|
| Production (live data) | `https://api.crisphive.com/v1` | `chsk_live_…` |
| Sandbox (isolated test data) | same URL | `chsk_test_…` |

- Authenticate EVERY request with a secret API key as a Bearer token:
  `Authorization: Bearer chsk_test_…`
- **The key prefix selects the data environment** — a `chsk_test_` key can only
  ever read/write sandbox data; there is no separate sandbox host.
- Keys are created on the business dashboard (shown once — store securely,
  server-side only; never embed a `chsk_` key in client-side code).
- A key is either full-access or **restricted** to a set of permission codes
  chosen at creation (e.g. `customers_view`, `job_create`). A restricted key
  calling outside its scope gets `403`.
- Invalid/revoked key → `401` with `error_code: "API_KEY_INVALID"`.

## MCP server (agent-native access)

The same API is also exposed as an **MCP (Model Context Protocol) server** at
`https://api.crisphive.com/mcp` — Streamable HTTP, stateless, tools-only, same
`Authorization: Bearer chsk_…` header. Every `/v1` operation is one tool named
by its `operationId` (`listCustomers`, `createCustomer`, `createJobRequest`,
`listJobRequestBookingWindows`, …); path/query/body inputs are flattened into
one arguments object. Header inputs become arguments too: create tools take an
optional `idempotency_key` (→ `Idempotency-Key` header) and endpoints that
need the customer timezone take `x_timezone` (→ `X-Timezone` header, e.g.
required on `listJobRequestBookingWindows`). Tool results return the
standard response envelope below as text. If you are an agent with MCP client
support, prefer connecting there (`claude mcp add --transport http crisphive
https://api.crisphive.com/mcp --header "Authorization: Bearer chsk_test_…"`);
if you are writing integration code for a backend, use the REST API described
in the rest of this document. Everything else here (envelope, pagination, sync
cursor, sandbox rules, rate limits) applies to both.

### Two ways to authenticate the MCP endpoint

1. **API key (server-to-server, developers).** Send `Authorization: Bearer
   chsk_live_…` / `chsk_test_…` — the key prefix picks live vs sandbox. This is
   the path above (`--header`), for agents you run yourself.
2. **OAuth 2.1 (end-user connectors — claude.ai / ChatGPT / any MCP client that
   speaks OAuth).** The `/mcp` endpoint is also a full OAuth 2.1
   Authorization Server, so a business owner can authorize an agent WITHOUT
   handling a key. A compliant MCP client does this automatically: it gets a
   `401` with `WWW-Authenticate: Bearer resource_metadata="…"`, reads the
   discovery documents, registers itself, runs the authorization-code + PKCE
   flow, and calls `/mcp` with the issued access token. You normally write NO
   OAuth code — the MCP client library handles it. Discovery + flow:
   - `GET /.well-known/oauth-protected-resource` (RFC 9728) → the Authorization Server
   - `GET /.well-known/oauth-authorization-server` (RFC 8414) → `authorization_endpoint`, `token_endpoint`, `registration_endpoint`; PKCE `S256` required; grants `authorization_code` + `refresh_token`
   - `POST /oauth/register` (RFC 7591 Dynamic Client Registration) → `client_id` (public client, no secret — PKCE is the proof)
   - `GET/POST /oauth/authorize` → the business owner consents (they must be
     signed in to CrispHive)
   - `POST /oauth/token` → `{ access_token (Bearer, ~1h), refresh_token }`; also `grant_type=refresh_token` to rotate
   Access tokens are scoped to the granting owner's business, region and
   environment. Requesting narrower `scope` (permission codes, e.g.
   `customers_view`) limits what the agent can do; omitting it grants full
   business access.

## Response envelope (single parser)

Every response — success and error — uses ONE envelope shape:

```json
{ "error_code": 0, "message": "Success", "errors": null, "data": { } }
```

- `error_code` is `0` on success; a stable string code on failure
  (e.g. `"CUSTOMER_NOT_FOUND"`, `"VALIDATION_ERROR"`, `"API_KEY_INVALID"`).
- `errors` carries field-level validation details when present.
- `data` holds the payload. Write ONE response parser and share it across all
  calls: unwrap `data` when `error_code === 0`, otherwise raise/return an error
  keyed by `error_code` (match codes, never message strings — messages are
  localized via the optional `X-Locale` header).

## Endpoint catalog (the exposed surface)

Only the endpoints below exist on `/v1`. Anything else you may find in
dashboards or internal docs is NOT part of the developer API.

### Customers — full CRUD (CRM sync)
- `GET    /v1/customers` — list (paginated; also supports the `since` sync cursor)
- `POST   /v1/customers` — create (supports `Idempotency-Key`)
- `GET    /v1/customers/{id}`
- `PUT    /v1/customers/{id}`
- `DELETE /v1/customers/{id}`

Create body essentials: `full_name` (required), at least one of `phone`/`email`,
optional `uid` (your own system's ID, max 32 chars), `tier` (`regular`|`vip`),
`address`, `preferred_technician_id`, `service_area_id`. Response:
`{ "customer_id": "<uuid>" }`.

### Job requests (bookings) — create & track
- `POST /v1/job-requests` — create a booking (supports `Idempotency-Key`)
- `GET  /v1/job-requests` — list (paginated)
- `GET  /v1/job-requests/changes` — incremental sync feed (see below)
- `GET  /v1/job-requests/booking-windows` — bookable date/period windows
- `GET  /v1/job-requests/{id}` — full detail (status, schedule, crew)
- `GET  /v1/job-requests/{id}/timeline` — per-status progress timeline

Create body essentials: `customer_id` (required, an existing customer's UUID)
and `job_dates` (1–12 entries of `{ "date": "YYYY-MM-DD", "periods":
[{ "period": "morning"|"afternoon"|"evening" }] }`). Optional: `job_type_id`,
`skill_ids` (≤20), `description` (≤2000 chars). Call `booking-windows` first
and offer only the returned windows. Quoting, confirming and completing a job
are dashboard operations — your integration observes those transitions via the
changes feed (and webhooks), it does not drive them.

### Reference catalogs — read-only (discover IDs for the writes above)
- `GET /v1/job-types`, `GET /v1/job-types/{id}` — for `job_type_id`
- `GET /v1/skills` (flat, primary), `GET /v1/skill-categories`,
  `GET /v1/skill-categories/{id}/skills` — for `skill_ids`
- `GET /v1/service-areas`, `GET /v1/service-areas/{id}` — for `service_area_id`
- `GET /v1/technicians`, `GET /v1/technicians/{id}` — for `preferred_technician_id`
- `GET /v1/vehicles`, `GET /v1/vehicles/{id}` — fleet, display-only

## Pagination (every list endpoint)

Query `?page=<n>&limit=<m>` (default page 1, limit 15, max 1000). List
responses wrap items with a `meta` object:

```json
{ "meta": { "total": 120, "count": 15, "per_page": 15, "current_page": 1, "total_pages": 8 } }
```

## Keeping data in sync — `GET /v1/job-requests/changes`

Incremental short-poll feed. Protocol:

1. First poll: call WITHOUT `since`. Store the returned `next_since`.
2. Every poll after: `GET /v1/job-requests/changes?since=<next_since>` echoing
   the last `next_since` verbatim (RFC3339).
3. Response carries the changed job requests + `next_since` (new watermark) +
   `has_more`. If `has_more` is `true`, poll again immediately; otherwise wait
   your polling interval (30–60s is typical).
4. Upsert by `id` — the feed is at-least-once; you may see a row twice.

The same `since`/`next_since` pattern is available on `GET /v1/customers`.

## Idempotency (safe retries)

On `POST /v1/customers` and `POST /v1/job-requests`, send an
`Idempotency-Key` header (any unique string ≤255 chars, e.g. a UUID per
logical operation). Retrying with the same key replays the original response
(marked `Idempotent-Replayed: true`) instead of creating a duplicate.
Same key + different body → `422 IDEMPOTENCY_KEY_REUSE`. Concurrent duplicate
in flight → `409 IDEMPOTENCY_IN_PROGRESS` (retry after a short delay).
Always set this header in your SDK's create calls with auto-retry.

## Quickstart flow (what a first integration does)

```text
1. GET  /v1/skills, /v1/job-types            → cache reference IDs
2. POST /v1/customers                        → { customer_id }        (Idempotency-Key!)
3. GET  /v1/job-requests/booking-windows     → pick offered date/periods
4. POST /v1/job-requests                     → booking created        (Idempotency-Key!)
5. Loop: GET /v1/job-requests/changes?since=… → upsert local copies
```

Use a `chsk_test_` key for all development — sandbox data never touches real
customers. Switch to `chsk_live_` only in production config.

## Conventions & gotchas

- All IDs are UUIDs; all timestamps are RFC3339 UTC (`*_at` fields).
- Times-of-day are integer minutes since midnight (0–1440), never `"HH:MM"`.
- The API is versioned by path: `/v1` responses only ever gain fields
  (extend-only). Parse leniently — ignore unknown fields.
- Localized messages: send `X-Locale: en|de|fr|ar|es|ja|ko` (optional).
- Rate limits apply per key/IP; on `429`, back off and retry with jitter.
  Booking creation is rate-limited — batch imports should throttle themselves.
- Webhooks (push instead of polling) are configured by the business on the
  dashboard; payloads are HMAC-SHA256 signed (`CrispHive-Signature` header) and
  deduplicated by event `id`. A receiver must answer the signed verification
  `ping` with 2xx before the endpoint activates, and 5 consecutive delivery
  failures auto-disable it (re-verify from the dashboard to resume) — so make
  your receiver return 2xx fast and process asynchronously.
