# Farsight

Farsight watches streaming video or ingests images and produces typed data based on its analysis. It's used for scientific research, security, embedded devices, among other things. If you need clean typed analysis of images or video streams, farsight has you covered.

- Base URL: https://farsight.observer/v1
- Auth: `Authorization: Bearer <api key>` (keys start with `fs_`). Create keys in the dashboard at https://farsight.observer/keys.
- Errors: `{"error": {"code": "...", "message": "...", "field": "..."}}` with HTTP 401, 404, 422, or 503.

## Concepts

- Chamber: one physical viewing area. It has questions, views, an optional `context`, an optional webhook, and an optional SRT stream.
- Context: free text about the chamber (species, layout, what matters), up to 600 words. It is added to every analysis prompt. Each image can also carry its own `context` (up to 200 words). Answers still come only from what is visible.
- Question (an "eval" in the dashboard): `{key?, prompt, type, min?, max?}`. `type` is `boolean` or `integer`. No floats, no strings. An answer is null when the image cannot decide it.
- Key: the snake_case answer name. Omit it and Farsight makes one from the prompt: filler words (a, the, is, how many, ...) are dropped and the rest joined, so "Is a fish visible?" becomes `fish_visible`. A clash gets a number: `fish_visible_2`.
- View: one camera position in a chamber. Pass `view` (id or name) with an image to pin it; a new name creates the view. Omit it and Farsight matches the image to a known view, or creates one.
- Observation: one image plus its analysis. `status` is `queued`, `processing`, `complete`, or `failed`.

## Quick start

```sh
KEY=fs_your_key
# 1. Create a chamber with questions
curl -s https://farsight.observer/v1/chambers -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" -d '{
  "name": "Moth box A",
  "context": "Silkworms on mulberry. Cocoons are white and oval. Adults are pale grey moths.",
  "questions": [
    {"key": "cocooning_stage", "prompt": "Is any caterpillar spinning or inside a cocoon?", "type": "boolean"},
    {"prompt": "How many adult moths are visible?", "type": "integer", "min": 0}
  ],
  "webhook_url": "https://example.com/farsight"
}'

# 2. Send an image (raw bytes; view is optional)
curl -s "https://farsight.observer/v1/chambers/CHAMBER_ID/observations?view=top" -H "Authorization: Bearer $KEY" \
  -H "Content-Type: image/jpeg" --data-binary @frame.jpg

# 3. Read the result (or wait for the webhook)
curl -s https://farsight.observer/v1/observations/OBSERVATION_ID -H "Authorization: Bearer $KEY"
```

## Endpoints

### Chambers

- `GET /v1/chambers` list.
- `POST /v1/chambers` body `{name, context?, snapshot_minutes?, questions?, webhook_url?}`. `snapshot_minutes` is 1 to 1440, default 120. `context` is up to 600 words; send null or "" to clear it.
- `GET /v1/chambers/{id}` full chamber with questions, views, webhook, stream.
- `PATCH /v1/chambers/{id}` any of the create fields. `questions` replaces the whole list.
- `DELETE /v1/chambers/{id}` deletes the chamber, its images, and its stream.

### Questions

- `GET /v1/chambers/{id}/questions`
- `POST /v1/chambers/{id}/questions` body `{key?, prompt, type, min?, max?}`. Same key updates it. No key makes a new one from the prompt and returns it. `min` and `max` are optional for integers.
- `DELETE /v1/chambers/{id}/questions/{key}`

### Observations

- `POST /v1/chambers/{id}/observations` returns 202 and the queued observation. Send the image one of three ways:
  - raw bytes with `Content-Type: image/jpeg|png|webp|gif`, options in the query string: `?view=top&context=...&captured_at=2026-01-01T00:00:00Z`
  - `multipart/form-data` with field `image`, plus optional `view`, `context`, and `captured_at` fields
  - JSON `{"image_base64": "...", "view": "top", "context": "Lid was opened at 09:00."}` or `{"image_url": "https://..."}`
  `context` is optional, up to 200 words, and applies to this image only.
  Max 10 MB and 60 images per chamber per minute (422, field `rate`). `captured_at` is ISO 8601 or unix seconds/ms; default is receipt time.
