API Reference
Every route Selyf's API actually exposes, re-verified against source: method, auth, rate limits, validation, and real request/response examples.
See Authentication for the OAuth model these routes share, and Errors and Rate Limits for the shared error and throttling contract every route below follows. No example on this page uses a real account, email address, token, or credential.
Authentication
/auth/googleStarts the Google OAuth sign-in flow by redirecting to Google's consent screen.
- Auth
- None
- CSRF
- Not applicable -- a browser navigation, not a state-changing API call
- Rate limit
- 20 requests / 60s
Response
302 Found -- redirects to accounts.google.com
Errors
- SERVICE_UNAVAILABLE (503) -- Google OAuth is not configured server-side
Example
curl -i https://api.selyf.com/auth/googleHTTP/1.1 302 Found
Location: https://accounts.google.com/o/oauth2/v2/auth?client_id=...&redirect_uri=...&scope=openid+email+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fgmail.readonly&state=.../auth/google/callbackGoogle redirects here after consent. Exchanges the authorization code, establishes a session, and redirects to the web app.
- Auth
- None
- CSRF
- The OAuth state parameter is validated (single-use, 10-minute lifetime) -- a separate mechanism from the app's own X-CSRF-Token
- Rate limit
- 20 requests / 60s
Request
Query / params: code (string, from Google), state (string, from Google), error (string, optional)
Validation: An invalid, expired, or mismatched state redirects to the web app with ?error=oauth_failed rather than returning a JSON error -- this endpoint is only ever reached via Google's own redirect, never called directly
Response
302 Found -- redirects to the web app with ?connected=1
Errors
- Redirects with ?error=oauth_failed on any failure (invalid state, missing code, token exchange failure)
- SERVICE_UNAVAILABLE (503) -- Google OAuth is not configured server-side
Example
(Called only by Google's own redirect -- not intended to be called directly.)HTTP/1.1 302 Found
Location: https://www.selyf.com/?connected=1/auth/sessionReturns whether the current request is authenticated, and if so, the account email and a CSRF token for subsequent requests.
- Auth
- None required to call -- this is how a client checks whether it is authenticated
- CSRF
- Not applicable -- read-only
- Rate limit
- Global default (100 requests / 60s)
Response
200 OK
{
"authenticated": true,
"csrfToken": "<csrf-token>",
"email": "you@example.com"
}Example
curl -i --cookie "<session-cookie>" https://api.selyf.com/auth/session{
"authenticated": true,
"csrfToken": "<csrf-token>",
"email": "you@example.com"
}/auth/logoutEnds the current session.
- Auth
- None required -- a request with no session simply has nothing to clear
- CSRF
- Not required
- Rate limit
- Global default (100 requests / 60s)
Request
Body: None
Validation: None
Response
204 No Content
Example
curl -i -X POST --cookie "<session-cookie>" https://api.selyf.com/auth/logoutHTTP/1.1 204 No Content/auth/google/disconnectRevokes Gmail access with Google, cancels the Gmail watch, clears stored credentials, and ends the session.
- Auth
- Required (session cookie)
- CSRF
- Required (X-CSRF-Token header)
- Rate limit
- 20 requests / 60s
Request
Body: None
Validation: None beyond auth/CSRF
Response
204 No Content
Errors
- AUTH_REQUIRED (401) -- no active session
- CSRF_FAILED (403) -- missing or invalid X-CSRF-Token, or Origin not in the configured web origin
- SERVICE_UNAVAILABLE (503) -- no database configured
Example
curl -i -X POST \
-H "X-CSRF-Token: <csrf-token>" \
--cookie "<session-cookie>" \
https://api.selyf.com/auth/google/disconnectHTTP/1.1 204 No Content/auth/accountPermanently deletes the account and all associated data.
- Auth
- Required (session cookie)
- CSRF
- Required (X-CSRF-Token header)
- Rate limit
- 20 requests / 60s
Request
Body: { "confirm": true } -- required, must be the literal boolean true
Validation: VALIDATION_FAILED if the body is missing or confirm is not exactly true
Response
204 No Content
Errors
- AUTH_REQUIRED (401) -- no active session
- CSRF_FAILED (403) -- missing or invalid X-CSRF-Token
- VALIDATION_FAILED (400) -- body is not { "confirm": true }
- SERVICE_UNAVAILABLE (503) -- no database configured
Example
curl -i -X DELETE \
-H "X-CSRF-Token: <csrf-token>" \
-H "Content-Type: application/json" \
--cookie "<session-cookie>" \
-d '{"confirm": true}' \
https://api.selyf.com/auth/accountHTTP/1.1 204 No ContentMail & sync
/api/mail/syncTriggers a bounded Gmail inbox sync for the authenticated account (up to 50 new messages per call).
- Auth
- Required
- CSRF
- Required
- Rate limit
- 10 requests / 60s
Request
Body: None
Validation: None beyond auth/CSRF. A sync already in progress for this account returns RATE_LIMITED rather than starting a second, overlapping one.
Response
200 OK
{
"messagesSynced": 12,
"hasMore": false,
"lastSyncedAt": "2026-08-30T12:00:00.000Z"
}Errors
- AUTH_REQUIRED (401)
- CSRF_FAILED (403)
- RATE_LIMITED (429) -- a sync is already in progress for this account
- SYNC_FAILED (502) -- the Gmail API request failed
- SERVICE_UNAVAILABLE (503) -- no database configured
Example
curl -i -X POST \
-H "X-CSRF-Token: <csrf-token>" \
--cookie "<session-cookie>" \
https://api.selyf.com/api/mail/sync{
"messagesSynced": 12,
"hasMore": false,
"lastSyncedAt": "2026-08-30T12:00:00.000Z"
}/api/mail/sync-stateReturns the last successful sync time without triggering a new sync or spending Gmail API quota.
- Auth
- Required
- CSRF
- Not applicable -- read-only
- Rate limit
- Global default (100 requests / 60s)
Response
200 OK
{
"lastSyncedAt": "2026-08-30T12:00:00.000Z"
}Errors
- AUTH_REQUIRED (401)
- SERVICE_UNAVAILABLE (503) -- no database configured
Example
curl -i --cookie "<session-cookie>" https://api.selyf.com/api/mail/sync-state{
"lastSyncedAt": "2026-08-30T12:00:00.000Z"
}/api/mail/threadsLists synced threads for the authenticated account, newest first, paginated.
- Auth
- Required
- CSRF
- Not applicable -- read-only
- Rate limit
- Global default (100 requests / 60s)
Request
Query / params: limit (integer, 1-100, default 20), offset (integer, >= 0, default 0)
Validation: VALIDATION_FAILED if limit/offset are out of range or not integers
Response
200 OK
{
"threads": [
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"gmailThreadId": "18c9f2a1b3d4e5f6",
"subject": "Quarterly report",
"snippet": "Attached is the draft for review...",
"lastMessageAt": "2026-08-29T09:15:00.000Z",
"isUnread": true
}
],
"total": 143,
"hasMore": true
}Errors
- AUTH_REQUIRED (401)
- VALIDATION_FAILED (400)
- SERVICE_UNAVAILABLE (503)
Example
curl -i --cookie "<session-cookie>" \
"https://api.selyf.com/api/mail/threads?limit=20&offset=0"{
"threads": [
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"gmailThreadId": "18c9f2a1b3d4e5f6",
"subject": "Quarterly report",
"snippet": "Attached is the draft for review...",
"lastMessageAt": "2026-08-29T09:15:00.000Z",
"isUnread": true
}
],
"total": 143,
"hasMore": true
}/api/mail/threads/:threadIdReturns one thread's messages, fetching and caching each message's full body from Gmail on demand.
- Auth
- Required
- CSRF
- Not applicable -- read-only
- Rate limit
- Global default (100 requests / 60s)
Request
Query / params: threadId (path parameter, UUID)
Validation: VALIDATION_FAILED if threadId is not a UUID; NOT_FOUND if it does not belong to the authenticated account
Response
200 OK
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"gmailThreadId": "18c9f2a1b3d4e5f6",
"subject": "Quarterly report",
"isUnread": true,
"messages": [
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"threadId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"fromName": "Jordan Lee",
"fromAddress": "jordan@example.com",
"subject": "Quarterly report",
"snippet": "Attached is the draft for review...",
"sentAt": "2026-08-29T09:15:00.000Z",
"isUnread": true,
"bodyText": "Hi, attached is the draft for review...",
"bodyHtml": "<p>Hi, attached is the draft for review...</p>",
"attachments": []
}
]
}Errors
- AUTH_REQUIRED (401)
- VALIDATION_FAILED (400)
- NOT_FOUND (404)
- SERVICE_UNAVAILABLE (503)
Example
curl -i --cookie "<session-cookie>" \
https://api.selyf.com/api/mail/threads/3fa85f64-5717-4562-b3fc-2c963f66afa6{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"gmailThreadId": "18c9f2a1b3d4e5f6",
"subject": "Quarterly report",
"isUnread": true,
"messages": [ ... ]
}AI analysis & drafting
/api/mail/threads/:threadId/analyzeReturns a cached analysis for the thread's latest message, or generates and stores a new one.
- Auth
- Required
- CSRF
- Required
- Rate limit
- 20 requests / 60s
Request
Query / params: threadId (path parameter, UUID)
Validation: VALIDATION_FAILED if threadId is not a UUID
Response
200 OK
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"userId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"messageId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"priority": "IMPORTANT",
"replyStatus": "NEEDS_REPLY",
"confidence": 0.86,
"reason": "Asks a direct question about the attached draft.",
"suggestedAction": "Review the attachment and reply with feedback.",
"model": "configured-provider-model",
"createdAt": "2026-08-29T09:16:02.000Z",
"updatedAt": "2026-08-29T09:16:02.000Z"
}Errors
- AUTH_REQUIRED (401)
- CSRF_FAILED (403)
- VALIDATION_FAILED (400)
- NOT_FOUND (404) -- thread not found, or has no synced message yet
- AI_BUDGET_EXCEEDED (429) -- this account's daily or monthly AI token budget is exhausted
- AI_RATE_LIMITED (429) -- the AI provider itself rate-limited the request
- AI_REQUEST_FAILED (502)
- SERVICE_UNAVAILABLE (503) -- no database or no AI provider configured
Example
curl -i -X POST \
-H "X-CSRF-Token: <csrf-token>" \
--cookie "<session-cookie>" \
https://api.selyf.com/api/mail/threads/3fa85f64-5717-4562-b3fc-2c963f66afa6/analyze{
"priority": "IMPORTANT",
"replyStatus": "NEEDS_REPLY",
"confidence": 0.86,
"reason": "Asks a direct question about the attached draft.",
"suggestedAction": "Review the attachment and reply with feedback.",
"model": "configured-provider-model",
"createdAt": "2026-08-29T09:16:02.000Z",
"updatedAt": "2026-08-29T09:16:02.000Z"
}/api/mail/threads/:threadId/draftGenerates an editable reply draft for the thread.
- Auth
- Required
- CSRF
- Required
- Rate limit
- 20 requests / 60s
Request
Query / params: threadId (path parameter, UUID)
Body: Optional JSON: { tone?: "default" | "shorter" | "professional" | "friendly", language?: string (max 60 chars), preferredTone?: string (max 60 chars), signature?: string (max 300 chars) }
Validation: VALIDATION_FAILED if threadId is not a UUID, or the body does not match the schema above
Response
200 OK
{
"draft": {
"subject": "Re: Quarterly report",
"body": "Hi Jordan,\n\nThanks for sending this over...",
"language": "English"
},
"model": "configured-provider-model"
}Errors
- AUTH_REQUIRED (401)
- CSRF_FAILED (403)
- VALIDATION_FAILED (400)
- NOT_FOUND (404) -- thread not found, or has no synced message yet
- AI_BUDGET_EXCEEDED (429)
- AI_RATE_LIMITED (429)
- AI_REQUEST_FAILED (502)
- SERVICE_UNAVAILABLE (503)
Example
curl -i -X POST \
-H "X-CSRF-Token: <csrf-token>" \
-H "Content-Type: application/json" \
--cookie "<session-cookie>" \
-d '{"tone": "professional"}' \
https://api.selyf.com/api/mail/threads/3fa85f64-5717-4562-b3fc-2c963f66afa6/draft{
"draft": {
"subject": "Re: Quarterly report",
"body": "Hi Jordan,\n\nThanks for sending this over...",
"language": "English"
},
"model": "configured-provider-model"
}Analytics
/api/analytics/eventRecords a single product-analytics event.
- Auth
- Required, unless the named event is on a small public allowlist (e.g. page views)
- CSRF
- Not required -- a forged call here costs nothing and changes no account state beyond an analytics counter
- Rate limit
- 60 requests / 60s
Request
Body: event (string, max 64 chars), properties (optional, validated against a fixed per-event schema), anonymousId (optional UUID, used only when no session exists)
Validation: VALIDATION_FAILED if the body does not match the schema above. An event name the server does not recognize is accepted but silently ignored -- this keeps an older deployed frontend from erroring on a since-removed event.
Response
200 OK
{ "ok": true }Errors
- VALIDATION_FAILED (400)
- AUTH_REQUIRED (401) -- a non-public event with no session
Example
curl -i -X POST \
-H "Content-Type: application/json" \
-d '{"event": "page_view"}' \
https://api.selyf.com/api/analytics/event{ "ok": true }Status
/api/status/incidentsPublic incident and maintenance feed, consumed by status.selyf.com.
- Auth
- None
- CSRF
- Not applicable
- Rate limit
- Global default (100 requests / 60s)
Response
200 OK
{
"incidents": [
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"kind": "incident",
"state": "resolved",
"title": "Elevated API latency",
"message": "Investigated and resolved; no data was affected.",
"affectedComponents": ["api"],
"startsAt": "2026-08-20T10:00:00.000Z",
"resolvedAt": "2026-08-20T10:45:00.000Z"
}
]
}Errors
- SERVICE_UNAVAILABLE (503) -- no database configured
Example
curl -i https://api.selyf.com/api/status/incidents{
"incidents": [
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"kind": "incident",
"state": "resolved",
"title": "Elevated API latency",
"message": "Investigated and resolved; no data was affected.",
"affectedComponents": ["api"],
"startsAt": "2026-08-20T10:00:00.000Z",
"resolvedAt": "2026-08-20T10:45:00.000Z"
}
]
}System
/healthLiveness/readiness probe reporting database, Gmail, and AI-provider reachability.
- Auth
- None -- reachable by monitoring infrastructure with no session, from any origin
- CSRF
- Not applicable
- Rate limit
- Global default (100 requests / 60s)
Response
200 OK (or 503 if the database is unreachable -- the body still describes every component either way)
{
"status": "ok",
"service": "mail-ai-api",
"time": "2026-08-30T12:00:00.000Z",
"database": "ok",
"gmail": { "state": "operational", "detail": "..." },
"ai": { "state": "operational", "detail": "..." }
}Example
curl -i https://api.selyf.com/health{
"status": "ok",
"service": "mail-ai-api",
"time": "2026-08-30T12:00:00.000Z",
"database": "ok",
"gmail": { "state": "operational", "detail": "..." },
"ai": { "state": "operational", "detail": "..." }
}/A human-facing HTML page -- not a JSON endpoint -- showing the same live status as /health, for anyone who lands on the bare API domain in a browser.
- Auth
- None
- CSRF
- Not applicable
- Rate limit
- Global default (100 requests / 60s)
Response
200 OK -- text/html
Example
curl -i https://api.selyf.com/(An HTML page. See it directly at https://api.selyf.com/.)