API reference
Matilda has no versioned public API for third parties. The endpoints below exist for the BlueBubbles server, the Matilda Worker, the web chat, and the dashboard's own forms.
Public and token-gated
GET /health
Returns {"status": "ok", "agent": "matilda"}. Open.
POST /webhook
Receives BlueBubbles new-message events. When a webhook token exists, the request must carry it as ?token=. If no token has ever been generated and none is set in the environment, the route accepts unauthenticated posts.
Events are de-duplicated on message GUID within a 120-second, 1,000-entry window.
Worker API
Authenticated with a per-device bearer key. The worker mints the key on the Mac during Connect Follow Up Boss and registers only its SHA-256 hash. See Matilda Worker.
POST /api/worker/device/start
Unauthenticated. Body: {"key_id", "secret_hash", "label"}. Registers a pending device and returns {"ok": true, "url": ...}, the Follow Up Boss sign-in link the worker opens in the browser. The device becomes active once sign-in completes.
POST /api/worker/fub/start
Returns a fresh sign-in link for an already-linked device. Used by Reconnect.
POST /api/worker/link
Body: {"tracked": N}, where tracked is how many conversations the Mac is syncing. Validates the key and returns {"ok": true, "device": ..., "workspace": ..., "fub": {...}, "tracking": {"used", "limit"}}. The worker calls this roughly every 20 seconds as a heartbeat. A device is shown as online when it has linked within the last 90 seconds.
POST /api/worker/unlink
Removes the device. Used by Sign out in the worker.
POST /api/worker/ingest
Body: {"conversations": [...], "tracked": N, "suggestions": true}. Stores messages with source personal_imessage, de-duplicates by GUID, runs extraction, records lead events, and creates or enriches the person in the Mac's CRM. Returns {"ok": true, "processed": N, "results": [...]}. Never auto-replies.
With "suggestions": true, the notes, tasks, appointments, and stage move extraction finds, and the drip campaigns that fit (Follow Up Boss only), are not written to the CRM. They become suggestions, and each result carries "suggestions": [...], every open suggestion for that lead, or [] while the lead isn't in the CRM. Without the flag, as older workers send it, notes, tasks, appointments, and the stage go straight to the CRM.
A suggestion looks like this:
{"id": 12, "kind": "task", "label": "Task: send listings", "detail": "Due Fri, Oct 10",
"reason": null, "editable": true, "title": "send listings", "text": null,
"due": "2026-10-10", "start": null, "stage": null, "plan_id": null,
"state": "open", "created_at": 1760000000.0, "expires_at": 1760140800.0}
kind is plan, task, appointment, stage, or note. due is a date and start a local time (YYYY-MM-DDTHH:MM) in the lead's time zone. expires_at is when a task or appointment goes out of date, in epoch seconds, or null.
Suggestions and drip campaigns
Every route below is a POST with the device key. A handle is the lead's phone number in any US format, and id and plan_id may be numbers or numeric strings. Errors are {"error": ...}: 400 for bad input, 404 not_found for an id that isn't this Mac's, 409 not_open, not_accepted, or not_synced (the lead isn't in the Mac's CRM), and 502 crm_error with a short detail when the CRM refuses.
| Path | Body | Returns |
|---|---|---|
/api/worker/suggestions | {"handle"}, or {} for every lead | {"ok", "handle", "suggestions"}, or {"ok", "leads": {"+1...": [...]}} |
/api/worker/suggestions/accept | {"id", "edits"?}, edits being title, text, due, or start | {"ok", "handle", "suggestion", "summary", "suggestions"} |
/api/worker/suggestions/dismiss | {"id"} | {"ok", "handle", "suggestions"} |
/api/worker/suggestions/undo | {"id"} | {"ok", "handle", "suggestion", "suggestions"} |
/api/worker/crm/plans | {"handle"} | {"ok", "handle", "available", "reason", "plans"} |
/api/worker/crm/plans/toggle | {"handle", "plan_id", "on"} | The same as /crm/plans, plus "suggestions" |
suggestions in each answer is the lead's open suggestions after the change. summary is the activity line, such as Added task: send listings (due Fri, Oct 10). A plan is {"id", "name", "on", "status", "suggested", "reason", "suggestion_id"}, status being running, paused, finished, or null. available is false, with a reason, when the CRM has no drip campaigns (Lofty), the lead isn't in the CRM yet, or Follow Up Boss refuses to list action plans.
Customer session
Cookie-gated. The cookie is set by the login flow below.
| Method and path | Purpose |
|---|---|
POST /api/login/start | Body {"phone", "email"}. Always returns {"ok": true} regardless of match. Rate limited per IP and per phone. |
POST /api/login/verify | Body {"phone", "code"}. Sets the session cookie on success. |
POST /api/logout | Clears the cookie. |
POST /api/chat | Body {"message"}, capped at 4,000 characters. Returns {"reply"}. |
GET /api/chat/history | ?limit= clamped to 200. |
GET /api/account | Returns name, email, timezone, and reminders flag. |
POST /api/account | Updates the same fields. |
Dashboard endpoints
Session-gated and role-gated on the server. The UI only hides what a request would refuse.
| Method and path | Min role | Purpose |
|---|---|---|
POST /dashboard/ask | Viewer | Portfolio Q&A |
POST /dashboard/settings | Admin, Owner for secrets | Save integration settings |
POST /dashboard/test/{service} | Admin | Run a connectivity test |
POST /dashboard/webhook-token | Owner | Generate or rotate the webhook token |
POST /dashboard/tasks/create | Admin | Create a task |
POST /dashboard/routines/create, .../delete | Admin | Manage routines |
POST /dashboard/contractors/create, .../delete | Admin | Manage contractors |
POST /dashboard/contact/{phone}/fub-sync | Admin | Sync a lead to Follow Up Boss |
POST /dashboard/contact/{phone}/delete | Admin | Delete a lead |
POST /dashboard/devices/{key_id}/limit, .../{key_id}/delete | Owner | Set a Mac's sync limit, remove a Mac |
POST /dashboard/integrations/request, .../{id}/status | Viewer to request, Admin to triage | Integration requests |
POST /dashboard/profile/password | Viewer | Change own password |
POST /dashboard/team/* | Admin, Owner for owner changes | Organization, teams, members, roles, demo switch |
See Organization and roles for the full permission table.