REST API

Read and write your data over PostgREST at api.busymate.net, with row-level security.

Read and write your Busymate data over a standard PostgREST API at https://api.busymate.net/rest/v1/, with row-level security and role-based access enforced on every call.

api.busymate.net is a CNAME to the Busymate database, so everything here is plain PostgREST — the same REST surface any Supabase project exposes. If you've used @supabase/supabase-js or hit a Supabase REST endpoint before, this will feel familiar; if not, every example below is a copy-pasteable curl.

TL;DR

Base URLhttps://api.busymate.net/rest/v1/
AuthAn OAuth access token as both Authorization: Bearer <token> and apikey: <token>.
ReadsGET /rest/v1/<table>?select=… with PostgREST filters (?col=eq.value).
WritesPOST / PATCH / DELETE on /rest/v1/<table>.
RPCsPOST /rest/v1/rpc/<name> with a JSON body of named args.
Access controlPostgres RLS + has_capability(section, action) — you only see/touch what your role allows.

Authentication

Every request needs an OAuth access token — the same token type that authenticates MCP, Realtime, and the BusyBro ask API. There is one token, three ways in.

Pass it in two headers:

http
Authorization: Bearer <token>
apikey: <token>

Yes, the token goes in both headers — apikey is the PostgREST gateway key and Authorization is your identity. Put the same OAuth token in each.

How you obtain the token:

  • External clients — run the OAuth 2.1 flow (Dynamic Client Registration + PKCE) against the MCP server. See MCP → Connect.
  • In the dashboard — the logged-in dashboard exchanges your session for the identical token at POST /first-party-token; you don't normally handle it by hand.

The token carries your user identity and role, so the database knows who you are and what you're allowed to do on every call.

What you can reach — and what governs it

Every table lives in the public schema and has row-level security (RLS) enabled. On top of RLS, most reads and writes are gated by capabilities — your role grants view / edit on sections like devices, entries, settings, services, tags, users. The database checks has_capability(section, action) for you; a token whose role lacks the capability simply gets an empty result (for reads) or a permission error (for writes). See Roles & permissions for the model.

Practically, that means:

  • Admins (a role that grants every capability) read and write the whole fleet.
  • Scoped roles are limited to the rows they own — e.g. captures and settings for devices you own (owner_user_id = auth.uid()).

Tables you'll typically read or write:

TableHoldsTypical use
devicesOne row per connected iOS app / proxy / bmc instance (uuid, name, os, owner_user_id, setup).List your fleet; rename a device.
device_statusLive heartbeat per device (ip, vpn_state, source, country). Online-ness is derived (last_heartbeat_at > now() - 90s).Show who's online.
entriesThe capture firehose — one row per request/response pair. payload is the full TrafficLogEntry; host is a generated column. Partitioned weekly.Read captured traffic (prefer the filtered RPCs below for search).
workspaces / workspace_tabs / workspace_entriesCapture scopes, their tabs, and the entry↔workspace links.Organise the feed.
service_groupsProxyman-style "Apps" — wildcard SSL-proxy domain lists.Configure which domains get decrypted.
service_group_agentsJoin linking a service group to BusyBro agents (agent_id, is_primary, position); exactly one primary per group.Set which specialist agents a service uses (RLS mirrors service_groups).
busybro_agentsThe unified BusyBro specialist-agent registry (slug, label, instructions, model) — authored under BusyBro → Agents, linked to service groups + delegated to by run_task.Define reusable expert personas.
tags / entry_tagsUser labels and their join to entries.Tag captures.
settings_global / settings_deviceGlobal defaults and per-device overrides (data jsonb, breakpoint patterns, env vars).Read or change settings.
breakpoint_eventsDurable pause records (resumed_at IS NULL = currently held).Inspect held requests.
service_stats / service_statusSingle-row live snapshots — see Stats & status.Monitor the fleet.

Backend internals — migrations, triggers, partitioning, the device-JWT path — live under Under the Hood. This page is the developer-facing read/write surface.

Reading data

Standard PostgREST. Select columns with select, filter with ?col=<op>.<value>, order, and paginate.

List your devices:

bash
curl -s "https://api.busymate.net/rest/v1/devices?select=uuid,name,os,owner_user_id&order=name.asc" \
  -H "Authorization: Bearer $TOKEN" \
  -H "apikey: $TOKEN"

