Command Inbox
Reference

API routes

Complete HTTP API reference with auth, bodies, and responses

Base path: /api. JSON routes use Content-Type: application/json unless noted.

Shared conventions

Authentication

PatternRoutesMechanism
Session cookieMost user routesBetter Auth auth.api.getSession({ headers })
Session + GoogleInbox, agent, advanced searchrequireSessionApi() + isTenantFullyConnected
Bearer tokenCronAuthorization: Bearer $CRON_SECRET
Corsair signatureWebhooksx-corsair-signature via processWebhook()

Common errors

401 Unauthorized

{ "error": "Unauthorized" }

403 Google not connected

{ "error": "Google not connected" }

400 Invalid JSON

{ "error": "Invalid JSON body" }

400 Validation (Zod)

{
  "error": "Invalid request",
  "fieldErrors": { "threadId": ["threadId is required"] },
  "formErrors": []
}

503 Phase 2 AI not configured

{ "error": "At least one AI provider key is required (...)" }

Shared types

Classification

{
  threadId: string;
  priority: "high" | "medium" | "low";
  lane: "reply" | "schedule" | "fyi" | "done";
  subject: string;
  sender: string;
  snippet: string;
  schedulingIntent: {
    proposedTimes: string[];
    attendees: string[];
    duration: number;
    confidence: number;
  } | null;
  classifiedAt: string;
}

SearchHit

{
  threadId: string;
  subject: string;
  sender: string;
  snippet: string;
  lane: string;
  priority: string;
  score: number;
}

Auth

GET|POST /api/auth/[...all]

Better Auth handler — sign-in, sign-out, session, OAuth callbacks.

AuthVaries by sub-route
ResponseJSON or redirect per Better Auth

Example callback: GET /api/auth/callback/google


GET /api/auth/callback/corsair

Corsair OAuth callback for Gmail + Calendar connect.

AuthSession cookie required

Query: code, state (must match oauth_state cookie)

Redirects (not JSON):

ConditionLocation
No session/sign-in
Missing params/onboarding/connect?error=missing_oauth_params
State mismatch/onboarding/connect?error=invalid_oauth_state
Fully connected/inbox
Partial/onboarding/connect

Connect

GET /api/connect/google

Start Gmail OAuth via Corsair.

AuthSession cookie

Response: 302 redirect to Google OAuth (sets oauth_state cookie) or /sign-in / ?error=kek_mismatch


Inbox

All inbox routes use requireSessionApi().

POST|DELETE /api/inbox/snooze

POST — Snooze thread

// Request
{ "threadId": "string", "until": "2026-06-13T12:00:00.000Z" }

// Response 200
{ "success": true }

DELETE — Cancel snooze

// Request
{ "threadId": "string" }

// Response 200
{ "success": true }

POST /api/inbox/archive

// Request
{ "threadId": "string" }

// Response 200
{ "success": true, "classification": Classification | null }

Archives in Gmail and sets lane to done.

500: { "error": "Archive failed" }


POST /api/inbox/restore

// Request
{ "threadId": "string", "lane": "reply" | "schedule" | "fyi" | "done" }

// Response 200
{ "success": true, "classification": Classification | null }

POST /api/inbox/draft

Requires assertPhase2Env().

// Request
{
  "threadId": "string",
  "tone": "professional" | "friendly" | "brief",
  "provider": "openai" | "gemini"
}

// Response 200
{ "draftHtml": "string", "source": "ai" | "template" }

404: { "error": "Thread not found" }


POST|PUT|DELETE /api/inbox/send

Queued send with 5s undo window (UNDO_WINDOW_MS = 5000).

POST — Queue send

// Request
{
  "to": ["user@example.com"],
  "subject": "string",
  "body": "string",
  "threadId": "string",
  "sendAt": "2026-06-13T12:00:00.000Z"
}

// Response 200
{
  "scheduledSendId": "string",
  "sendAt": "2026-06-13T12:00:05.000Z",
  "undoWindowMs": 5000
}

PUT — Dispatch immediately

// Request
{ "scheduledSendId": "string" }

