Skip to main content

Inbound Email API

/api/inbound-emails backs the Inbound Email screen — every email SABLE has ingested, with the project it landed on or the candidates for one.

GET /api/inbound-emails?scope=all|unassigned|rejected

all and unassigned both hold back rejected rows; pass scope=rejected to see those instead. Admin/lead only.

POST /api/inbound-emails

{ "id": "email-source-uuid", "project_id": "project-uuid", "remember_domain": true }
{ "id": "email-source-uuid", "action": "reject", "reason": "wrong client" }
{ "id": "email-source-uuid", "action": "restore" }

Why this route never returns the email body

Email bodies are client correspondence; this route deliberately returns metadata and the subject line only, never the body. Reading a thread's content is treated as a different permission from triaging where it belongs — the account page already shows attributed mail's content to the people who actually own the project, so the triage screen doesn't need to duplicate that exposure.

Assignment is not cosmetic

Setting project_id here does real work, not just a relabel. A source with no project_id skips knowledge indexing and the living project-note entry at ingest time. fathom-sync's bounded link-repair sweep (see fathom-sync) completes exactly those side effects once project_id becomes non-null — so an assignment made through this route is what makes the email searchable and puts it into the project's note, on the next scheduled poll, with no separate re-extraction step.

remember_domain

Setting this alongside an assignment teaches the router: future email from the same sending domain gets a stronger signal toward the same project, without requiring the same manual assignment every time.

Where the code lives

  • sable-agents-demo/web/api/inbound-emails.js, _inbound_email.js
  • sable-agents-demo/supabase/functions/email-webhook/index.ts, email-sync
  • sable-agents-demo/web/inbound.html