{"openapi":"3.1.0","info":{"title":"AI Note Taker API","version":"0.15.0","description":"Structured decision memo API — Bun + Hono + Prisma + Postgres. Register via API docs, verify email with OTP, then login to get a Bearer token."},"servers":[{"url":"/api","description":"Local"}],"paths":{"/auth/register":{"post":{"tags":["Auth"],"summary":"Register a new user","description":"Create account and send a 6-digit verification code to the email. Must call /auth/verify-email next.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RegisterInput"}}}},"responses":{"201":{"description":"Created — verification code sent","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MessageResponse"}}}},"400":{"description":"Validation error"},"409":{"description":"Email already registered"}}}},"/auth/verify-email":{"post":{"tags":["Auth"],"summary":"Verify email with OTP","description":"Submit the 6-digit code received via email. Returns a JWT on success.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VerifyEmailInput"}}}},"responses":{"200":{"description":"Email verified — JWT returned","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthResponse"}}}},"400":{"description":"Invalid or expired code"}}}},"/auth/resend-verification":{"post":{"tags":["Auth"],"summary":"Resend verification code","description":"Invalidates previous codes and sends a new 6-digit OTP.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResendVerificationInput"}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MessageResponse"}}}}}}},"/auth/login":{"post":{"tags":["Auth"],"summary":"Login","description":"Authenticate with email + password. Email must be verified first.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginInput"}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthResponse"}}}},"401":{"description":"Invalid credentials"},"403":{"description":"Email not verified (includes needsVerification flag)"}}}},"/auth/forgot-password":{"post":{"tags":["Auth"],"summary":"Request password reset code","description":"Sends a 6-digit reset code to the email if the account exists.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ForgotPasswordInput"}}}},"responses":{"200":{"description":"OK (always succeeds to prevent email enumeration)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MessageResponse"}}}}}}},"/auth/reset-password":{"post":{"tags":["Auth"],"summary":"Reset password with OTP","description":"Verify the reset code and set a new password.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResetPasswordInput"}}}},"responses":{"200":{"description":"Password reset successful","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MessageResponse"}}}},"400":{"description":"Invalid or expired code"}}}},"/auth/me":{"get":{"tags":["Auth"],"summary":"Get current user","security":[{"BearerAuth":[]}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserProfile"}}}},"401":{"description":"Unauthorized"}}}},"/auth/users":{"get":{"tags":["Auth"],"summary":"List all users","description":"Returns all users with their status, ID, email verification state, and memo/tag counts.","security":[{"BearerAuth":[]}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/UserListItem"}}}}},"401":{"description":"Unauthorized"}}}},"/auth/user/{id}":{"delete":{"tags":["Auth"],"summary":"Delete user by ID","description":"Permanently delete a user and all their data by user ID.","security":[{"BearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"User ID to delete"}],"responses":{"200":{"description":"User deleted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MessageResponse"}}}},"401":{"description":"Unauthorized"},"404":{"description":"User not found"}}}},"/memos":{"post":{"tags":["Memos"],"summary":"Create a new memo","security":[{"BearerAuth":[]}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"title":{"type":"string"}}}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid"}}}}}}}},"get":{"tags":["Memos"],"summary":"List memos","security":[{"BearerAuth":[]}],"parameters":[{"name":"q","in":"query","schema":{"type":"string"},"description":"Search by title"},{"name":"tags","in":"query","schema":{"type":"string"},"description":"Comma-separated tag names"},{"name":"limit","in":"query","schema":{"type":"integer","default":20}},{"name":"offset","in":"query","schema":{"type":"integer","default":0}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/MemoListItem"}}}}}}}},"/memos/{id}":{"get":{"tags":["Memos"],"summary":"Get memo by ID","security":[{"BearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MemoDetail"}}}},"404":{"description":"Not found"}}},"put":{"tags":["Memos"],"summary":"Save memo (title, body, tags)","security":[{"BearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SaveMemoInput"}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"}}}}}},"404":{"description":"Not found"}}},"delete":{"tags":["Memos"],"summary":"Delete memo","security":[{"BearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"}}}}}},"404":{"description":"Not found"}}}},"/dashboard/nodes":{"get":{"tags":["Dashboard"],"summary":"List nodes with filters","security":[{"BearerAuth":[]}],"parameters":[{"name":"node_types","in":"query","schema":{"type":"string"},"description":"Comma-separated node types"},{"name":"tags","in":"query","schema":{"type":"string"},"description":"Comma-separated tag names"},{"name":"confidence","in":"query","schema":{"type":"string"}},{"name":"outcome","in":"query","schema":{"type":"string"}},{"name":"limit","in":"query","schema":{"type":"integer","default":50}},{"name":"offset","in":"query","schema":{"type":"integer","default":0}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/DashboardNode"}}}}}}}},"/dashboard/due":{"get":{"tags":["Dashboard"],"summary":"List memos due for review","security":[{"BearerAuth":[]}],"parameters":[{"name":"within_days","in":"query","schema":{"type":"integer","default":0},"description":"0 = overdue only"}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/MemoListItem"}}}}}}}},"/tags":{"get":{"tags":["Tags"],"summary":"List all tags","security":[{"BearerAuth":[]}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"}}}}}}}}}},"/tags/{name}":{"delete":{"tags":["Tags"],"summary":"Delete tag by name","description":"Deletes a tag and removes it from all memo associations. Does not modify memo content.","security":[{"BearerAuth":[]}],"parameters":[{"name":"name","in":"path","required":true,"schema":{"type":"string"},"description":"Tag name (URL-encoded)"}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"}}}}}},"404":{"description":"Tag not found"}}}},"/seed":{"post":{"tags":["System"],"summary":"Seed sample data for all users (no auth)","description":"Creates 3 sample memos with structured nodes, tags, and metadata for every user in the database. Safe to call multiple times — each call adds new memos.","responses":{"200":{"description":"Seed complete","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"users":{"type":"integer"},"memos":{"type":"integer"},"tags":{"type":"integer"},"nodes":{"type":"integer"}}}}}}}}},"/health":{"get":{"tags":["System"],"summary":"Health check","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string"}}}}}}}}},"/sources/oauth/github/start":{"get":{"tags":["Sources"],"summary":"Start GitHub source OAuth flow","description":"Generates a pending OAuth session and returns the GitHub authorization URL for the frontend to redirect to. The callback at /sources/oauth/github/callback will store the access token, then the frontend creates the permanent connection via POST /sources with { oauthPendingId }.","security":[{"BearerAuth":[]}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"pendingId":{"type":"string","format":"uuid"},"authUrl":{"type":"string","format":"uri"}}}}}},"500":{"description":"OAuth not configured"}}}},"/sources/oauth/zendesk/start":{"get":{"tags":["Sources"],"summary":"Start Zendesk source OAuth flow","description":"Requires the user's Zendesk subdomain. Generates a pending OAuth session and returns the Zendesk authorization URL.","security":[{"BearerAuth":[]}],"parameters":[{"name":"subdomain","in":"query","required":true,"schema":{"type":"string","example":"mycompany"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"pendingId":{"type":"string","format":"uuid"},"authUrl":{"type":"string","format":"uri"},"subdomain":{"type":"string"}}}}}},"400":{"description":"Invalid subdomain"},"500":{"description":"OAuth not configured"}}}},"/sources/oauth/github/pending/{pendingId}/repos":{"get":{"tags":["Sources"],"summary":"List repos for a pending GitHub OAuth session","description":"After the GitHub callback has completed, call this to fetch the authenticated user's repositories so they can pick one for the connection.","security":[{"BearerAuth":[]}],"parameters":[{"name":"pendingId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"login":{"type":"string"},"repos":{"type":"array","items":{"type":"object","properties":{"id":{"type":"integer"},"name":{"type":"string"},"fullName":{"type":"string"},"private":{"type":"boolean"},"owner":{"type":"string"},"defaultBranch":{"type":"string"},"description":{"type":"string","nullable":true}}}}}}}}},"400":{"description":"OAuth not completed"},"404":{"description":"Pending session not found"},"502":{"description":"GitHub error"}}}},"/sources/oauth/github/callback":{"get":{"tags":["Sources"],"summary":"GitHub OAuth callback (called by GitHub — do not call directly)","description":"Public endpoint. GitHub redirects here with ?code=...&state=... The server exchanges the code, stores the access token in the pending session, and redirects to the frontend /settings page.","responses":{"302":{"description":"Redirect to /settings"}}}},"/sources/oauth/zendesk/callback":{"get":{"tags":["Sources"],"summary":"Zendesk OAuth callback (called by Zendesk — do not call directly)","description":"Public endpoint. Zendesk redirects here with ?code=...&state=... The server exchanges the code (against {subdomain}.zendesk.com/oauth/tokens), stores the access token, and redirects to /settings.","responses":{"302":{"description":"Redirect to /settings"}}}},"/sources":{"get":{"tags":["Sources"],"summary":"List source connections","security":[{"BearerAuth":[]}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/SourceConnection"}}}}}}},"post":{"tags":["Sources"],"summary":"Create source connection (zendesk / github)","description":"Create a connection by pasting a personal API token. Use POST /ingestion/upload for file-based connections.","security":[{"BearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateSourceConnectionInput"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SourceConnection"}}}},"400":{"description":"Validation error"}}}},"/sources/{id}":{"get":{"tags":["Sources"],"summary":"Get one source connection","security":[{"BearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SourceConnection"}}}},"404":{"description":"Not found"}}},"delete":{"tags":["Sources"],"summary":"Delete source connection (cascades to items)","security":[{"BearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Deleted"},"404":{"description":"Not found"}}}},"/sources/{id}/sync":{"post":{"tags":["Sources"],"summary":"Trigger sync from external API","description":"Synchronously fetches the latest items from the external source and upserts them. Only valid for zendesk and github (not upload).","security":[{"BearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Sync result","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"fetched":{"type":"integer"},"created":{"type":"integer"},"updated":{"type":"integer"}}}}}},"404":{"description":"Not found"},"500":{"description":"Sync failed"}}}},"/source-items":{"get":{"tags":["Sources"],"summary":"List ingested items","security":[{"BearerAuth":[]}],"parameters":[{"name":"sourceConnectionId","in":"query","required":false,"schema":{"type":"string","format":"uuid"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":50,"maximum":200}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","default":0}}],"responses":{"200":{"description":"OK (normalizedContent truncated to 240 chars)","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/SourceItemListItem"}}}}}}}},"/source-items/{id}":{"get":{"tags":["Sources"],"summary":"Get one source item with full content","security":[{"BearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SourceItemDetail"}}}},"404":{"description":"Not found"}}}},"/ingestion/upload":{"post":{"tags":["Sources"],"summary":"Upload a File (txt file or JSON dump)","description":"Creates a new SourceConnection with provider=upload and stores the file content as a single SourceItem. Only .txt files are accepted — schema detection is deferred to the AI playbook generation step.","security":[{"BearerAuth":[]}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"type":"object","required":["file"],"properties":{"file":{"type":"string","format":"binary"},"label":{"type":"string","description":"Optional label (defaults to filename)"}}}}}},"responses":{"201":{"description":"Upload + parse result","content":{"application/json":{"schema":{"type":"object","properties":{"connection":{"$ref":"#/components/schemas/SourceConnection"},"fetched":{"type":"integer"},"created":{"type":"integer"},"updated":{"type":"integer"}}}}}},"400":{"description":"Invalid file"},"413":{"description":"File too large"}}}},"/test-email":{"post":{"tags":["System"],"summary":"Test email delivery (no auth)","description":"Send a test email via Brevo to verify configuration. Returns the full Brevo API response for debugging.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["to"],"properties":{"to":{"type":"string","format":"email","example":"kunal@infrahive.ai","description":"Recipient email — replace this entirely with your address"}}}}}},"responses":{"200":{"description":"Brevo response (check success field)","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"brevoStatus":{"type":"integer"},"brevoResponse":{"type":"object"},"config":{"type":"object","properties":{"senderEmail":{"type":"string"},"senderName":{"type":"string"},"apiKeySet":{"type":"boolean"},"apiKeyPrefix":{"type":"string"}}}}}}}}}}}},"components":{"securitySchemes":{"BearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT","description":"Login or register first, then paste the token here."}},"schemas":{"RegisterInput":{"type":"object","required":["email","password"],"properties":{"email":{"type":"string","format":"email","example":"user@example.com"},"password":{"type":"string","minLength":6,"example":"mypassword"},"name":{"type":"string","example":"Jane Doe"}}},"LoginInput":{"type":"object","required":["email","password"],"properties":{"email":{"type":"string","format":"email","example":"demo@example.com"},"password":{"type":"string","example":"demo1234"}}},"VerifyEmailInput":{"type":"object","required":["email","code"],"properties":{"email":{"type":"string","format":"email","example":"user@example.com"},"code":{"type":"string","example":"483921","description":"6-digit OTP from email"}}},"ResendVerificationInput":{"type":"object","required":["email"],"properties":{"email":{"type":"string","format":"email","example":"user@example.com"}}},"ForgotPasswordInput":{"type":"object","required":["email"],"properties":{"email":{"type":"string","format":"email","example":"user@example.com"}}},"ResetPasswordInput":{"type":"object","required":["email","code","newPassword"],"properties":{"email":{"type":"string","format":"email","example":"user@example.com"},"code":{"type":"string","example":"483921","description":"6-digit OTP from email"},"newPassword":{"type":"string","minLength":6,"example":"newpassword123"}}},"AuthResponse":{"type":"object","properties":{"token":{"type":"string","description":"JWT Bearer token"},"user":{"$ref":"#/components/schemas/UserProfile"}}},"MessageResponse":{"type":"object","properties":{"message":{"type":"string"},"email":{"type":"string","format":"email","description":"Included on register"}}},"UserProfile":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"email":{"type":"string","format":"email"},"name":{"type":"string","nullable":true},"emailVerified":{"type":"boolean"},"createdAt":{"type":"string","format":"date-time"}}},"UserListItem":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"email":{"type":"string","format":"email"},"name":{"type":"string","nullable":true},"emailVerified":{"type":"boolean"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"memoCount":{"type":"integer"},"tagCount":{"type":"integer"}}},"MemoListItem":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"title":{"type":"string"},"tags":{"type":"array","items":{"type":"string"}},"confidence":{"type":"string","nullable":true},"reviewDate":{"type":"string","format":"date","nullable":true},"outcome":{"type":"string","nullable":true},"updatedAt":{"type":"string","format":"date-time"},"reviewDue":{"type":"boolean"}}},"MemoDetail":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"title":{"type":"string"},"tiptapJson":{"type":"object","description":"ProseMirror JSON document"},"tags":{"type":"array","items":{"type":"string"}},"confidence":{"type":"string","nullable":true},"reviewDate":{"type":"string","format":"date","nullable":true},"outcome":{"type":"string","nullable":true},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}},"SaveMemoInput":{"type":"object","required":["title","tiptapJson","tags"],"properties":{"title":{"type":"string"},"tiptapJson":{"type":"object","description":"ProseMirror JSON document"},"tags":{"type":"array","items":{"type":"string"}}}},"DashboardNode":{"type":"object","properties":{"nodeId":{"type":"string","format":"uuid"},"nodeType":{"type":"string"},"content":{"type":"string"},"position":{"type":"integer"},"memoId":{"type":"string","format":"uuid"},"memoTitle":{"type":"string"},"memoTags":{"type":"array","items":{"type":"string"}},"confidence":{"type":"string","nullable":true},"reviewDate":{"type":"string","format":"date","nullable":true},"outcome":{"type":"string","nullable":true}}},"CreateSourceConnectionInput":{"type":"object","required":["provider"],"description":"Two paths: (1) OAuth completion — set oauthPendingId (from /sources/oauth/{provider}/start). For GitHub, also include config.owner+config.repo from the repo picker. (2) Manual token paste — set credentials.token.","properties":{"provider":{"type":"string","enum":["zendesk","github"],"example":"github"},"label":{"type":"string","example":"Untools — main repo"},"oauthPendingId":{"type":"string","format":"uuid","nullable":true,"description":"When present, the access token is pulled from the pending OAuth session."},"config":{"type":"object","description":"Provider-specific config. Zendesk: { subdomain } (also filled automatically from the pending OAuth session). GitHub: { owner, repo } (required — the repo the user picked).","example":{"owner":"untools","repo":"ainotetaker"}},"credentials":{"type":"object","description":"Manual token path only. Zendesk: { token: 'email/token:api_token' } (Basic auth). GitHub: { token: 'ghp_xxx' } (PAT). Not required when oauthPendingId is set.","example":{"token":"ghp_xxx"}}}},"SourceConnection":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"provider":{"type":"string","enum":["zendesk","github","upload"]},"label":{"type":"string"},"config":{"type":"object"},"status":{"type":"string","enum":["connected","syncing","error","disconnected"]},"lastSyncAt":{"type":"string","format":"date-time","nullable":true},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"itemCount":{"type":"integer"}}},"SourceItemListItem":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"sourceConnectionId":{"type":"string","format":"uuid"},"externalId":{"type":"string"},"itemType":{"type":"string"},"title":{"type":"string"},"normalizedContent":{"type":"string","description":"Truncated to 240 characters"},"metadata":{"type":"object"},"processedAt":{"type":"string","format":"date-time","nullable":true},"createdAt":{"type":"string","format":"date-time"}}},"SourceItemDetail":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"sourceConnectionId":{"type":"string","format":"uuid"},"externalId":{"type":"string"},"itemType":{"type":"string"},"title":{"type":"string"},"rawContent":{"type":"string"},"normalizedContent":{"type":"string"},"metadata":{"type":"object"},"processedAt":{"type":"string","format":"date-time","nullable":true},"createdAt":{"type":"string","format":"date-time"}}}}}}