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://. 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.
-
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"} -
Add its camera.
With an
external_idyou 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"}' -
Write the routine.
Add
"dry_run": trueto 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": []} -
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.
| Endpoint | Details | Answer |
|---|---|---|
GET /places | Your places, each with its cameras and their location words. | {"data": […]} |
POST /places | name, tz (IANA), kind: home, parent, rental, away, shop, project or other. | 201 |
POST /cameras | place_id, name, kind (camera or doorbell), location_words, optional external_id for idempotency. | 201 |
POST /events | camera_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 /routines | Optional place_id. Each with its read-back, mode and practice end. | {"data": […]} |
POST /routines | place_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 /outcomes | One 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 /alerts | Delivered 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(orsnapshotto store a frame for look-checks without reading it),X-Peekaboo-Event-TimeandX-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.
Example photo
Ask for API access.
The API isn’t open yet. Tell us what you’d build and a person will reply.