API reference

The REST routes and tRPC procedures SafeCircle's own apps call, with auth requirements, inputs, outputs and rate limits.

8 min read

First-party API

This is the API SafeCircle's apps use. It is unversioned and can change without notice, and third-party access is not offered yet. Admin-only procedures exist but are intentionally not documented here.

Base URL: https://api.safecircle.tech

Calling conventions

Data lives under /trpc. Authentication and a few utility routes are plain REST.

tRPC wire format

The backend runs tRPC without a data transformer, so inputs and outputs are plain JSON. Dates come back as ISO 8601 strings. You can call a procedure in single or batch form.

Put the input, URL-encoded JSON, in the input query parameter. The result is in result.data.

curl "https://api.safecircle.tech/trpc/alerts.list?input=%7B%22childId%22%3A%22CHILD_UUID%22%2C%22limit%22%3A20%7D" \
  -H "Authorization: Bearer SESSION_TOKEN"
{ "result": { "data": [ { "id": "…", "riskLevel": "high" } ] } }

The dashboard calls queries with GET and the input parameter, and mutations with a plain POST. Its helper also accepts a json wrapper in responses, which the backend does not currently emit.

Errors

tRPC errors use the standard envelope with an HTTP status and a message.

CodeStatusTypical cause
UNAUTHORIZED401No valid session or device token
FORBIDDEN403The resource belongs to someone else
NOT_FOUND404Unknown ID, or an ID you do not own
BAD_REQUEST400Failed validation, or an invalid or expired pairing code
TOO_MANY_REQUESTS429You hit a rate limit (this one comes from the Worker, not tRPC)

Auth levels

LevelHow you authenticateNotes
PublicNothingOnly devices.pair and search.familyRules
SessionBetter Auth session, as a cookie or Authorization: BearerAny signed-in user
ParentSessionAlmost every data procedure
Devicex-device-token header from pairingChild app only

See Authentication for how each credential is obtained.

tRPC procedures

All procedures below are called as /trpc/<router>.<procedure>. Parent procedures only return data for children that belong to the signed-in parent.

children

ProcedureTypeAuthInputOutput
children.listqueryParentnoneChildren, oldest first, each with devices (id, deviceName, lastSeen, pairedAt) and alerts (IDs of unread alerts)
children.createmutationParentname: 1–50 characters, letters, numbers, spaces, hyphens and apostrophesThe new child

devices

ProcedureTypeAuthInputOutput
devices.createPairingTokenmutationParentchildId (UUID)token (6 digits), expiresAt (5 minutes). Invalidates earlier unused codes for that child
devices.pairmutationPublicpairingToken, deviceName (1–100 characters)deviceToken, deviceId, childId. The code is single-use
devices.listqueryParentchildIdDevices of that child
devices.revokemutationParentdeviceId{ ok: true }. The device token stops working immediately
devices.registerFcmTokenmutationParenttoken, platform (android or ios, default android){ ok: true }. Re-registering an existing token moves it to the current account

alerts

ProcedureTypeAuthInputOutput
alerts.createmutationDeviceriskLevel (medium or high), categories (at least one string), confidence (0–1), reasoning, appSource{ id }. Triggers push delivery
alerts.listqueryParentchildId, limit (1–100, default 50)Alerts, newest first
alerts.getqueryParentalertIdOne alert
alerts.markReadmutationParentalertId{ ok: true }

An alert has id, deviceId, childId, riskLevel, categories, confidence, reasoning, appSource, readAt (null while unread) and createdAt.

analytics

All three accept an optional childId. Without it they aggregate across all your children.

ProcedureInputOutput
analytics.alertsByDaydays (1–365, default 30)[ { date, count } ]
analytics.alertsByCategorynone[ { category, count } ], highest first
analytics.alertsByRiskLevelnone{ high, medium }

activity

ProcedureInputOutput
activity.listcursor (ISO timestamp, optional), limit (1–50, default 20){ items, nextCursor }. Items are alerts and device pairings, newest first. Pass nextCursor as cursor for the next page

An item has id, type (alert or device_paired), childId, childName, description, createdAt and a small meta object.

notifications

ProcedureTypeInputOutput
notifications.getquerynonehighRisk, mediumRisk, quietStart, quietEnd. Defaults to both on and no quiet hours
notifications.updatemutationchildId (optional, null for the global setting), highRisk, mediumRisk, quietStart, quietEnd (HH:mm)The saved values

notifications.update saves the whole record. A field you omit goes back to its default (true for the toggles, empty for quiet hours), so always send all of them. Quiet hours use the UTC clock and apply to medium alerts only.

users

ProcedureTypeAuthOutput
ProcedureTypeAuthInputOutput
search.getSettingsqueryParentnone{ prefs, siteRules }
search.saveSettingsmutationParentprefs and siteRules (up to 500 rules, each a domain and an action of block, lower or raise){ ok: true }
search.familyLinkmutationDevicenone{ url }, a signed link that locks safe search on the child's device
search.familyRulesqueryPublictoken{ siteRules }, or null if the token is invalid or the device was unpaired

See SafeCircle Search for what the family link does.

REST routes

Method and pathAuthPurpose
GET /NoneLiveness probe, returns OK
GET /healthNone{ status, version, service }
* /api/auth/*VariesBetter Auth: sign-in, sign-up, sessions, 2-step verification, passkeys. See Authentication
POST /api/feedbackSession or x-device-tokenSend a bug report or feature request
POST /api/model-feedbackNoneRating of an AI Playground result
POST /api/telemetry/eventNoneOne anonymous product event. Returns 204
GET, PUT, DELETE /api/media/{users or children}/:id/{avatar or banner}SessionProfile photo and banner
POST /api/telegram/generate-link-code, GET /api/telegram/status, POST /api/telegram/unlink-selfSessionTelegram linking from the dashboard

Other /api/telegram/* routes are called by the bot with a private key, and GET /api/telemetry/metrics is protected by a secret. Neither is available to third parties.

Feedback

POST /api/feedback with a JSON body:

{
  "type": "bug",
  "title": "Short summary",
  "description": "What happened",
  "context": { "source": "dashboard", "screen": "alerts", "version": "0.3.2", "os": "optional" }
}

type is bug or feature. source is dashboard, parent-app, child-app or playground. The title is 2–200 characters and the description up to 5000. A child app can authenticate with its device token, and the report is filed on behalf of the paired parent. The response is 201 with { "postUrl": "…" }.

Profile images

  • GET returns the image. It is private to the owner, and admins can read.
  • PUT takes the raw image bytes with a Content-Length. The limit is 1 MB. The type is detected from the file contents, and only JPEG, PNG and WebP are accepted (415 otherwise).
  • DELETE removes it and returns 204.
  • Writes are limited to 20 per minute per IP.

Telemetry event

POST /api/telemetry/event with { "name": "page_view", "route": "/alerts", "props": {} }. name is 1–64 characters. At most 10 props are kept, and any property whose key looks like an ID, email, user, token, name, IP or phone is dropped. See Observability.

Rate limits

Limits are per client IP, counted per Worker instance, and enforced with 429 Too Many Requests and a Retry-After header.

ScopeLimit
All routes100 requests per minute
/api/auth/* (except get-session)30 requests per minute
GET /api/auth/get-sessionCounted in the global 100 per minute
Image uploads and deletes20 per minute
SafeCircle Search (separate service)30 per minute anonymous, 120 per minute signed in

Treat these as abuse protection, not as a quota you can plan around. Cache session lookups and batch your tRPC calls.