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 URL | https://api.busymate.net/rest/v1/ |
| Auth | An OAuth access token as both Authorization: Bearer <token> and apikey: <token>. |
| Reads | GET /rest/v1/<table>?select=… with PostgREST filters (?col=eq.value). |
| Writes | POST / PATCH / DELETE on /rest/v1/<table>. |
| RPCs | POST /rest/v1/rpc/<name> with a JSON body of named args. |
| Access control | Postgres 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:
Authorization: Bearer <token>
apikey: <token>Yes, the token goes in both headers —
apikeyis the PostgREST gateway key andAuthorizationis 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:
| Table | Holds | Typical use |
|---|---|---|
devices | One row per connected iOS app / proxy / bmc instance (uuid, name, os, owner_user_id, setup). | List your fleet; rename a device. |
device_status | Live heartbeat per device (ip, vpn_state, source, country). Online-ness is derived (last_heartbeat_at > now() - 90s). | Show who's online. |
entries | The 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_entries | Capture scopes, their tabs, and the entry↔workspace links. | Organise the feed. |
service_groups | Proxyman-style "Apps" — wildcard SSL-proxy domain lists. | Configure which domains get decrypted. |
service_group_agents | Join 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_agents | The 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_tags | User labels and their join to entries. | Tag captures. |
settings_global / settings_device | Global defaults and per-device overrides (data jsonb, breakpoint patterns, env vars). | Read or change settings. |
breakpoint_events | Durable pause records (resumed_at IS NULL = currently held). | Inspect held requests. |
service_stats / service_status | Single-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:
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:
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:
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):
| RPC | Body args | Returns |
|---|---|---|
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:
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:
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/_countover a rawGET /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:
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
| Symptom | Cause / fix |
|---|---|
401 on every request | Missing 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 error | Your 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. |