- `GET /v1/chambers/{id}/observations?limit=20&cursor=...&view=VIEW_ID` newest first; follow `next_cursor`.
- `GET /v1/observations/{id}`
- `GET /v1/observations/{id}/image`

Observation shape:
```json
{
  "id": "ob_...", "chamber_id": "ch_...", "status": "complete", "source": "api",
  "view": {"id": "vw_...", "name": "top"},
  "context": null,
  "answers": {"cocooning_stage": true, "moth_count": 0},
  "note": "Two caterpillars on the left branch; one partly wrapped in silk.",
  "error": null,
  "image_url": "https://farsight.observer/v1/observations/ob_.../image",
  "captured_at": "...", "created_at": "...", "completed_at": "..."
}
```
Analysis usually finishes in 3 to 15 seconds. Poll `GET /v1/observations/{id}` about every 2 seconds, or use the webhook.

### Views

- `GET /v1/chambers/{id}/views`
- `PATCH /v1/chambers/{id}/views/{view_id}` body `{name}`
- `DELETE /v1/chambers/{id}/views/{view_id}`
- `GET /v1/views/{view_id}/image` the reference frame used for matching.

### Webhook

- Set `webhook_url` on the chamber (https). Events: `observation.completed`, `observation.failed`, `ping`.
- Body: `{"type": "observation.completed", "created_at": "...", "data": <observation>}`.
- Header `Farsight-Signature: t=<unix>,v1=<hex>` where hex is HMAC-SHA256 of `<t>.<raw body>` with the chamber's `webhook.secret`. Reject if t is more than 5 minutes old.
- Retries with backoff for up to 6 attempts on a non-2xx response.
- `POST /v1/chambers/{id}/webhook/test` sends a ping. `POST /v1/chambers/{id}/webhook/rotate` returns a new secret.

### Stream

- `POST /v1/chambers/{id}/stream` enables SRT ingest and returns `{srt_url, state, snapshot_minutes, playback_url, hls_url, public, public_url}`. `DELETE` disables it.
- Publish with any SRT caller, for example:
  `ffmpeg -re -i INPUT -c:v libx264 -preset veryfast -b:v 2500k -g 60 -c:a aac -f mpegts "SRT_URL"`
- `playback_url` (an embeddable player) and `hls_url` are set once the first broadcast is seen. The dashboard feed plays the stream live.
- Streams are private by default. `POST /v1/chambers/{id}/stream/public` makes one public and returns the stream with `public_url`: a signed-out page that shows only the live player. `DELETE /v1/chambers/{id}/stream/public` makes it private again; the old link stops working and the next public link is new. Disabling the stream also makes it private. A public page can stay cached for up to 30 seconds after it is made private.
- Every `snapshot_minutes` (minimum 1, default 120) Farsight takes a frame from the live stream and analyses it as an observation with `source: "stream"`.
- The last hour of video is kept as 60 second MP4 clips: `GET /v1/chambers/{id}/clips`, then `GET /v1/clips/{clip_id}`.

### Keys

- `GET /v1/keys`, `POST /v1/keys` body `{name}` returns the full key, `DELETE /v1/keys/{id}` revokes. The dashboard at https://farsight.observer/keys can show any active key again.

## Tips for agents

- Write each question prompt as a plain visual check a person could answer from one frame.
- Use integer questions for counts and levels. Add `min`/`max` when there is a real range (for example 0 to 5); answers are clamped to it.
- Put what you know about the animals and setup in the chamber `context`. It helps the model read the frame; it never replaces looking.
- Keep one camera per view name, and send `view` when you know it; matching is best effort.
