Docs

Farsight

Farsight watches scientific observation chambers (tanks, cages, terraria, rearing boxes with moths, fish, rats, and so on). You send images (or an SRT stream). Farsight analyses each image asynchronously and answers the chamber's questions as typed data: booleans and integers only.

Concepts

  • Chamber: one physical viewing area. It has questions, views, an optional webhook, and an optional SRT stream.
  • Question: {key, prompt, type, min?, max?}. key is snake_case. type is boolean or integer. No floats, no strings. An answer is null when the image cannot decide it.
  • 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

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",
  "snapshot_minutes": 5,
  "questions": [
    {"key": "cocooning_stage", "prompt": "Is any caterpillar spinning or inside a cocoon?", "type": "boolean"},
    {"key": "moth_count", "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, snapshot_minutes?, questions?, webhook_url?}. snapshot_minutes is 1 to 1440, default 5.
  • 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.
  • 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&captured_at=2026-01-01T00:00:00Z
    - multipart/form-data with field image, plus optional view and captured_at fields
    - JSON {"image_base64": "...", "view": "top"} or {"image_url": "https://..."}
    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:

{
  "id": "ob_...", "chamber_id": "ch_...", "status": "complete", "source": "api",
  "view": {"id": "vw_...", "name": "top"},
  "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 (SRT)

  • POST /v1/chambers/{id}/stream enables ingest and returns {srt_url, state, snapshot_minutes}. 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"
  • Every snapshot_minutes (minimum 1) 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 once, DELETE /v1/keys/{id} revokes.

Tips for agents

  • Write each question prompt as a plain visual check a person could answer from one frame.
  • Use integer questions with min/max for counts and levels (for example 0 to 5). Answers are clamped to the range.
  • Keep one camera per view name, and send view when you know it; matching is best effort.