Skip to main content

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.

PathBodyReturns
/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 pathPurpose
POST /api/login/startBody {"phone", "email"}. Always returns {"ok": true} regardless of match. Rate limited per IP and per phone.
POST /api/login/verifyBody {"phone", "code"}. Sets the session cookie on success.
POST /api/logoutClears the cookie.
POST /api/chatBody {"message"}, capped at 4,000 characters. Returns {"reply"}.
GET /api/chat/history?limit= clamped to 200.
GET /api/accountReturns name, email, timezone, and reminders flag.
POST /api/accountUpdates the same fields.

Dashboard endpoints​

Session-gated and role-gated on the server. The UI only hides what a request would refuse.

Method and pathMin rolePurpose
POST /dashboard/askViewerPortfolio Q&A
POST /dashboard/settingsAdmin, Owner for secretsSave integration settings
POST /dashboard/test/{service}AdminRun a connectivity test
POST /dashboard/webhook-tokenOwnerGenerate or rotate the webhook token
POST /dashboard/tasks/createAdminCreate a task
POST /dashboard/routines/create, .../deleteAdminManage routines
POST /dashboard/contractors/create, .../deleteAdminManage contractors
POST /dashboard/contact/{phone}/fub-syncAdminSync a lead to Follow Up Boss
POST /dashboard/contact/{phone}/deleteAdminDelete a lead
POST /dashboard/devices/{key_id}/limit, .../{key_id}/deleteOwnerSet a Mac's sync limit, remove a Mac
POST /dashboard/integrations/request, .../{id}/statusViewer to request, Admin to triageIntegration requests
POST /dashboard/profile/passwordViewerChange own password
POST /dashboard/team/*Admin, Owner for owner changesOrganization, teams, members, roles, demo switch

See Organization and roles for the full permission table.