๐ raw markdown for agents/LLMs: /API.md ยท all docs
Bluebut Read API (v1)
Read-only HTTP/JSON access to Bluebut's Facebook accounts and operational state (groups, people, posts, jobs) โ so people, scripts, and other Bofrid systems can fetch what the dashboard sees without the UI, the supabase service-role key, or any knowledge of keyvault/hostbun internals.
- Base URL (local):
http://localhost:8787/api/v1 - Base URL (hosted):
https://bluebut-bridge.blpk.cc/api/v1 - Method:
GETonly. Everything is read-only. - Format: JSON. Every response has an
ok: true|false. - Served by: the bridge (
ui/bridge/opencli-bridge.mjsโapi-v1.mjs). Run it withnpm run bridge.
Auth
Every data endpoint needs the shared password (default ddash). Pass it
either way:
# header
curl -H "Authorization: Bearer ddash" http://localhost:8787/api/v1/accounts
# or query param
curl "http://localhost:8787/api/v1/accounts?token=ddash"
/openapi.json and /docs are open (schema + a static page, no data).
Override the password with API_TOKEN in the root .env. Set API_TOKEN= (empty)
to disable auth entirely.
Secrets are never returned. FB passwords and cookie jars are stripped from every account row before it leaves the process โ callers see identity, status, and cookie presence (
has_cookies,cookie_count), never the credentials.
Interactive docs
GET /api/v1/docsโ a self-contained page (no CDN) listing every endpoint with a "Try it" button. Paste the password once; it's kept inlocalStorage.GET /api/v1/openapi.jsonโ the OpenAPI 3.1 spec (import into Postman / Insomnia / generate a client).
Endpoints
| Method | Path | Returns |
|---|---|---|
| GET | /accounts |
All FB accounts (keyvault registry, secrets stripped) |
| GET | /accounts/{slug} |
One account |
| GET | /accounts/health |
Cookie-only liveness sweep (no browser) |
| GET | /accounts/intel |
Offline cookie-truth (drift / contamination / stale / dead-page) |
| GET | /groups |
FB group list |
| GET | /groups/{id} |
Group detail: row + members (roles) + recent posts + our memberships |
| GET | /people |
People / leads (fb_person identity hub) |
| GET | /posts |
Group-post log (what Bluebut has posted) |
| GET | /jobs |
Live + recent job/queue state |
| GET | /summary |
Counts: accounts, groups, people, posts last 7d |
GET /accounts
The canonical account list = union of the bluebut-owned namespace and the droidfleet farm (ours wins). Secrets stripped.
{
"ok": true,
"count": 96,
"accounts": [
{
"account": "pojtie",
"display_name": "โฆ",
"account_type": "profile",
"fb_id": "61โฆ",
"last_status": "logged_in",
"kv_status": "active",
"source": "chromebox_sessions",
"proxy": { "scheme": "socks5h", "host": "โฆ", "port": 1080 },
"has_cookies": true,
"cookie_count": 9,
"updated_at": "2026-06-25Tโฆ"
}
]
}
GET /accounts/{slug}
{ "ok": true, "account": { โฆ } } or 404 { "ok": false, "error": "no account \"x\"" }.
GET /accounts/health?account=<slug>
Replays each stored cookie jar against facebook.com over plain HTTPS and reads
the inlined USER_ID โ live/dead. No browser. Omit account for the whole
fleet; pass it to check one (and get an egress-IP echo).
{ "ok": true, "total": 96, "live": 41,
"accounts": [ { "account": "pojtie", "valid": true, "status": "logged_in",
"fb_id": "61โฆ", "expected_id": "61โฆ", "name": "โฆ", "http": 200,
"via": "proxy", "egress_ip": "โฆ", "ms": 740, "error": null } ] }
GET /accounts/intel?account=<slug>
Decodes + reconciles every jar offline โ surfaces drift / contamination /
stale / dead-page that the stored fields hide. Verdicts:
ok | page | drift | contaminated | stale | logged_out.
{ "ok": true, "total": 96, "counts": { "ok": 40, "page": 3, "stale": 12 },
"accounts": [ { "account": "uthyra-se", "verdict": "contaminated",
"detail": "jar identity is pojtie", "uid": "61โฆ",
"identity_account": "pojtie", "identity_name": "โฆ",
"stored_fb_id": "61โฆ", "session_age_days": 4, "cookie_count": 9 } ] }
GET /groups
Query: limit (1โ500, default 100), offset, search (case-insensitive name),
is_test (true|false).
curl -H "Authorization: Bearer ddash" \
"http://localhost:8787/api/v1/groups?search=lidkoping&limit=20"
GET /groups/{id}
{id} = fb_group_id. Returns the group row plus:
membersโfb_group_memberedges (fb_user_id,role)recent_postsโ last 20group_postrowsour_membershipsโfb_account_group(which of our accounts are members)
Need all of a group's associated data (full roster with member identity, every post, scrape signals) in one shot, or to fetch it for every group at once? Hit the raw entity-graph surface directly โ see Direct entity-graph access below.
GET /people
Query: limit, offset, intent (seeker|landlord|unknown),
outreach_status (new|queued|contacted|replied|converted|closed), city,
first_seen_channel (admin_scrape|feed_post|dm_reply).
GET /posts
Query: limit, offset, fb_group_id, status
(pending|posted|failed|skipped). Newest first.
GET /jobs
Live bridge job state: { active, queued, recent, busyAccounts }. Same shape the
dashboard's Jobs page uses.
GET /summary
{ "ok": true,
"accounts": { "total": 96, "active": 90, "logged_in": 41 },
"groups": { "total": 412, "admin": 18 },
"people": { "total": 5230 },
"posts": { "last_7d": 12 },
"generated_at": "2026-06-25Tโฆ" }
Direct entity-graph access (PostgREST)
The curated /api/v1 endpoints above cover the common cases. For arbitrary
reads of the whole entity graph โ every group with every association embedded,
ad-hoc filters, joins the curated API doesn't expose โ query the raw PostgREST
surface that the dashboard itself speaks.
- Base URL:
https://bluebut-rest.blpk.cc/rest/v1 - Auth: the Supabase-shaped service-role JWT (
apikey+Authorization: Bearerheaders). Same key asVITE_SUPABASE_KEYin the root.env. - Shape: standard PostgREST โ
select=,order=,limit=,*.eq./*.in.filters, and resource embedding of related rows.
Fetch every group + ALL associated data in one query
As of 2026-06-30 the entity graph carries the foreign keys PostgREST needs
to embed a group's children, so a single select= pulls the whole tree:
KEY=<service-role-jwt>
curl -s -H "apikey: $KEY" -H "Authorization: Bearer $KEY" \
"https://bluebut-rest.blpk.cc/rest/v1/fb_group?select=\
fb_group_id,name,member_count,\
fb_account_group(chromebox_account,is_member),\
fb_group_member(role,fb_user(name)),\
group_post(status,created_at),\
fb_person_signal(signal_type),\
fb_post(post_url)"
Drop the trailing &limit=1 to get all 416 groups; add &order=member_count.desc,
&category=eq.housing, paginate with Range: headers, etc.
Embeddable relationships off fb_group (FK โ fb_group.fb_group_id):
| Embed | Table | Note |
|---|---|---|
fb_account_group(*) |
fb_account_group |
which of our accounts are members |
fb_group_member(*) |
fb_group_member |
FB roster + role; nest fb_user(*) for identity |
fb_group_invite(*) |
fb_group_invite |
pending admin/group invites |
group_post(*) |
group_post |
every post Bluebut published to the group |
fb_post(*) |
fb_post |
feed scrape results (nest fb_person(*) for poster) |
fb_person_signal(*) |
fb_person_signal |
observed events in the group |
fb_group_member โ fb_user, fb_post โ fb_person, and
fb_person_signal โ fb_person are likewise embeddable, so the roster/post rows
carry the person identity inline.
The FK declarations live in
adapters/facebook/SCHEMA.sql(idempotent). If aselect=โฆ(โฆ)embed ever returnsPGRST200 "could not find a relationship", the FK is missing or the PostgREST schema cache is stale โ re-apply the schema (it ends withNOTIFY pgrst, 'reload schema').
Errors
| Status | Meaning |
|---|---|
| 401 | Missing / wrong password |
| 404 | Unknown account / group / endpoint |
| 405 | Non-GET method (API is read-only) |
| 502 | Upstream (hostbun / keyvault) failure โ error carries the detail |
Notes
- CORS is open (
*) โ callable from a browser. - Account data is canonical in keyvault; groups/people/posts come from the
self-owned entity graph (
bluebut-dbvia PostgRESTbluebut-rest.blpk.cc); jobs are the bridge's in-memory registry. See the repoCLAUDE.md"Three data planes" for the full model.