Read the most recent captures for one device:

bash
curl -s "https://api.busymate.net/rest/v1/entries?select=id,ts,host,payload&device_uuid=eq.$DEVICE_UUID&order=ts.desc&limit=50" \
  -H "Authorization: Bearer $TOKEN" \
  -H "apikey: $TOKEN"

PostgREST operators you'll use most: eq, neq, in, gt/lt, ilike, is. Range pagination via Range headers or limit/offset.

Writing data

POST to insert, PATCH (with a filter) to update, DELETE (with a filter) to remove — each gated by your edit capability and RLS scope.

Rename a device you own:

bash
curl -s -X PATCH "https://api.busymate.net/rest/v1/devices?uuid=eq.$DEVICE_UUID" \
  -H "Authorization: Bearer $TOKEN" \
  -H "apikey: $TOKEN" \
  -H "content-type: application/json" \
  -H "Prefer: return=representation" \
  -d '{ "name": "Test iPhone" }'

If your role can't edit that section, or the row isn't in your scope, the write is rejected — RLS is the real gate, not the request shape.

Callable RPCs

Some operations are exposed as Postgres functions over POST /rest/v1/rpc/<name>. These are the ones meant for integrators (each granted to your authenticated role and scoped to what you're allowed to see):

RPCBody argsReturns
effective_settings_for_device{ "target_uuid": "<device-uuid>" }The merged settings document a device receives — global + per-device override, with service-group domains expanded. A device may only ask for its own uuid.
expand_service_groups{ "group_ids": ["<uuid>", …] }The union of ssl_proxy_domains across the given service groups.
entries_filtered_list{ "target_workspace": "<uuid>", "device_uuids": [...], "hosts": [...], "search_q": "stripe", "before_id": 123, "lim": 200 }Captured entries matching the filter, ordered newest-first with keyset pagination (before_id). Trigram-indexed search across host/path/url; scoped to your devices unless admin.
entries_filtered_count{ "target_workspace": "<uuid>", "device_uuids": [...], "hosts": [...], "search_q": "stripe" }The count matching the same filter (the denominator the feed shows).
get_stats{}A fresh fleet-stats snapshot. Requires the stats:view capability — see Stats & status.

Resolve a device's effective settings:

bash
curl -s -X POST "https://api.busymate.net/rest/v1/rpc/effective_settings_for_device" \
  -H "Authorization: Bearer $TOKEN" \
  -H "apikey: $TOKEN" \
  -H "content-type: application/json" \
  -d '{ "target_uuid": "'"$DEVICE_UUID"'" }'

Search captures across a workspace:

bash
curl -s -X POST "https://api.busymate.net/rest/v1/rpc/entries_filtered_list" \
  -H "Authorization: Bearer $TOKEN" \
  -H "apikey: $TOKEN" \
  -H "content-type: application/json" \
  -d '{ "target_workspace": "'"$WORKSPACE_UUID"'", "search_q": "api.stripe.com", "lim": 50 }'

Prefer entries_filtered_list / _count over a raw GET /entries?host=ilike.… for search — they force the trigram index and won't hit the statement timeout on large capture sets.

Using a client library

Anything that speaks PostgREST works. With @supabase/supabase-js, point it at the Busymate host and attach your OAuth token:

js
import { createClient } from "@supabase/supabase-js";
 
const supabase = createClient("https://api.busymate.net", PUBLISHABLE_KEY, {
  global: { headers: { Authorization: `Bearer ${TOKEN}` } },
});
 
const { data, error } = await supabase
  .from("devices")
  .select("uuid, name, os")
  .order("name");

The same client gives you Realtime and RPC calls (supabase.rpc("get_stats")) over the identical token.

Troubleshooting

SymptomCause / fix
401 on every requestMissing or expired token, or you only set one of the two headers. Pass the OAuth token in both Authorization: Bearer and apikey.
Read returns [] (empty array, no error)RLS/capability scope — your role lacks the view capability for that section, or the rows aren't yours. Check your role.
Write rejected with a permission errorYour role lacks the matching edit capability, or the row is outside your ownership scope.
Search GET /entries?host=ilike.… times out (57014)Use rpc/entries_filtered_list instead — it uses the trigram index.