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 happened, what didn’t, and what couldn’t be seen. Early access: keys are issued from Settings once your account is enabled.
Basics
Base URL https://tendline-6iz.pages.dev/api/v1. JSON in and out, snake_case fields, times in epoch milliseconds.
AuthorizationEvery request:Bearer tl_live_…. Keys are shown once when created and can be revoked any time.Settings › Partner APIRate limits1,200 requests an hour per key; 240 events an hour per camera.429 + messageErrorsA status code and{"ok": false, "error": "code", "message": "…"}written for people.400, 401, 404, 422, 429PracticeNew routines run a silent practice week unless you create them with"mode": "live".7 days
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.
curl -s https://tendline-6iz.pages.dev/api/v1/places \
-H "Authorization: Bearer $TENDLINE_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"}
curl -s https://tendline-6iz.pages.dev/api/v1/cameras \
-H "Authorization: Bearer $TENDLINE_KEY" -H "content-type: application/json" \
-d '{"place_id": "p_…", "name": "Entry", "kind": "doorbell", "location_words": ["front door"], "external_id": "nvr-7"}'
# external_id makes this call safe to repeat: the same camera comes back.
curl -s https://tendline-6iz.pages.dev/api/v1/routines \
-H "Authorization: Bearer $TENDLINE_KEY" -H "content-type: application/json" \
-d '{"place_id": "p_…", "text": "Tell me if the cleaner hasn'"'"'t arrived by 11 AM on Saturdays", "mode": "live"}'
# → {"id": "r_…", "title": "…", "kind": "expect", "mode": "live", "read_back": "…", "notes": [], "unresolved_locations": []}
# Add "dry_run": true to see the read-back without saving.
curl -s https://tendline-6iz.pages.dev/api/v1/events \
-H "Authorization: Bearer $TENDLINE_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
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.201POST /camerasplace_id,name,kind(camera or doorbell),location_words, optionalexternal_idfor idempotency.201POST /eventscamera_id, and animage(base64 JPEG/PNG, under 1.5 MB), alabel(person, car, package, animal, motion), or both. Optionalts, clamped to the last 15 minutes. A label alone is recorded without reading a frame.observation + alertsGET /routinesOptionalplace_id. Each with its read-back, mode and practice end.{"data": […]}POST /routinesplace_id,text, optionalmode(practice or live) anddry_run. Unreadable text returns 422 with a reason.201POST /routines/{id}{"mode": "practice" | "live" | "paused"}200DELETE /routines/{id}Pauses it (history is kept).200GET /outcomesOne row per routine window:met,missed,unconfirmed,violated,okorpending, with first and last seen times and the coverage line. Optionalsince,place_id. Last 8 days.{"data": […]}GET /alertsDelivered alerts (practice alerts are left out). Optionalsince. 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, Tendline tries again 2, 4, 8 and 16 minutes later with the same event id (and X-Tendline-Attempt), so dedupe on the id. After that, use GET /alerts with since to catch up.
// X-Tendline-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
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-Tendline-Label (or snapshot to store a frame for look-checks without reading it), X-Tendline-Event-Time and X-Tendline-Event-Id (retries with the same id are ignored) headers. That’s how the Home Assistant and Frigate setup works.
Team chats (Slack, Discord, Google Chat, Mattermost) connect in Settings with an incoming-webhook URL; no code needed.
The same product rules apply to every integration: no identity, no audio, no verdicts, and “couldn’t confirm” when a camera couldn’t see. Principles.
Ask for API access
The Partner API is in early access. Leave your email with Partner / API ticked and tell us which cameras or software you work with. A person will reply.
The free plan will stay free. Joining the list costs nothing and commits you to nothing.