For Engineering
HTTP API
Generated from
laurelin/routes.jsbyscripts/generate-api-docs.js. Do not edit by hand — your changes will be overwritten on the next deploy.
All routes are matched via the match(method, /regex/) helper in handleLaurelinRoutes(). Path params are extracted from regex capture groups. POST/PUT bodies are parsed JSON via readBody(req). Responses use json(res, status, data).
Total endpoints: 153
/companies
GET/api/laurelin/companiesGET/api/laurelin/companies/:idPOST/api/laurelin/companiesPOST/api/laurelin/companies/:id/childrenPOST/api/laurelin/companies/:id/contactsPOST/api/laurelin/companies/:id/linksPOST/api/laurelin/companies/:id/mergePOST/api/laurelin/companies/:id/parentsPUT/api/laurelin/companies/:idPUT/api/laurelin/companies/:id/parents/:subIdDELETE/api/laurelin/companies/:idDELETE/api/laurelin/companies/:id/children/:subIdDELETE/api/laurelin/companies/:id/contacts/:subIdDELETE/api/laurelin/companies/:id/parents/:subId
/people
GET/api/laurelin/peoplePOST/api/laurelin/peoplePOST/api/laurelin/people/:id/merge— POST /api/laurelin/people/:targetId/merge — consolidate source person into target Body: { source_id, changed_by, new_name? } Mirrors the companies merge: moves all FKs onto target, consolidates emails into target.additional_emails, backfills empty target fields from source, then hard- deletes the source row. Logs to changelog.PUT/api/laurelin/people/:idPUT/api/laurelin/people/:id/telegram-tracking— PUT /api/laurelin/people/:id/telegram-tracking Toggle per-contact telegram tracking. Setting enabled=0 silently drops future business_messages from this person (sender or chat counterparty). The Sync tab's Blocked contacts section is the path to undo it.DELETE/api/laurelin/people/:id
/contacts
GET/api/laurelin/contacts— Backward compat: GET /contacts → /people?type=externalPOST/api/laurelin/contacts— Backward compat: POST /contactsPUT/api/laurelin/contacts/:id— Backward compat: PUT /contacts/:idDELETE/api/laurelin/contacts/:id— Backward compat: DELETE /contacts/:id
/projects
GET/api/laurelin/projectsGET/api/laurelin/projects/:idGET/api/laurelin/projects/:id/status-updatesPOST/api/laurelin/projectsPOST/api/laurelin/projects/:id/companiesPOST/api/laurelin/projects/:id/contacts— Backward compat: POST /projects/:id/contacts → project_people with role='contact'POST/api/laurelin/projects/:id/linksPOST/api/laurelin/projects/:id/members— Backward compat: POST /projects/:id/members → project_peoplePOST/api/laurelin/projects/:id/peoplePOST/api/laurelin/projects/:id/relationsPOST/api/laurelin/projects/:id/status-updatesPUT/api/laurelin/projects/:idPUT/api/laurelin/projects/:id/status-updates/:subIdDELETE/api/laurelin/projects/:idDELETE/api/laurelin/projects/:id/companies/:subIdDELETE/api/laurelin/projects/:id/contacts/:subId— Backward compat: DELETE /projects/:id/contacts/:contactIdDELETE/api/laurelin/projects/:id/members/:subId— Backward compat: DELETE /projects/:id/members/:memberIdDELETE/api/laurelin/projects/:id/people/:subIdDELETE/api/laurelin/projects/:id/relations/:subIdDELETE/api/laurelin/projects/:id/status-updates/:subId
/interactions
GET/api/laurelin/interactionsGET/api/laurelin/interactions/needs-enrichment— GET /api/laurelin/interactions/needs-enrichment Read-only feed of interactions that have ingest-stage data but no enrichment yet. Powers the Phase 6 enrichment agents (summarize- interaction, notion-meeting-parser): each runs on a cron, calls this endpoint to find work, processes a bounded batch, writes back via PUT /interactions/:id/enrichment. Query params: limit cap rows returned (default 50, max 200) source filter by source (outlook|calendar|notion); omit for any need 'summary' (default) | 'participants' | 'either' since ISO timestamp; default = 7 days ago The "needs summary" criterion: llm_summary IS NULL AND the row has at least some seed content (subject or summary). The "needs participants" criterion: source='notion' AND fewer than 2 internal participants attributed (mostly catches freeform pages where the deterministic adapter only gotcreated_by).POST/api/laurelin/interactionsPUT/api/laurelin/interactions/:idPUT/api/laurelin/interactions/:id/enrichment— PUT /api/laurelin/interactions/:id/enrichment Phase 6 enrichment write path. Accepts a small, narrowly-scoped patch — only the columns/relations enrichment agents are allowed to touch. Never modifies watermarks, never deletes anything, never changes company/project attribution. Body shape: { llm_summary?: string, // writes if non-empty additional_participant_emails?: string[],// internal emails to add enriched_by?: string // free-form audit string }DELETE/api/laurelin/interactions/:id
/key-dates
GET/api/laurelin/key-datesPOST/api/laurelin/key-datesPUT/api/laurelin/key-dates/:idPUT/api/laurelin/key-dates/:id/hidePUT/api/laurelin/key-dates/:id/unhideDELETE/api/laurelin/key-dates/:id
/settings
GET/api/laurelin/settingsPUT/api/laurelin/settings
/sync
GET/api/laurelin/sync/activityGET/api/laurelin/sync/do-not-trackGET/api/laurelin/sync/health— GET /api/laurelin/sync/health — Comprehensive ingest health snapshot. Returns one big JSON object describing worker state, per-team-member ingest status (OAuth, watermarks, recent volumes), aggregate metrics for the last 24h, privacy-rule counts, and anissues[]array of autodetected problems with severity ('error' | 'warn' | 'info'). Designed for periodic polling by a dashboard (apps/sync_health.jsx) and by any external monitor. Read-only; no side effects. Safe for unauthenticated callers — exposes only counts, no message content.GET/api/laurelin/sync/inbox— GET /api/laurelin/sync/inbox — List inbox items with filters. When include_counts=1, returns { items, total, truncated, by_bucket } so the Intake UI can show accurate count chips even when the items list is capped. Otherwise returns the legacy array shape.GET/api/laurelin/sync/inbox/:id/matches— GET /api/laurelin/sync/inbox/:id/matches — Find potential duplicate entities for mergeGET/api/laurelin/sync/llm/config— ─── LLM (Claude) wiring (CRM Phase 4) ───GET/api/laurelin/sync/skip-rules— GET /api/laurelin/sync/skip-rules — List all skip rulesGET/api/laurelin/sync/source-counts— GET /api/laurelin/sync/source-counts?since=ISO Per-source interaction count over a time window. Default window: 7 days. Powers the connection-status banners on the Sync admin page so each source block can show "12 logged in last 7d" next to its title.GET/api/laurelin/sync/stats— GET /api/laurelin/sync/stats — Pending counts for badgeGET/api/laurelin/sync/watermarks— GET /api/laurelin/sync/watermarks — Agent reads its watermarksGET/api/laurelin/sync/worker/statusPOST/api/laurelin/sync/do-not-trackPOST/api/laurelin/sync/inbox— POST /api/laurelin/sync/inbox — Agent submits items (batch, dedup via INSERT OR IGNORE with merge on conflict)POST/api/laurelin/sync/inbox/bulk— POST /api/laurelin/sync/inbox/bulk — Bulk approve/dismissPOST/api/laurelin/sync/lost-threads/scan— ─── Lost-thread detector (CRM Phase 4) ───POST/api/laurelin/sync/pre-filter— POST /api/laurelin/sync/pre-filter — Server-side filtering (zero tokens)POST/api/laurelin/sync/skip-rules— POST /api/laurelin/sync/skip-rules — Add a skip rulePOST/api/laurelin/sync/summarize/:id— Lazy summarization: client asks for one when the user views an item. Cached in interactions.llm_summary; subsequent requests return immediately.PUT/api/laurelin/sync/inbox/:id/approve— PUT /api/laurelin/sync/inbox/:id/approve — Approve an inbox item, creating the real entityPUT/api/laurelin/sync/inbox/:id/dismiss— PUT /api/laurelin/sync/inbox/:id/dismiss — Dismiss an inbox itemPUT/api/laurelin/sync/inbox/:id/save-contacts— PUT /api/laurelin/sync/inbox/:id/save-contacts — Save contacts/company without creating the interactionPUT/api/laurelin/sync/inbox/:id/telegram-triage— PUT /api/laurelin/sync/inbox/:id/telegram-triage — Approve a telegram triage row by either creating a new person or attaching to an existing one. Wires affiliation + interaction + interaction_people in one tx.PUT/api/laurelin/sync/inbox/telegram-triage-bulk— PUT /api/laurelin/sync/inbox/telegram-triage-bulk Same shape as /telegram-triage but acts on N pending sync_inbox rows at once — typically every pending row from the same telegram sender. Creates (or merges) the person ONCE, sets up the affiliation ONCE, then logs N interactions linked to that person + company. Used by the chat-list group card to clear "five intake cards for the same handle" in a single action.PUT/api/laurelin/sync/llm/configPUT/api/laurelin/sync/watermarks— PUT /api/laurelin/sync/watermarks — Agent updates watermarksDELETE/api/laurelin/sync/activity/:id— Un-link: hard-delete the interaction. Linking rows in interaction_people / interaction_companies cascade. The user is saying "this should never have been logged" — if the same message resurfaces (e.g. the watermark gets reset), the auto-router will re-evaluate and the user can un-link again.DELETE/api/laurelin/sync/do-not-track/:restDELETE/api/laurelin/sync/skip-rules/:id— DELETE /api/laurelin/sync/skip-rules/:id — Remove a skip rule
/sync/outlook
GET/api/laurelin/sync/outlook/auth-urlGET/api/laurelin/sync/outlook/callbackGET/api/laurelin/sync/outlook/configGET/api/laurelin/sync/outlook/statusPOST/api/laurelin/sync/outlook/disconnectPOST/api/laurelin/sync/outlook/poll— Manual trigger: run one Outlook poll cycle now and return the result. Useful for testing the OAuth wiring without waiting for the worker tick.PUT/api/laurelin/sync/outlook/config
/sync/slack
GET/api/laurelin/sync/slack/auth-urlGET/api/laurelin/sync/slack/callbackGET/api/laurelin/sync/slack/channelsGET/api/laurelin/sync/slack/configGET/api/laurelin/sync/slack/statusPOST/api/laurelin/sync/slack/backfillPOST/api/laurelin/sync/slack/disconnectPOST/api/laurelin/sync/slack/events— Slack Events webhook. Needs the raw body for signature verification, so we bypass the JSON-parsing readBody helper.PUT/api/laurelin/sync/slack/channel-mapPUT/api/laurelin/sync/slack/config
/sync/telegram
GET/api/laurelin/sync/telegram/blocked-contacts— GET /api/laurelin/sync/telegram/blocked-contacts External people whose telegram tracking has been turned off. Surfaced on the Sync tab so muted contacts have a visible "unblock" affordance.GET/api/laurelin/sync/telegram/configGET/api/laurelin/sync/telegram/statusPOST/api/laurelin/sync/telegram/pair-codePOST/api/laurelin/sync/telegram/poll— Manual trigger: run one Telegram poll cycle now. Useful for testing pairing / scope without waiting for the worker tick.PUT/api/laurelin/sync/telegram/chat-overridePUT/api/laurelin/sync/telegram/chat-scopePUT/api/laurelin/sync/telegram/chat-tag— Tag (or untag) a telegram chat with a company. Future inbound messages from unknown senders in this chat will auto-create a contact under that company instead of going to triage.PUT/api/laurelin/sync/telegram/config
/sync/notion
GET/api/laurelin/sync/notion/pipeline/checkpoints— ─── Pipeline checkpoints (snapshot / restore of Notion-synced fields) ───GET/api/laurelin/sync/notion/pipeline/reconcileGET/api/laurelin/sync/notion/pipeline/statusGET/api/laurelin/sync/notion/statusPOST/api/laurelin/sync/notionPOST/api/laurelin/sync/notion/pipeline— ─── Notion Companies/Pipeline sync (bidirectional) ───POST/api/laurelin/sync/notion/pipeline/checkpointsPOST/api/laurelin/sync/notion/pipeline/checkpoints/([a-f0-9-]+)/restoreDELETE/api/laurelin/sync/notion/pipeline/checkpoints/([a-f0-9-]+)
/changelog
GET/api/laurelin/changelog/:id/:subId
/company-links
DELETE/api/laurelin/company-links/:id
/debug
GET/api/laurelin/debug/schema— ─── Diagnostics ───
/focus
GET/api/laurelin/focus— ─── Valinor Focus: team-wide focus board (week / month / annual) ─── GET /api/laurelin/focus Returns { week, month, annual } where each is { current: { period_key, body, updated_at, updated_by } | null, history: [ ... older rows, most recent first ] }. "Current" is the row whose period_key matches today's date partition. Period keys: week = ISO YYYY-Www (Monday-start), month = YYYY-MM, annual = YYYY. The roll-over is implicit on the client: when today moves to a new week/month/year, "current" becomes a fresh row.PUT/api/laurelin/focus— PUT /api/laurelin/focus Body: { kind: 'week'|'month'|'annual', body: string, updated_by?: string } Upserts the row for today's period_key. Period_key derived server-side so the client can't accidentally write last week's row.
/lost-threads
GET/api/laurelin/lost-threads— GET /api/laurelin/lost-threads?user={id}&status=&tier=&direction=&min_days_stale=&max_days_stale=&sort=&team_filter=GET/api/laurelin/lost-threads/:id— GET /api/laurelin/lost-threads/:idGET/api/laurelin/lost-threads/diagnostics— GET /api/laurelin/lost-threads/diagnostics?user={id}&window_days=7 Funnel counters that explain why a user's candidate list looks the way it does. Each row is "after this gate, this many threads remain".GET/api/laurelin/lost-threads/draft-queue— GET /api/laurelin/lost-threads/draft-queue?user={id}POST/api/laurelin/lost-threads/:id/promote— POST /api/laurelin/lost-threads/:id/promote (create interaction + mark resolved)POST/api/laurelin/lost-threads/:id/request-draft— POST /api/laurelin/lost-threads/:id/request-draftPOST/api/laurelin/lost-threads/batch— POST /api/laurelin/lost-threads/batch — upsert an array of candidatesPUT/api/laurelin/lost-threads/:id— PUT /api/laurelin/lost-threads/:id (generic update: status, matches, etc.)PUT/api/laurelin/lost-threads/:id/draft-result— PUT /api/laurelin/lost-threads/:id/draft-result (called by scan agent)
/management
GET/api/laurelin/management/overview— GET /api/laurelin/management/overviewGET/api/laurelin/management/requests/:id/comments— GET /api/laurelin/management/requests/:id/commentsPOST/api/laurelin/management/requests— POST /api/laurelin/management/requestsPOST/api/laurelin/management/requests/:id/comments— POST /api/laurelin/management/requests/:id/commentsPOST/api/laurelin/management/requests/:id/vote— POST /api/laurelin/management/requests/:id/vote — toggles vote for voter_namePOST/api/laurelin/management/roadmap— POST /api/laurelin/management/roadmapPUT/api/laurelin/management/components/:id— PUT /api/laurelin/management/components/:slugPUT/api/laurelin/management/requests/:id— PUT /api/laurelin/management/requests/:idPUT/api/laurelin/management/roadmap/:id— PUT /api/laurelin/management/roadmap/:idDELETE/api/laurelin/management/roadmap/:id— DELETE /api/laurelin/management/roadmap/:id
/my-day
GET/api/laurelin/my-day
/personal-blocks
GET/api/laurelin/personal-blocks— GET /api/laurelin/personal-blocks?user_id=... — list a user's blocks.POST/api/laurelin/personal-blocks— POST /api/laurelin/personal-blocks Body: { rule_type, pattern, scope: 'me'|'everyone', source?, reason?, apply_retroactive?: boolean }DELETE/api/laurelin/personal-blocks/:id— DELETE /api/laurelin/personal-blocks/:id
/project-links
PUT/api/laurelin/project-links/:idDELETE/api/laurelin/project-links/:id
/search
GET/api/laurelin/search
/team-priorities
GET/api/laurelin/team-priorities— ─── Team Priority Tasks (read-only feed from Notion for Laurelin Dev) ───