Partners / API reference

Places, cameras and routines in. Outcomes out.

One JSON API for camera software, installers and property tools. Write routines in plain English, send frames or your own detector’s labels, and get back what was seen, what wasn’t seen by its time, and what couldn’t be seen.

The API isn’t open yet. Tell us what you’d build and a person will reply.

Basics.

Base URL https://getpeekaboo.pages.dev/api/v1. JSON in and out, snake_case fields, times in epoch milliseconds.

  • AuthorizationEvery request carries Bearer tl_live_…. Keys are shown once when created and can be revoked any time, under Settings › Partner API.
  • Rate limits1,200 requests an hour per key; 240 events an hour per camera. Past either, the answer is 429 with a message.
  • ErrorsA status code (400, 401, 404, 422 or 429) and {"ok": false, "error": "code", "message": "…"}, with the message written for people.
  • PracticeNew routines run a silent practice week of 7 days unless you create them with "mode": "live".

A complete integration in four calls.

A rental manager’s cleaner check: the place, its entry camera, one routine, then events from the camera system.

  1. Create the place.

    curl -s https://getpeekaboo.pages.dev/api/v1/places \
      -H "Authorization: Bearer $PEEKABOO_KEY" -H "content-type: application/json" \
      -d '{"name": "Unit 4B", "kind": "rental", "tz": "America/New_York"}'
    # → {"id": "p_…", "name": "Unit 4B", "kind": "rental", "tz": "America/New_York"}
  2. Add its camera.

    With an external_id you can repeat this call: the same camera comes back.

    curl -s https://getpeekaboo.pages.dev/api/v1/cameras \
      -H "Authorization: Bearer $PEEKABOO_KEY" -H "content-type: application/json" \
      -d '{"place_id": "p_…", "name": "Entry", "kind": "doorbell", "location_words": ["front door"], "external_id": "nvr-7"}'
  3. Write the routine.

    Add "dry_run": true to see the read-back without saving.

    curl -s https://getpeekaboo.pages.dev/api/v1/routines \
      -H "Authorization: Bearer $PEEKABOO_KEY" -H "content-type: application/json" \
      -d '{"place_id": "p_…", "text": "Tell me if the cleaner isn'"'"'t seen by 11 AM on Saturdays", "mode": "live"}'
    # → {"id": "r_…", "title": "…", "kind": "expect", "mode": "live", "read_back": "…", "notes": [], "unresolved_locations": []}
  4. Send events.

    curl -s https://getpeekaboo.pages.dev/api/v1/events \
      -H "Authorization: Bearer $PEEKABOO_KEY" -H "content-type: application/json" \
      -d '{"camera_id": "c_…", "label": "person", "ts": 1762455600000, "image": "<base64 JPEG>"}'
    # → {"observation": {"usable": true, "people": 1, "packages": 0, "carriers": [], "description": "…"}, "alerts": []}

Endpoints.

EndpointDetailsAnswer
GET /placesYour places, each with its cameras and their location words.{"data": […]}
POST /placesname, tz (IANA), kind: home, parent, rental, away, shop, project or other.201
POST /camerasplace_id, name, kind (camera or doorbell), location_words, optional external_id for idempotency.201
POST /eventscamera_id, and an image (base64 JPEG/PNG, under 1.5 MB), a label (person, car, package, animal, motion), or both. Optional ts, clamped to the last 15 minutes. A label alone is recorded without reading a frame.observation + alerts
GET /routinesOptional place_id. Each with its read-back, mode and practice end.{"data": […]}
POST /routinesplace_id, text, optional mode (practice or live) and dry_run. Unreadable text returns 422 with a reason.201
POST /routines/{id}{"mode": "practice" | "live" | "paused"}200
DELETE /routines/{id}Pauses it (history is kept).200
GET /outcomesOne row per routine window: met, missed, unconfirmed, violated, ok or pending, with first and last seen times and the coverage line. Optional since, place_id. Last 8 days.{"data": […]}
GET /alertsDelivered alerts (practice alerts are left out). Optional since. Last 8 days.{"data": […]}

Webhooks.

Add a webhook in Settings and every alert is POSTed to it as alert.created, signed with the secret shown when you added it. A test event is sent when you connect.

If your endpoint doesn’t answer 2xx within 5 seconds, Peekaboo tries again 2, 4, 8 and 16 minutes later with the same event id (and X-Peekaboo-Attempt), so dedupe on the id. After that, use GET /alerts with since to catch up.

// X-Peekaboo-Signature: t=1762455600,v1=<hex>
// v1 = HMAC-SHA256(secret, `${t}.${rawBody}`)
import { createHmac, timingSafeEqual } from "node:crypto";

export function verify(rawBody, header, secret, toleranceSec = 300) {
  const { t, v1 } = Object.fromEntries(header.split(",").map(p => p.split("=")));
  if (Math.abs(Date.now() / 1000 - Number(t)) > toleranceSec) return false;
  const want = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest();
  const got = Buffer.from(v1 ?? "", "hex");
  return got.length === want.length && timingSafeEqual(got, want);
}

// Body:
// {"type": "alert.created", "id": "x_…", "created": 1762455600000,
//  "data": {"kind": "missed", "title": "The cleaner wasn't seen by 11 AM",
//           "body": "…", "coverage": null, "routine_id": "r_…", "run_key": "2026-11-07",
//           "place": {"id": "p_…", "name": "Unit 4B"}, "recipients": ["me"]}}

Simpler than an API.

  • A push URL for each camera.

    If you only need to send pictures from one system, each camera can have its own private push URL instead: POST the JPEG, optionally with X-Peekaboo-Label (or snapshot to store a frame for look-checks without reading it), X-Peekaboo-Event-Time and X-Peekaboo-Event-Id (retries with the same id are ignored) headers. That’s how the Home Assistant and Frigate setup works.

  • Team chats, with no code.

    Slack, Discord, Google Chat and Mattermost connect in Settings with an incoming-webhook URL.

  • The same rules, whichever way.

    No identity, no audio, no verdicts, and “couldn’t confirm” when a camera couldn’t see. Principles.

A wet street at night. Houses with closed garage doors are lit by their own lamps.

Example photo

Ask for API access.

The API isn’t open yet. Tell us what you’d build and a person will reply.

Cameras you work with

If the button doesn’t respond, email admin1@sovnode.ai.