// Response 200
{ "success": true, "messageId": "string" }

// Response 409
{ "error": "Send not ready or already handled" }

DELETE — Cancel queued send

// Request
{ "scheduledSendId": "string" }

// Response 200
{ "success": true | false }

POST|PATCH|DELETE /api/inbox/meeting

POST/PATCH require assertPhase2Env().

POST — Create meeting

// Request
{
  "threadId": "string",
  "slotStart": "2026-06-13T14:00:00.000Z",
  "durationMinutes": 30
}

// Response 200
{
  "success": true,
  "eventId": "string",
  "hangoutLink": "string",
  "htmlLink": "string",
  "draftHtml": "string",
  "meeting": {
    "threadId": "string",
    "eventId": "string",
    "start": "ISO",
    "durationMinutes": 30
  },
  "classification": Classification | null
}
StatusError
400No attendees found
404Thread not found
409Thread already has a meeting
503AI not configured

PATCH — Reschedule

Same request body as POST. Returns meeting + draft without classification.

DELETE — Cancel meeting

// Request
{ "threadId": "string" }

// Response 200
{ "success": true, "eventId": "string" }

POST /api/inbox/reembed

Requires assertPhase2Env().

// Request
{ "provider": "openai" | "gemini" }

// Response 200
{ "status": "running", "provider": "openai" }
{ "status": "nothing-to-do", "provider": "openai", "total": 0 }
{ "status": "started", "provider": "openai", "total": 42 }

POST /api/search

Session only (no Google-connect check). Requires Phase 2 AI.

// Request
{ "query": "string", "limit": 20, "provider": "openai" }

// Response 200
{ "results": SearchHit[] }

Returns [] if backfill incomplete.


POST /api/search/advanced

Session + Google fully connected.

// Request
{
  "query": "string",
  "sender": "string",
  "after": "ISO datetime",
  "before": "ISO datetime",
  "hasAttachment": true,
  "lane": "reply" | "schedule" | "fyi" | "done",
  "limit": 30
}

// Response 200
{ "results": SearchHit[] }

500: { "error": "Advanced search failed" }


Agent

POST /api/agent/chat

requireSessionApi() + assertPhase2Env(). Max duration 60s.

// Request
{
  "messages": [/* AI SDK UIMessage[] */],
  "provider": "openai"
}

200: AI SDK UI Message Stream (SSE). Tool calls include approval pauses for typed write tools (send_email, create_calendar_invite, etc.).

500: { "error": "Agent request failed" }


Cron

POST /api/cron/process-due

Authorization: Bearer $CRON_SECRET

No body.

// Response 200
{
  "success": true,
  "sentCount": 0,
  "expiredSnoozes": 0
}

Processes due scheduled sends and expired snoozes for all users.


Webhooks

POST /api/webhooks

Runtime: nodejs. Corsair signature verification.

Query: tenantId (required — user ID)

Headers: x-corsair-signature

Body: Raw JSON (Corsair webhook payload)

// Response 200
{ "success": true, "plugin": "gmail" }

400: Plain text Missing tenantId or Invalid JSON

On gmail.messageChanged, async classification runs. Verified webhooks return 200 even if async handler fails (logged).

URL pattern: {APP_URL}/api/webhooks?tenantId={userId}


Quick reference table

RouteMethodsAuthPhase 2 AI
/api/auth/[...all]GET, POSTBetter Auth
/api/auth/callback/corsairGETSession
/api/connect/googleGETSession
/api/inbox/snoozePOST, DELETESession + Google
/api/inbox/archivePOSTSession + Google
/api/inbox/restorePOSTSession + Google
/api/inbox/draftPOSTSession + Googleyes
/api/inbox/sendPOST, PUT, DELETESession + Google
/api/inbox/meetingPOST, PATCH, DELETESession + GooglePOST/PATCH
/api/inbox/reembedPOSTSession + Googleyes
/api/searchPOSTSessionyes
/api/search/advancedPOSTSession + Google
/api/agent/chatPOSTSession + Googleyes
/api/cron/process-duePOSTBearer
/api/webhooksPOSTCorsair sig