{"openapi":"3.0.3","info":{"title":"AI Classify Service API","version":"1.0.0","description":"API for Google Drive document scanning, classification, sorting, splitting, uploads, and email dispatch."},"servers":[{"url":"http://localhost:8080","description":"Local development"},{"url":"https://api.aisortify.com","description":"Production"},{"url":"https://api.aisortify.com","description":"Configured app base URL"}],"tags":[{"name":"Drive","description":"Google Drive connection and scanning"},{"name":"Classify","description":"Document classification and sorting"},{"name":"GCS","description":"Google Cloud Storage upload helpers"},{"name":"Split","description":"Document split/grouping"},{"name":"Mistral OCR","description":"OCR and structured extraction with Mistral"},{"name":"Templates","description":"Folder tree template management"},{"name":"Usage","description":"Plan limits and usage checks"},{"name":"Email","description":"Email dispatch workers"},{"name":"Payable Invoices","description":"Invoice payment tracking rules and tasks"},{"name":"Billing","description":"Subscription webhooks and billing sync"},{"name":"Files","description":"File processing maintenance workers"},{"name":"Account","description":"Account lifecycle and deletion"},{"name":"Team","description":"Team members and invitations"},{"name":"Realtime","description":"Realtime voice agent sessions and safe app tools"},{"name":"Legacy","description":"Development and legacy endpoints"}],"paths":{"/api/payable-invoices/tasks":{"get":{"tags":["Payable Invoices"],"summary":"List payable invoice tasks","description":"Lists invoice payment tasks for the authenticated user organization. Optional filters support fileId, paymentStatus, decisionStatus, reminderStatus, and limit.","security":[{"bearerAuth":[]}],"parameters":[{"name":"fileId","in":"query","schema":{"type":"string"}},{"name":"paymentStatus","in":"query","schema":{"type":"string","enum":["unknown","unpaid","paid"]}},{"name":"decisionStatus","in":"query","schema":{"type":"string","enum":["needs_decision","decided","dismissed"]}},{"name":"reminderStatus","in":"query","schema":{"type":"string","enum":["none","scheduled","sent","cancelled","failed"]}},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":200}}],"responses":{"200":{"description":"Tasks returned","content":{"application/json":{"example":{"success":true,"tasks":[{"id":"0f7a7685-5f6a-4aa9-b3e6-34427d111111","file_id":"7e1c45d0-1111-4ac8-9a5d-111111111111","external_file_id":"provider-file-id","storage_provider":"google_drive","payment_status":"unpaid","decision_status":"decided","due_date":"2026-07-01","remind_at":"2026-07-01T09:00:00.000Z","reminder_status":"scheduled"}]}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}},"post":{"tags":["Payable Invoices"],"summary":"Track a file as a payable invoice","description":"Creates or updates a payment task for a specific file, even when the file is outside a tracked folder.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["fileId"],"properties":{"fileId":{"type":"string"},"action":{"type":"string","enum":["paid","unpaid","remind","later","dismiss"]},"remindAt":{"type":"string","format":"date-time"},"dueDate":{"type":"string","format":"date"},"reminderChannel":{"type":"string","enum":["in_app","desktop","mobile","email","calendar"]},"notes":{"type":"string"}}},"example":{"fileId":"provider-file-id","action":"remind","remindAt":"2026-07-01T09:00:00.000Z"}}}},"responses":{"200":{"description":"Existing task updated"},"201":{"description":"Task created"},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/api/payable-invoices/tasks/{taskId}":{"patch":{"tags":["Payable Invoices"],"summary":"Update a payable invoice task","security":[{"bearerAuth":[]}],"parameters":[{"name":"taskId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["action"],"properties":{"action":{"type":"string","enum":["paid","unpaid","remind","later","dismiss"]},"remindAt":{"type":"string","format":"date-time"},"dueDate":{"type":"string","format":"date"},"paidAt":{"type":"string","format":"date-time"},"reminderChannel":{"type":"string","enum":["in_app","desktop","mobile","email","calendar"]},"notes":{"type":"string","nullable":true}}},"example":{"action":"paid"}}}},"responses":{"200":{"description":"Task updated"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"Task not found"}}}},"/api/payable-invoices/reminders/test-email":{"post":{"tags":["Payable Invoices"],"summary":"Send a payable invoice reminder test email","description":"Sends a test reminder email to workspace recipients. Optional template fields override persisted reminder settings for this test only. Optional taskId, fileId, externalFileId, or driveFileId selects the invoice used to resolve template variables such as openFileUrl and openAppUrl; if omitted, sample invoice data is used.","security":[{"bearerAuth":[]}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"emailSubjectTemplate":{"type":"string","nullable":true,"description":"Subject template override for this test send."},"emailHtmlTemplate":{"type":"string","nullable":true,"description":"HTML template override for this test send."},"emailTextTemplate":{"type":"string","nullable":true,"description":"Plain-text template override for this test send."},"taskId":{"type":"string","format":"uuid","description":"Existing payable invoice task to use for template variables."},"fileId":{"type":"string","description":"Internal file id or provider file id to use when no taskId is supplied."},"externalFileId":{"type":"string","description":"Provider file id fallback used to resolve the selected invoice."},"driveFileId":{"type":"string","description":"Legacy Google Drive file id fallback used to resolve the selected invoice."},"openFileUrl":{"type":"string","format":"uri","description":"Client-resolved preview URL. The backend resolves URLs from the selected invoice when taskId/fileId is supplied."},"openAppUrl":{"type":"string","format":"uri","description":"Client-resolved HTTPS app-open redirect preview. The backend resolves an HTTPS /open/payable-invoices URL from the selected invoice when taskId/fileId is supplied because email clients may strip native deep links."},"nativeOpenAppUrl":{"type":"string","description":"Optional client-resolved native aisortify:// deep link for diagnostics. Legacy ai-sortify:// desktop links remain accepted during migration. Email HTML should use openAppUrl instead."}}},"example":{"emailSubjectTemplate":"Zahlungserinnerung: {{fileName}}","emailHtmlTemplate":"<div><a href=\"{{openFileUrl}}\">Rechnung in Drive öffnen</a><a href=\"{{openAppUrl}}\">In AI Sortify öffnen</a></div>","emailTextTemplate":"Rechnung in Drive öffnen: {{openFileUrl}}\nIn AI Sortify öffnen: {{openAppUrl}}","taskId":"ec6307c7-806c-48d3-869e-390752e01aee","fileId":"5a077170-e4aa-494e-8a2d-cdd07c964d7a","externalFileId":"1W6LaUqEZiUuHvTtavK_drq1000x0CgNT","driveFileId":"1W6LaUqEZiUuHvTtavK_drq1000x0CgNT"}}}},"responses":{"200":{"description":"Test email sent","content":{"application/json":{"example":{"success":true,"recipientCount":1}}}},"400":{"description":"No recipient or invalid request"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"Selected task or file not found"}}}},"/api/payable-invoices/rules":{"get":{"tags":["Payable Invoices"],"summary":"List payable invoice folder rules","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Rules returned","content":{"application/json":{"example":{"success":true,"rules":[{"id":"2d8116f2-1111-41fa-9fd8-111111111111","folder_tree_id":"0f7a7685-1111-4aa9-b3e6-111111111111","folder_path":"Ausgaben/Rechnungen","default_action":"ask","default_delay_days":7,"enabled":true}]}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}},"post":{"tags":["Payable Invoices"],"summary":"Create a payable invoice folder rule","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["folderTreeId"],"properties":{"folderTreeId":{"type":"string","format":"uuid"},"defaultAction":{"type":"string","enum":["ask","mark_unpaid","remind","ignore"]},"defaultDelayDays":{"type":"integer","minimum":0,"maximum":3650},"enabled":{"type":"boolean"}}},"example":{"folderTreeId":"0f7a7685-1111-4aa9-b3e6-111111111111","defaultAction":"ask","defaultDelayDays":7,"enabled":true}}}},"responses":{"201":{"description":"Rule created"},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/api/payable-invoices/rules/{ruleId}":{"patch":{"tags":["Payable Invoices"],"summary":"Update a payable invoice folder rule","security":[{"bearerAuth":[]}],"parameters":[{"name":"ruleId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"defaultAction":{"type":"string","enum":["ask","mark_unpaid","remind","ignore"]},"defaultDelayDays":{"type":"integer","nullable":true,"minimum":0,"maximum":3650},"enabled":{"type":"boolean"}}}}}},"responses":{"200":{"description":"Rule updated"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"Rule not found"}}},"delete":{"tags":["Payable Invoices"],"summary":"Delete a payable invoice folder rule","security":[{"bearerAuth":[]}],"parameters":[{"name":"ruleId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Rule deleted"},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/api/team/invite":{"post":{"tags":["Team"],"summary":"Invite a team member by email","description":"Backend-owned invite flow. The frontend sends only an email; the backend validates permissions, self-invite, duplicate membership, pending invites, plan team limit, and Supabase Auth invite handling.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["email"],"properties":{"email":{"type":"string","format":"email","example":"person@example.com"}}},"example":{"email":"person@example.com"}}}},"responses":{"200":{"description":"Invite accepted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":true},"member":{"type":"object","properties":{"id":{"type":"string","nullable":true},"email":{"type":"string","format":"email"},"status":{"type":"string","example":"invitation_pending"},"role":{"type":"string","example":"member"}}},"delivery":{"type":"string","enum":["supabase_invite","existing_user_attached"]}}},"example":{"success":true,"member":{"id":"9e4214e5-5dbb-4a3e-92a1-d0dca1111111","email":"person@example.com","status":"invitation_pending","role":"member"},"delivery":"supabase_invite"}}}},"400":{"description":"Invalid invite request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"examples":{"invalidEmail":{"value":{"success":false,"error":"invalid_email"}},"selfInvite":{"value":{"success":false,"error":"cannot_invite_self"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"description":"Team member plan limit reached","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"error":{"type":"string","example":"team_member_limit_reached"},"limit":{"$ref":"#/components/schemas/TeamMemberLimit"}}}}}},"403":{"description":"Caller cannot invite team members","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"success":false,"error":"team_invite_forbidden"}}}},"409":{"description":"Duplicate or unavailable invite","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"error":{"type":"string","enum":["team_member_exists","invite_already_pending","team_member_unavailable"]},"member":{"type":"object","nullable":true}}},"examples":{"exists":{"value":{"success":false,"error":"team_member_exists"}},"pending":{"value":{"success":false,"error":"invite_already_pending"}}}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/account/language":{"patch":{"tags":["Account"],"summary":"Update the authenticated user language","description":"Use after OAuth login/signup to sync the selected app locale into public.users.language. Email/password signup can also send language in Supabase auth metadata.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"language":{"type":"string","enum":["en","de","fr","tr"],"example":"de"},"locale":{"type":"string","enum":["en","de","fr","tr"],"example":"de","description":"Alias for language."}}},"example":{"language":"de"}}}},"responses":{"200":{"description":"Language updated","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":true},"user":{"type":"object","properties":{"id":{"type":"string"},"email":{"type":"string"},"language":{"type":"string","enum":["en","de","fr","tr"]}}}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/account/delete":{"post":{"tags":["Account"],"summary":"Delete the authenticated user account","description":"Production account deletion. Revokes Google Drive access, deletes personal operational data, snapshots anonymous aggregate metrics, and removes the Supabase auth user. Google Drive files are not deleted.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["confirm"],"properties":{"confirm":{"type":"string","enum":["DELETE"],"description":"Required confirmation string to prevent accidental deletion."}}},"example":{"confirm":"DELETE"}}}},"responses":{"200":{"description":"Account deletion completed","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":true},"drive":{"type":"object","properties":{"revoked":{"type":"boolean"},"error":{"type":"string","nullable":true}}},"gcs":{"type":"object","properties":{"requested":{"type":"number"},"deleted":{"type":"number"},"failed":{"type":"number"}}},"result":{"type":"object"}}}}}},"400":{"description":"Missing deletion confirmation","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"error":{"type":"string","example":"delete_account_confirmation_required"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/realtime/session":{"post":{"tags":["Realtime"],"summary":"Create an OpenAI Realtime ephemeral session","description":"Creates a short-lived Realtime session for WebRTC voice. Frontend uses the returned client secret to connect to OpenAI. The regular OpenAI API key stays on the backend. The backend injects trusted lightweight context from get_org_dashboard, Drive connection, and folder structure count so the assistant can answer plan, quota, Drive, and connection questions without extra tool calls.","security":[{"bearerAuth":[]}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/SupabaseAuthBody"},{"type":"object","properties":{"model":{"type":"string","default":"gpt-realtime-2"},"voice":{"type":"string","default":"alloy"},"timezone":{"type":"string","default":"Europe/Berlin"},"language":{"type":"string","default":"de","description":"Preferred assistant reply language. Use short codes such as de, en, fr."},"maxToolResults":{"type":"number","default":10,"minimum":1,"maximum":20,"description":"Default limit the assistant should use when calling search_files or search_email_logs unless the user asks for another count."},"confirmBeforeActions":{"type":"boolean","default":true,"description":"When true, the assistant must ask before any modifying action such as retry, move, rename, confirm, or email send."},"enabledTools":{"type":"array","default":["search_files","search_email_logs","get_quota_status","get_drive_status"],"items":{"type":"string","enum":["search_files","search_email_logs","get_quota_status","get_drive_status"]},"description":"Realtime tools enabled for this session. Omit to enable all safe read tools. Empty array disables tools. Plan/quota/Drive context is still injected into the session instructions even when tools are disabled."},"instructionsMode":{"type":"string","default":"concise","enum":["concise","detailed"],"description":"Controls how much explanation the assistant gives in spoken answers. Only concise and detailed are supported."},"vadMode":{"type":"string","default":"server_vad","enum":["semantic_vad","server_vad","disabled"],"description":"Voice activity detection mode. disabled is best for push-to-talk."},"vadEagerness":{"type":"string","default":"low","enum":["low","medium","high","auto"],"description":"Semantic VAD sensitivity. low waits longer and is less likely to trigger on noise."},"vadThreshold":{"type":"number","default":0.85,"minimum":0,"maximum":1,"description":"Server VAD sensitivity. Higher is less sensitive to background noise."},"vadSilenceMs":{"type":"number","default":650,"minimum":200,"maximum":3000,"description":"Server VAD silence duration before a user turn is considered finished."},"interruptionsEnabled":{"type":"boolean","default":false,"description":"When false, input VAD should not interrupt an assistant response."},"autoRespond":{"type":"boolean","default":true,"description":"When true, VAD automatically creates a response after the user finishes speaking."}}}]},"example":{"voice":"alloy","timezone":"Europe/Berlin","language":"de","maxToolResults":10,"confirmBeforeActions":true,"enabledTools":["search_files","search_email_logs","get_quota_status","get_drive_status"],"instructionsMode":"concise","vadMode":"server_vad","vadThreshold":0.85,"vadSilenceMs":650,"interruptionsEnabled":false,"autoRespond":true}}}},"responses":{"200":{"description":"Realtime session created","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":true},"client_secret":{"type":"string","nullable":true,"description":"OpenAI Realtime ephemeral client secret. Prefer this field on the frontend."},"config":{"type":"object","description":"Resolved realtime settings used to create the OpenAI session."},"session":{"type":"object"}}}}}},"400":{"$ref":"#/components/responses/ServerError"},"401":{"$ref":"#/components/responses/Unauthorized"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/realtime/text":{"post":{"tags":["Realtime"],"summary":"Route typed chat through the same safe app tools","description":"Uses a small OpenAI text model to answer typed chat. Backend injects trusted lightweight Drive/quota/user/org context on every request. Typed chat exposes only search_files and search_email_logs; Drive, plan, and quota questions should be answered directly from context without frontend keyword routing.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/SupabaseAuthBody"},{"type":"object","required":["message"],"properties":{"message":{"type":"string","example":"drive status"},"provider":{"type":"string","default":"openai","enum":["openai","vertex"],"description":"Typed chat provider. Use vertex to test Gemini/Vertex routing with the same injected app context and only search_files/search_email_logs tools."},"history":{"type":"array","maxItems":15,"description":"Typed-chat memory. Send the last user/assistant text turns so follow-ups like \"and yesterday\" then \"give them\" keep context. Backend trims to 15 messages and 1000 characters each. No explicit toolContext is needed.","items":{"type":"object","required":["role","content"],"properties":{"role":{"type":"string","enum":["user","assistant"]},"content":{"type":"string"}}}},"model":{"type":"string","description":"Optional text model override. For openai, backend default comes from OPENAI_TEXT_MODEL. For vertex, backend default is gemini-3.1-flash-lite."},"timezone":{"type":"string","default":"Europe/Berlin"},"language":{"type":"string","default":"de"},"maxToolResults":{"type":"number","default":10,"minimum":1,"maximum":20},"confirmBeforeActions":{"type":"boolean","default":true},"enabledTools":{"type":"array","items":{"type":"string","enum":["search_files","search_email_logs"]},"description":"Typed chat supports only search_files and search_email_logs. Omit to enable both. Drive, plan, and quota answers come from injected context."},"instructionsMode":{"type":"string","default":"concise","enum":["concise","detailed"]}}}]},"example":{"provider":"vertex","message":"give them","history":[{"role":"user","content":"how many files did I upload yesterday?"},{"role":"assistant","content":"1 file."},{"role":"user","content":"and failed ones?"},{"role":"assistant","content":"0 files."}],"timezone":"Europe/Berlin","language":"de","maxToolResults":10}}}},"responses":{"200":{"description":"Typed assistant result","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":true},"toolName":{"type":"string","nullable":true,"enum":["search_files","search_email_logs"]},"toolArgs":{"type":"object","nullable":true},"toolResult":{"type":"object","nullable":true,"description":"The executed tool result. Render toolResult.uiAction exactly like voice tool results."},"context":{"type":"object","nullable":true,"description":"Debug information showing which backend tool was called. Frontend does not need to send this back; use history for context."},"answer":{"type":"string","description":"Short answer or spokenSummary from the tool result."},"config":{"type":"object"}}}}}},"400":{"$ref":"#/components/responses/ServerError"},"401":{"$ref":"#/components/responses/Unauthorized"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/realtime/tools/search-files":{"post":{"tags":["Realtime"],"summary":"Run the voice-agent file search tool","description":"Simulation endpoint for the Realtime search_files tool. Searches files for the authenticated user organization using stable intent filters, flexible text matching, optional status, and optional ISO date range.","security":[{"bearerAuth":[]}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/SupabaseAuthBody"},{"type":"object","properties":{"query":{"type":"string","example":"Amazon invoice"},"entity":{"type":"string","example":"Amazon"},"intent":{"type":"string","enum":["invoice","receipt","delivery_note","payroll","contract","personal","fallback","email","tax","bank"]},"status":{"type":"string","enum":["review","done","error","processing","moved"]},"processingStep":{"type":"string","enum":["uploaded","classify","review","rename","move","done"]},"dateRange":{"type":"object","properties":{"from":{"type":"string","example":"2026-05-01"},"to":{"type":"string","example":"2026-06-01"}}},"timezone":{"type":"string","default":"Europe/Berlin"},"limit":{"type":"number","default":10,"maximum":20}}}]},"example":{"query":"Amazon invoice","entity":"Amazon","intent":"invoice","dateRange":{"from":"2026-05-01","to":"2026-06-01"},"limit":20}}}},"responses":{"200":{"description":"Search result with UI action","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":true},"count":{"type":"number"},"limit":{"type":"number","description":"Maximum number of rows returned in this response."},"hasMore":{"type":"boolean","description":"True when more rows may match than were returned. Frontend should show \"Showing first N results. More may match.\""},"resultMode":{"type":"string","enum":["list","count"]},"spokenSummary":{"type":"string"},"files":{"type":"array","items":{"type":"object"}},"uiAction":{"type":"object","properties":{"type":{"type":"string"},"title":{"type":"string"},"fileIds":{"type":"array","items":{"type":"string"}}}}}}}}},"400":{"$ref":"#/components/responses/ServerError"},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/api/realtime/tools/search-email-logs":{"post":{"tags":["Realtime"],"summary":"Run the voice-agent email log search tool","description":"Simulation endpoint for the Realtime search_email_logs tool. Searches grouped email delivery threads for the authenticated user organization. Use this for email delivery, failed email, resend, retry, recipient, and email history questions.","security":[{"bearerAuth":[]}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/SupabaseAuthBody"},{"type":"object","properties":{"query":{"type":"string","example":"monthly invoices","description":"Search recipient email, email rule name, folder name, status, delivery error, or email ids."},"recipientEmail":{"type":"string","example":"anexbm@gmail.com"},"status":{"type":"string","enum":["delivered","failed","sent","bounced","complained","pending"]},"retryable":{"type":"boolean","description":"When true, return only email threads that can be retried."},"dateRange":{"type":"object","properties":{"from":{"type":"string","example":"2026-05-01"},"to":{"type":"string","example":"2026-06-01"}}},"timezone":{"type":"string","default":"Europe/Berlin"},"limit":{"type":"number","default":10,"maximum":20}}}]},"example":{"recipientEmail":"anexbm@gmail.com","status":"failed","retryable":true,"dateRange":{"from":"2026-05-01","to":"2026-06-01"},"limit":10}}}},"responses":{"200":{"description":"Email log search result with UI action","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":true},"count":{"type":"number"},"limit":{"type":"number","description":"Maximum number of email rows returned in this response."},"hasMore":{"type":"boolean","description":"True when more email rows may match than were returned. Frontend should show a small \"showing first N\" message."},"spokenSummary":{"type":"string"},"emails":{"type":"array","items":{"type":"object"}},"uiAction":{"type":"object","properties":{"type":{"type":"string","example":"open_email_logs"},"title":{"type":"string"},"emailLogIds":{"type":"array","items":{"type":"string"}},"threadIds":{"type":"array","items":{"type":"string"}}}}}}}}},"400":{"$ref":"#/components/responses/ServerError"},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/api/realtime/tools/get-quota-status":{"post":{"tags":["Realtime"],"summary":"Run the voice-agent quota status tool","description":"Simulation endpoint for the Realtime get_quota_status tool. Returns current plan, billing period, quota usage, remaining limits, and monthly overview for the authenticated organization.","security":[{"bearerAuth":[]}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/SupabaseAuthBody"},{"type":"object","properties":{"category":{"type":"string","enum":["documents","splits","team","email_rules","all"],"default":"all"},"requested":{"type":"number","minimum":1,"maximum":100,"description":"Optional requested count, for example checking whether 10 more files can be scanned."}}}]},"example":{"category":"documents","requested":10}}}},"responses":{"200":{"description":"Quota and usage status","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":true},"plan":{"type":"object"},"period":{"type":"object"},"usage":{"type":"object"},"monthOverview":{"type":"object"},"categorySummary":{"type":"object","nullable":true},"spokenSummary":{"type":"string"},"uiAction":{"type":"object"}}}}}},"400":{"$ref":"#/components/responses/ServerError"},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/api/realtime/tools/get-drive-status":{"post":{"tags":["Realtime"],"summary":"Run the voice-agent Drive status tool","description":"Simulation endpoint for the Realtime get_drive_status tool. Returns safe Google Drive connection and folder structure status. OAuth tokens are never returned.","security":[{"bearerAuth":[]}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/SupabaseAuthBody"},{"type":"object","properties":{"includeFolders":{"type":"boolean","default":false,"description":"When true, include up to 50 folder structure rows."}}}]},"example":{"includeFolders":true}}}},"responses":{"200":{"description":"Safe Drive connection status","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":true},"drive":{"type":"object"},"folderStructure":{"type":"object"},"spokenSummary":{"type":"string"},"uiAction":{"type":"object"}}}}}},"400":{"$ref":"#/components/responses/ServerError"},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/api/drive/connection":{"post":{"tags":["Drive"],"summary":"Get a valid Google Drive access token","security":[{"bearerAuth":[]}],"description":"Reads the Supabase access token from the Authorization bearer header, resolves the user organization, and refreshes the Google Drive token if needed. Body access_token is still accepted as a temporary fallback.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SupabaseAuthBody"}}}},"responses":{"200":{"description":"Drive connection token","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":true},"access_token":{"type":"string"},"refreshed":{"type":"boolean"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"500":{"description":"Drive connection lookup or refresh failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"examples":{"organizationMissing":{"value":{"error":"organization_missing"}},"driveToken":{"value":{"error":"drive_missing_token"}}}}}}}},"delete":{"tags":["Drive"],"summary":"Disconnect Google Drive","security":[{"bearerAuth":[]}],"description":"Revokes the stored Google OAuth token best-effort, clears stored Drive tokens, marks the connection as disconnected, and keeps root_folder_id/folder metadata so reconnect can reuse the existing Drive structure.","responses":{"200":{"description":"Drive connection disconnected","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":true},"connection":{"type":"object","properties":{"org_id":{"type":"string","format":"uuid"},"provider":{"type":"string","example":"google"},"provider_email":{"type":"string","nullable":true,"example":"user@example.com"},"connected":{"type":"boolean","example":false},"connection_status":{"type":"string","example":"disconnected"},"root_folder_id":{"type":"string","nullable":true,"description":"Kept after disconnect. Reset structure is a separate operation."},"revoked":{"type":"boolean","description":"True when Google token revoke returned success. DB tokens are cleared even if revoke fails."}}}}},"example":{"success":true,"connection":{"org_id":"00000000-0000-0000-0000-000000000000","provider":"google","provider_email":"user@example.com","connected":false,"connection_status":"disconnected","root_folder_id":"1abcDriveRootFolder","revoked":true}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"500":{"description":"Disconnect failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/api/drive/oauth/exchange":{"post":{"tags":["Drive"],"summary":"Exchange Google OAuth code for a Drive connection","security":[{"bearerAuth":[]}],"description":"Mobile PKCE endpoint. Exchanges a Google authorization code, stores Drive tokens server-side, blocks switching Google accounts after a root folder exists, and returns a safe connection object without OAuth tokens.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["code","codeVerifier"],"properties":{"code":{"type":"string","description":"Google OAuth authorization code."},"codeVerifier":{"type":"string","description":"PKCE code verifier generated by the mobile app."},"platform":{"type":"string","enum":["ios","web"],"default":"ios","description":"Selects the Google OAuth client id and redirect URI pair. Defaults to ios for backwards compatibility."},"access_token":{"type":"string","deprecated":true,"description":"Deprecated fallback. Prefer Authorization: Bearer <Supabase token>."}}}}}},"responses":{"200":{"description":"Drive connected","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":true},"connection":{"type":"object","properties":{"org_id":{"type":"string","format":"uuid"},"owner_id":{"type":"string","format":"uuid"},"provider":{"type":"string","example":"google"},"provider_email":{"type":"string","format":"email"},"connected":{"type":"boolean","example":true},"token_expires_at":{"type":"string","format":"date-time"},"connection_status":{"type":"string","example":"connected"},"root_folder_id":{"type":"string","nullable":true}}}}}}}},"400":{"description":"Missing params or refresh token from Google","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"examples":{"missingParams":{"value":{"success":false,"error":"missing_params"}},"refreshToken":{"value":{"success":false,"error":"google_refresh_token_missing"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"409":{"description":"Google account cannot be switched for an existing generated structure","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"examples":{"emailMismatch":{"value":{"success":false,"error":"exchange_google_drive_email_mismatch"}},"alreadyLinked":{"value":{"success":false,"error":"drive_already_linked_with_another_organization"}}}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/drive/structure/generate":{"post":{"tags":["Drive"],"summary":"Generate Google Drive folder structure from a template","security":[{"bearerAuth":[]}],"description":"Creates the Drive folder tree using batched sibling folder creation, saves folder_trees rows, and stores drive_connections.root_folder_id. Paid templates require a non-free organization plan.","requestBody":{"required":true,"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/SupabaseAuthBody"},{"type":"object","required":["template_id"],"properties":{"template_id":{"type":"string"}}}]}}}},"responses":{"200":{"description":"Generated Drive folder structure","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":true},"root_folder_id":{"type":"string"},"folders_created":{"type":"number"},"folder_trees":{"type":"array","items":{"type":"object"}}}}}}},"400":{"description":"Missing template id, invalid template, or Drive not connected","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"examples":{"templateId":{"value":{"success":false,"error":"drive_generate_template_id_required"}},"driveToken":{"value":{"success":false,"error":"drive_missing_token"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Access denied or paid template requires a paid plan","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"examples":{"paidTemplate":{"value":{"success":false,"error":"drive_generate_paid_template_requires_paid_plan"}}}}}},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"Structure already exists","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"examples":{"exists":{"value":{"success":false,"error":"drive_generate_structure_already_exists"}}}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/drive/structure/reset":{"post":{"tags":["Drive"],"summary":"Reset generated Google Drive folder structure","security":[{"bearerAuth":[]}],"description":"Owner-only. Deletes the generated Drive root folder by ID as best effort, then calls drive_reset_structure_tx to clear database structure state.","requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SupabaseAuthBody"}}}},"responses":{"200":{"description":"Structure reset","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":true}}}}}},"400":{"description":"Drive connection or organization state is missing","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"examples":{"driveToken":{"value":{"success":false,"error":"drive_missing_token"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Only owner can reset structure","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"examples":{"accessDenied":{"value":{"success":false,"error":"access_denied"}}}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/scan/preview":{"post":{"tags":["Drive"],"summary":"Preview Drive scan before enqueueing classification tasks","security":[{"bearerAuth":[]}],"description":"Read-only preview. Lists candidate files and returns counts for valid, oversized, and unsupported files. Does not insert DB rows, move files, or enqueue Cloud Tasks.","requestBody":{"required":false,"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/SupabaseAuthBody"},{"type":"object","properties":{"includeFiles":{"type":"boolean","default":false,"description":"Debug only. Include file details in the preview response."},"files":{"type":"array","items":{"$ref":"#/components/schemas/DriveFileInput"}}}}]}}}},"responses":{"200":{"description":"Preview summary and files","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":true},"source":{"type":"string","enum":["drive","upload"]},"summary":{"type":"object","properties":{"total":{"type":"number"},"valid":{"type":"number"},"oversized":{"type":"number"},"unsupported":{"type":"number"},"maxFilesAllowed":{"type":"number"},"maxSizeBytes":{"type":"number"},"maxSizeMb":{"type":"number"},"quotaAllowed":{"type":"boolean"},"quotaRemaining":{"type":"number","nullable":true},"quotaLimit":{"type":"number","nullable":true},"quotaUsed":{"type":"number"}}},"quota":{"$ref":"#/components/schemas/ClassificationQuota"},"files":{"description":"Only returned when includeFiles is true.","type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"mimeType":{"type":"string"},"size":{"type":"number"},"status":{"type":"string","enum":["valid","oversized","unsupported"]},"reason":{"type":"string"}}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/scan/quota":{"post":{"tags":["Drive"],"summary":"Check classification quota before mobile upload","security":[{"bearerAuth":[]}],"description":"Lightweight mobile preflight. Checks whether the organization can classify the requested number of new files before the app uploads them. This does not reserve quota; /api/scan reserves quota after upload.","requestBody":{"required":true,"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/SupabaseAuthBody"},{"type":"object","required":["count"],"properties":{"count":{"type":"number","minimum":0,"example":3}}}]}}}},"responses":{"200":{"description":"Classification quota status","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":true},"quota":{"$ref":"#/components/schemas/ClassificationQuota"}}}}}},"400":{"description":"Invalid count","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"error":{"type":"string","example":"invalid_classification_count"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/scan":{"post":{"tags":["Drive"],"summary":"Scan Drive files and enqueue classification tasks","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/SupabaseAuthBody"},{"type":"object","properties":{"aiEngine":{"type":"string","enum":["mistral","vertex"],"default":"mistral","description":"Classification AI engine. mistral uses the Mistral OCR workflow; vertex uses the Vertex AI schema workflow."},"classificationPath":{"type":"string","nullable":true},"entityName":{"type":"string","nullable":true},"documentSourceMode":{"type":"string","enum":["drive_public_url","gcs","base64"],"default":"drive_public_url","description":"Mistral document source mode. Vertex currently uses GCS regardless of this value."},"files":{"type":"array","items":{"$ref":"#/components/schemas/DriveFileInput"}}}}]}}}},"responses":{"200":{"description":"Files queued","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":true},"inserted":{"type":"array","items":{"type":"string"}},"oversized":{"type":"array","items":{"type":"string"}},"duplicated":{"type":"array","items":{"type":"string"}},"quota":{"$ref":"#/components/schemas/ClassificationQuota"}}}}}},"400":{"description":"Too many files for one scan request","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"error":{"type":"string","example":"scan_file_count_exceeds_request_limit"},"quota":{"$ref":"#/components/schemas/ClassificationQuota"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"description":"Classification monthly plan limit reached","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"error":{"type":"string","example":"classification_limit_reached"},"quota":{"$ref":"#/components/schemas/ClassificationQuota"}}}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/scan/review":{"post":{"tags":["Drive"],"summary":"Scan Drive files for review without moving them","description":"Same input as /api/scan, but the classify task stops after AI classification. Files are saved with status=review and processing_step=review so the app can show the proposed folder, filename, reason, money fields, and evidence before the user confirms.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/SupabaseAuthBody"},{"type":"object","properties":{"aiEngine":{"type":"string","enum":["mistral","vertex"],"default":"mistral"},"documentSourceMode":{"type":"string","enum":["drive_public_url","gcs","base64"],"default":"drive_public_url"},"files":{"type":"array","items":{"$ref":"#/components/schemas/DriveFileInput"}}}}]}}}},"responses":{"200":{"description":"Files queued for review classification","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":true},"inserted":{"type":"array","items":{"type":"string"}},"oversized":{"type":"array","items":{"type":"string"}},"duplicated":{"type":"array","items":{"type":"string"}},"quota":{"$ref":"#/components/schemas/ClassificationQuota"}}}}}},"400":{"description":"Too many files for one scan request","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"error":{"type":"string","example":"scan_file_count_exceeds_request_limit"},"quota":{"$ref":"#/components/schemas/ClassificationQuota"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"description":"Classification monthly plan limit reached","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"error":{"type":"string","example":"classification_limit_reached"},"quota":{"$ref":"#/components/schemas/ClassificationQuota"}}}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/scan/confirm":{"post":{"tags":["Drive"],"summary":"Confirm reviewed files and enqueue Drive move/rename","description":"Moves and renames files that were previously classified by /api/scan/review. fileIds may be files.id UUIDs or Google Drive file IDs. Only files with status=review and a saved classification are confirmed.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/SupabaseAuthBody"},{"type":"object","required":["fileIds"],"properties":{"fileIds":{"type":"array","maxItems":50,"items":{"type":"string"},"description":"Files table UUIDs or Google Drive file IDs to confirm."}}}]}}}},"responses":{"200":{"description":"Confirm result","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":true},"confirmed":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"externalFileId":{"type":"string"}}}},"skipped":{"type":"array","items":{"type":"object","properties":{"fileId":{"type":"string"},"reason":{"type":"string","enum":["not_found_or_forbidden","not_in_review","missing_classification"]}}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/scan/review/{fileId}":{"patch":{"tags":["Drive"],"summary":"Save user edits for a reviewed file","description":"Updates the saved review result before confirmation. Only files with status=review can be edited. Confirm still receives fileIds only and uses the saved edited database result.","security":[{"bearerAuth":[]}],"parameters":[{"name":"fileId","in":"path","required":true,"schema":{"type":"string"},"description":"files.id UUID or Google Drive file ID."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/SupabaseAuthBody"},{"type":"object","properties":{"new_name":{"type":"string","description":"Proposed final filename. Extension is allowed; backend will preserve the original file extension during Drive rename."},"classification":{"type":"object","properties":{"folder_id":{"type":"string"},"classification_path":{"type":"string"},"doc_type":{"type":"string"},"reason":{"type":"string"},"confidence":{"type":"string","enum":["low","medium","high"]}}},"parties":{"type":"object","properties":{"issuer_name":{"type":"string"},"recipient_name":{"type":"string"},"entity_type":{"type":"string","enum":["EMPLOYEE","VENDOR","GOVERNMENT","NONE"]},"entity_name":{"type":"string"},"entity_short_name":{"type":"string"}}},"document":{"type":"object","properties":{"doc_id":{"type":"string"},"date":{"type":"string","format":"date"},"language":{"type":"string"}}},"money":{"type":"object","properties":{"direction":{"type":"string","enum":["ORG_RECEIVES_MONEY","ORG_PAYS_MONEY","NO_MONEY_DIRECTION","UNKNOWN"]},"currency":{"type":"string","nullable":true},"total_gross":{"type":"number","nullable":true},"total_net":{"type":"number","nullable":true},"tax_amount":{"type":"number","nullable":true},"tax_rate":{"type":"number","nullable":true},"payment_method":{"type":"string","nullable":true}}},"warnings":{"type":"array","items":{"type":"string"}}}}]},"example":{"new_name":"2026-05-18_Rechnung_Amazon_12345.pdf","classification":{"folder_id":"expenses","classification_path":"02_Ausgaben","doc_type":"invoice","reason":"Invoice from Amazon paid by the organization.","confidence":"high"},"parties":{"issuer_name":"Amazon EU S.a r.l.","recipient_name":"PP Essen GmbH","entity_type":"VENDOR","entity_name":"Amazon","entity_short_name":"Amazon"},"document":{"doc_id":"12345","date":"2026-05-18","language":"de"},"money":{"direction":"ORG_PAYS_MONEY","currency":"EUR","total_gross":35.4,"total_net":29.75,"tax_amount":5.65,"tax_rate":19,"payment_method":"card"},"warnings":[]}}}},"responses":{"200":{"description":"Review edits saved","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":true},"file":{"type":"object"}}}}}},"400":{"description":"Invalid classification path, missing classification, or missing organization."},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"File not found or forbidden for the current organization."},"409":{"description":"File is not currently in review state."},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/scan/retry":{"post":{"tags":["Drive"],"summary":"Retry files currently in error state","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/SupabaseAuthBody"},{"type":"object","required":["fileIds"],"properties":{"aiEngine":{"type":"string","enum":["mistral","vertex"],"default":"mistral","description":"AI engine to use when retrying classification."},"fileIds":{"type":"array","items":{"type":"string"}}}}]}}}},"responses":{"200":{"description":"Retry result","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":true},"retried":{"type":"array","items":{"type":"string"}},"skipped":{"type":"array","items":{"type":"object","properties":{"fileId":{"type":"string"},"reason":{"type":"string","enum":["not_found_or_forbidden","not_error"]}}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/usage/team-member-limit":{"post":{"tags":["Usage"],"summary":"Check whether the organization can add team members","security":[{"bearerAuth":[]}],"description":"Checks plans.max_team_members against active and pending users in the current organization. Use before inviting or creating team members.","requestBody":{"required":false,"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/SupabaseAuthBody"},{"type":"object","properties":{"count":{"type":"number","minimum":1,"default":1}}}]}}}},"responses":{"200":{"description":"Team member plan limit status","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":true},"limit":{"$ref":"#/components/schemas/TeamMemberLimit"}}}}}},"400":{"description":"Invalid count","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"examples":{"invalidCount":{"value":{"success":false,"error":"invalid_team_member_count"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/usage/email-rule-limit":{"post":{"tags":["Usage"],"summary":"Check whether the organization can create email rules","security":[{"bearerAuth":[]}],"description":"Checks plans.max_email_rules against enabled email rules in the current organization. Use before creating a new email automation rule.","requestBody":{"required":false,"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/SupabaseAuthBody"},{"type":"object","properties":{"count":{"type":"number","minimum":1,"default":1}}}]}}}},"responses":{"200":{"description":"Email rule plan limit status","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":true},"limit":{"$ref":"#/components/schemas/EmailRuleLimit"}}}}}},"400":{"description":"Invalid count","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"examples":{"invalidCount":{"value":{"success":false,"error":"invalid_email_rule_count"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/email/v2/rules/summary":{"get":{"tags":["Email"],"summary":"Get email rule delivery summary","security":[{"bearerAuth":[]}],"description":"Returns one aggregated row per email rule using the get_email_rule_summary RPC. Frontend should use this instead of aggregating raw email logs.","responses":{"200":{"description":"Per-rule email delivery summary","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":true},"rules":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string","nullable":true},"folderName":{"type":"string","nullable":true},"recipientEmail":{"type":"string","format":"email"},"label":{"type":"string","example":"Invoices → finance"},"enabled":{"type":"boolean"},"schedule":{"type":"string","nullable":true,"enum":["instant","daily","weekly","monthly"]},"lastRunAt":{"type":"string","nullable":true},"nextRunAt":{"type":"string","nullable":true},"delivered":{"type":"number","example":142},"failed":{"type":"number","example":0},"processing":{"type":"number","example":1},"total":{"type":"number","example":143},"lastActivityAt":{"type":"string","nullable":true}}}}}},"example":{"success":true,"rules":[{"id":"9e4214e5-5dbb-4a3e-92a1-d0dca1111111","name":"Invoices","folderName":"Invoices","recipientEmail":"finance@example.com","label":"Invoices → finance","enabled":true,"schedule":"daily","lastRunAt":"2026-05-24T08:00:00Z","nextRunAt":"2026-05-25T08:00:00Z","delivered":142,"failed":0,"processing":0,"total":142,"lastActivityAt":"2026-05-24T08:00:00Z"}]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/email/v2/send":{"post":{"tags":["Email"],"summary":"Send documents by matching enabled email rules","security":[{"bearerAuth":[]}],"description":"Email v2 workflow. Matches done files against enabled email rules by folder path, creates temporary Drive public permissions, sends with Resend, stores resend_email_id, and relies on webhook/cleanup for permission removal.","requestBody":{"required":true,"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/SupabaseAuthBody"},{"type":"object","required":["file_ids"],"properties":{"file_ids":{"type":"array","items":{"type":"string"}}}}]}}}},"responses":{"200":{"description":"Email send batches queued/sent through Resend","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":true},"requested":{"type":"number"},"matched_files":{"type":"number"},"matched_rules":{"type":"number"},"batches":{"type":"array","items":{"type":"object"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"500":{"description":"Email send failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"examples":{"fileIdsRequired":{"value":{"success":false,"error":"email_file_ids_required"}},"organizationMissing":{"value":{"success":false,"error":"organization_missing"}},"driveToken":{"value":{"success":false,"error":"drive_missing_token"}}}}}}}}},"/api/email/v2/rules/{ruleId}/schedule":{"patch":{"tags":["Email"],"summary":"Update an email rule schedule","security":[{"bearerAuth":[]}],"description":"Updates a rule schedule safely. Sets last_run_at to now and calculates next_run_at so switching to daily, weekly, or monthly does not immediately send older matching files.","parameters":[{"name":"ruleId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["schedule"],"properties":{"schedule":{"type":"string","enum":["instant","daily","weekly","monthly"]}}},"example":{"schedule":"weekly"}}}},"responses":{"200":{"description":"Email rule schedule updated","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":true},"rule":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"schedule":{"type":"string","enum":["instant","daily","weekly","monthly"]},"last_run_at":{"type":"string","nullable":true},"next_run_at":{"type":"string","nullable":true},"enabled":{"type":"boolean"}}}}}}}},"400":{"description":"Invalid schedule","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"examples":{"invalidSchedule":{"value":{"success":false,"error":"invalid_email_rule_schedule","allowed":["instant","daily","weekly","monthly"]}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"Rule not found or not accessible","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"examples":{"notFound":{"value":{"success":false,"error":"email_rule_not_found"}}}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/email/v2/rules/{ruleId}/enabled":{"patch":{"tags":["Email"],"summary":"Enable or disable an email rule","security":[{"bearerAuth":[]}],"description":"Updates a rule enabled state safely. Re-enabling a rule resets last_run_at to now and recalculates next_run_at so files from the disabled period are not sent as backlog.","parameters":[{"name":"ruleId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["enabled"],"properties":{"enabled":{"type":"boolean"}}},"examples":{"enable":{"value":{"enabled":true}},"disable":{"value":{"enabled":false}}}}}},"responses":{"200":{"description":"Email rule enabled state updated","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":true},"rule":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"schedule":{"type":"string","enum":["instant","daily","weekly","monthly"]},"last_run_at":{"type":"string","nullable":true},"next_run_at":{"type":"string","nullable":true},"enabled":{"type":"boolean"}}}}}}}},"400":{"description":"Invalid enabled value","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"examples":{"invalidEnabled":{"value":{"success":false,"error":"invalid_email_rule_enabled"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"Rule not found or not accessible","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"examples":{"notFound":{"value":{"success":false,"error":"email_rule_not_found"}}}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/email/v2/logs/{emailLogId}/resend":{"post":{"tags":["Email"],"summary":"Resend a previous email log","security":[{"bearerAuth":[]}],"description":"Creates a new email_sent_logs row linked to the original failed email by retry_of_log_id, reuses the original recipient and file_ids, creates fresh temporary Drive public permissions, and sends again with Resend. If a retry child log id is provided, the backend resolves it back to the original log automatically.","parameters":[{"name":"emailLogId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SupabaseAuthBody"}}}},"responses":{"200":{"description":"Retry send result","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmailRetryResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"500":{"description":"Retry failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"examples":{"notFound":{"value":{"success":false,"error":"email_log_not_found"}},"filesMissing":{"value":{"success":false,"error":"email_retry_files_missing"}},"driveToken":{"value":{"success":false,"error":"drive_missing_token"}},"resend":{"value":{"success":false,"error":"resend_email_failed"}}}}}}}}},"/api/email/v2/testing/failed-log":{"post":{"tags":["Email"],"summary":"Create a failed email log for retry testing","security":[{"bearerAuth":[]}],"description":"Development-only helper. Creates a failed email_sent_logs row from existing done files in the current organization so mobile can test the resend endpoint.","requestBody":{"required":true,"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/SupabaseAuthBody"},{"type":"object","required":["file_ids"],"properties":{"file_ids":{"type":"array","items":{"type":"string"}},"recipient_email":{"type":"string","format":"email","default":"test@example.com"},"rule_id":{"type":"string","format":"uuid","description":"Optional test rule id. Defaults to all-zero UUID."},"error_message":{"type":"string","default":"test_email_failed"}}}]}}}},"responses":{"200":{"description":"Failed email log created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmailTestFailedLogResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"Test endpoint disabled in production","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"examples":{"disabled":{"value":{"success":false,"error":"test_endpoint_disabled"}}}}}},"500":{"description":"Failed test log creation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"examples":{"missingFiles":{"value":{"success":false,"error":"email_test_files_missing_or_not_done"}},"fileIdsRequired":{"value":{"success":false,"error":"email_test_file_ids_required"}}}}}}}}},"/api/webhooks/resend":{"post":{"tags":["Email"],"summary":"Receive Resend delivery webhooks","description":"Raw-body webhook endpoint. Verifies Svix/Resend headers, updates email_sent_logs, and removes temporary Drive public permissions for delivered/failed/bounced/complained events.","responses":{"200":{"description":"Webhook processed","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","example":true}}}}}},"202":{"description":"Webhook ignored because event type is not handled","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","example":true},"ignored":{"type":"boolean","example":true}}}}}},"400":{"description":"Invalid webhook signature or payload","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","example":false},"error":{"type":"string","example":"webhook_invalid"}}}}}}}}},"/api/webhooks/revenuecat":{"post":{"tags":["Billing"],"summary":"Receive RevenueCat subscription webhooks","description":"Raw-body webhook endpoint. Use the organization id as the RevenueCat app_user_id/customer id. The endpoint stores the event, updates organizations.plan_id/subscription fields, and appends org_plan_history.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["event"],"properties":{"api_version":{"type":"string","example":"1.0"},"event":{"type":"object","required":["type","app_user_id"],"properties":{"id":{"type":"string","example":"event_123"},"type":{"type":"string","example":"INITIAL_PURCHASE"},"app_user_id":{"type":"string","format":"uuid","description":"Organization id. Owner user id is accepted as a fallback."},"product_id":{"type":"string","example":"ai_sortify_pro_monthly"},"entitlement_ids":{"type":"array","items":{"type":"string"},"example":["plan_pro"]},"environment":{"type":"string","example":"SANDBOX"},"store":{"type":"string","example":"APP_STORE"},"purchased_at_ms":{"type":"number","example":1778770000000},"expiration_at_ms":{"type":"number","example":1781448400000}}}}},"examples":{"proPurchase":{"value":{"api_version":"1.0","event":{"id":"evt_test_001","type":"INITIAL_PURCHASE","app_user_id":"00000000-0000-0000-0000-000000000000","product_id":"ai_sortify_pro_monthly","entitlement_ids":["plan_pro"],"environment":"SANDBOX","store":"APP_STORE","purchased_at_ms":1778770000000,"expiration_at_ms":1781448400000}}},"expiration":{"value":{"api_version":"1.0","event":{"id":"evt_test_002","type":"EXPIRATION","app_user_id":"00000000-0000-0000-0000-000000000000","product_id":"ai_sortify_pro_monthly","entitlement_ids":["plan_pro"],"environment":"SANDBOX","store":"APP_STORE","purchased_at_ms":1778770000000,"expiration_at_ms":1781448400000}}}}}}},"responses":{"200":{"description":"Webhook processed or duplicate ignored","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":true},"duplicate":{"type":"boolean","example":false},"event_id":{"type":"string","example":"evt_test_001"},"org_id":{"type":"string","format":"uuid"},"plan_id":{"type":"string","example":"pro"},"subscription_status":{"type":"string","example":"active"}}}}}},"400":{"description":"Invalid JSON or missing required RevenueCat event data","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"examples":{"invalidJson":{"value":{"success":false,"error":"revenuecat_webhook_invalid_json"}},"missingCustomer":{"value":{"success":false,"error":"revenuecat_customer_id_missing"}}}}}},"401":{"description":"Webhook bearer token is invalid","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"examples":{"unauthorized":{"value":{"success":false,"error":"revenuecat_webhook_unauthorized"}}}}}},"500":{"description":"Organization lookup failed or subscription update failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"examples":{"secretMissing":{"value":{"success":false,"error":"revenuecat_webhook_secret_missing"}},"orgMissing":{"value":{"success":false,"error":"revenuecat_organization_not_found"}}}}}}}}},"/api/internal/email/v2/cleanup-permissions":{"post":{"tags":["Email"],"summary":"Backup cleanup for expired email Drive permissions","description":"Backup cleanup endpoint for cron/Cloud Scheduler. Removes open email_public_permissions rows whose expires_at is in the past.","responses":{"200":{"description":"Cleanup result","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":true},"removed":{"type":"number"},"failed":{"type":"number"}}}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/internal/email/v2/dispatch":{"post":{"tags":["Email"],"summary":"Dispatch due email v2 rules","description":"Internal scheduler endpoint. Finds enabled email_rules where next_run_at is due or null, sends matching done files since last_run_at, and advances last_run_at/next_run_at after successful sends.","responses":{"200":{"description":"Dispatch result","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":true},"rules_checked":{"type":"number"},"rules":{"type":"array","items":{"type":"object"}}}}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/internal/files/mark-stale":{"post":{"tags":["Files"],"summary":"Mark stale processing files as retryable errors","description":"Internal scheduler endpoint. Finds files stuck in processing longer than the threshold and marks them as error with a precise timeout code, so the mobile app can show retry instead of endless progress.","parameters":[{"name":"x-internal-cron-secret","in":"header","required":false,"description":"Required only when INTERNAL_CRON_SECRET is configured on the server.","schema":{"type":"string"}}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"olderThanMinutes":{"type":"integer","default":15,"minimum":1,"maximum":1440,"description":"Only files whose updated_at is older than this threshold are marked stale."},"limit":{"type":"integer","default":100,"minimum":1,"maximum":500,"description":"Maximum number of stale files to mark in one run."}}},"example":{"olderThanMinutes":15,"limit":100}}}},"responses":{"200":{"description":"Stale file cleanup result","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":true},"thresholdMinutes":{"type":"integer","example":15},"limit":{"type":"integer","example":100},"marked":{"type":"integer","example":3},"files":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"external_file_id":{"type":"string"},"original_name":{"type":"string"},"previous_step":{"type":"string","example":"uploaded"},"error_code":{"type":"string","example":"classify_task_not_started"},"stale_since":{"type":"string","format":"date-time"}}}}}}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/classify/run":{"post":{"tags":["Classify"],"summary":"Classify and sort one Drive file using configured provider","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["fileId","userId"],"properties":{"fileId":{"type":"string"},"userId":{"type":"string"},"aiEngine":{"type":"string","enum":["mistral","vertex"],"default":"mistral","description":"Classification AI engine. vertex uses the new Vertex AI schema workflow."},"classificationPath":{"type":"string","nullable":true},"entityName":{"type":"string","nullable":true},"documentSourceMode":{"type":"string","enum":["gcs","base64","drive_public_url"],"default":"drive_public_url","description":"Optional Mistral document source. Vertex currently uses GCS. base64 is for small-file tests. drive_public_url makes a temporary public Drive URL for Mistral."}}}}}},"responses":{"200":{"description":"Classification result or business error","content":{"application/json":{"schema":{"type":"object","properties":{"finalResult":{"type":"object"},"fileId":{"type":"string"}}}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/classify/mistral":{"post":{"tags":["Classify"],"summary":"Classify and sort one Drive file with Mistral OCR","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["fileId","userId"],"properties":{"fileId":{"type":"string"},"userId":{"type":"string"},"classificationPath":{"type":"string","nullable":true},"entityName":{"type":"string","nullable":true}}}}}},"responses":{"200":{"description":"Mistral classification result or business error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ClassifyWorkflowResponse"}}}},"500":{"description":"Unexpected Mistral classification failure","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"examples":{"mistralCapacity":{"value":{"success":false,"error":"Service tier capacity exceeded for this model."}},"timeout":{"value":{"success":false,"error":"Request timed out"}}}}}}}}},"/api/classify/vertex":{"post":{"tags":["Classify"],"summary":"Classify and sort one Drive file with Vertex AI","description":"Uses the new schema-template based Vertex AI workflow. It uploads the Drive file to GCS, calls Vertex with a generated responseSchema, then uses the same filename/path/sort pipeline as Mistral.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["fileId","userId"],"properties":{"fileId":{"type":"string"},"userId":{"type":"string"},"classificationPath":{"type":"string","nullable":true},"entityName":{"type":"string","nullable":true}}}}}},"responses":{"200":{"description":"Vertex classification result or business error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ClassifyWorkflowResponse"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/classify/schema/mistral":{"get":{"tags":["Classify"],"summary":"Generate the final Mistral schema for a folder tree template","description":"Debug/test endpoint. Pass a folder_tree_templates.name and optional organizationName to preview the runtime Mistral JSON schema with generated folder/path/document type enums.","parameters":[{"name":"templateName","in":"query","required":true,"schema":{"type":"string"}},{"name":"organizationName","in":"query","required":false,"schema":{"type":"string","default":"TEST_ORG"}}],"responses":{"200":{"description":"Generated Mistral schema","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/ServerError"}}},"post":{"tags":["Classify"],"summary":"Generate the final Mistral schema for a folder tree template","description":"Debug/test endpoint. Pass a folder_tree_templates.name and optional organizationName to preview the runtime Mistral JSON schema with generated folder/path/document type enums.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["templateName"],"properties":{"templateName":{"type":"string","description":"folder_tree_templates.name"},"organizationName":{"type":"string","default":"TEST_ORG"}}},"examples":{"gastro":{"value":{"templateName":"gastro","organizationName":"PP Essen GmbH"}}}}}},"responses":{"200":{"description":"Generated Mistral schema","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"404":{"description":"Template not found"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/classify/schema/vertex":{"get":{"tags":["Classify"],"summary":"Generate the final Vertex responseSchema for a folder tree template","description":"Debug/test endpoint. Pass a folder_tree_templates.name and optional organizationName to preview the runtime Vertex responseSchema with generated folder/path enums.","parameters":[{"name":"templateName","in":"query","required":true,"schema":{"type":"string"}},{"name":"organizationName","in":"query","required":false,"schema":{"type":"string","default":"TEST_ORG"}}],"responses":{"200":{"description":"Generated Vertex responseSchema","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/ServerError"}}},"post":{"tags":["Classify"],"summary":"Generate the final Vertex responseSchema for a folder tree template","description":"Debug/test endpoint. Pass a folder_tree_templates.name and optional organizationName to preview the runtime Vertex responseSchema with generated folder/path enums.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["templateName"],"properties":{"templateName":{"type":"string","description":"folder_tree_templates.name"},"organizationName":{"type":"string","default":"TEST_ORG"}}},"examples":{"gastro":{"value":{"templateName":"gastro","organizationName":"PP Essen GmbH"}}}}}},"responses":{"200":{"description":"Generated Vertex responseSchema","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"404":{"description":"Template not found"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/classify/logs":{"get":{"tags":["Classify"],"summary":"List recent classify workflow logs","description":"Debug endpoint backed by in-memory logs. Shows recent classify runs and latest steps.","responses":{"200":{"description":"Recent classify runs","content":{"application/json":{"schema":{"type":"object"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/classify/logs/{id}":{"get":{"tags":["Classify"],"summary":"Get one classify workflow log roadmap","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Classify run with all recorded steps","content":{"application/json":{"schema":{"type":"object"}}}},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/classify/logs/page":{"get":{"tags":["Classify"],"summary":"Open classify workflow log page","description":"Debug HTML page. Click a run to see the step roadmap for the new Mistral workflow.","responses":{"200":{"description":"HTML log page","content":{"text/html":{"schema":{"type":"string"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/folder-tree-templates/page":{"get":{"tags":["Templates"],"summary":"Open folder tree template manager page","responses":{"200":{"description":"HTML management page","content":{"text/html":{"schema":{"type":"string"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/folder-tree-templates":{"get":{"tags":["Templates"],"summary":"List folder tree templates","responses":{"200":{"description":"Folder tree templates","content":{"application/json":{"schema":{"type":"object"}}}},"500":{"$ref":"#/components/responses/ServerError"}}},"post":{"tags":["Templates"],"summary":"Create folder tree template","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FolderTreeTemplatePayload"}}}},"responses":{"201":{"description":"Created template","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/ai/folder-tree-templates/generate":{"post":{"tags":["Templates"],"summary":"Generate an AI folder tree base structure","description":"Preferred AI namespace. Platform-super-admin endpoint. Generates a base_structure draft for folder_tree_templates. It does not save to the database and does not create Google Drive folders. Localization is handled by the /api/ai/folder-tree-templates/preview endpoint.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/SupabaseAuthBody"},{"$ref":"#/components/schemas/FolderTreeTemplatePreviewRequest"}]}}}},"responses":{"200":{"description":"Generated base structure","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":true},"base_structure":{"type":"object"},"meta":{"type":"object"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Platform super admin access required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/folder-tree-templates/generate":{"post":{"tags":["Templates"],"summary":"Generate an AI folder tree base structure (legacy URL)","description":"Legacy alias for /api/ai/folder-tree-templates/generate. Kept for backwards compatibility.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/SupabaseAuthBody"},{"$ref":"#/components/schemas/FolderTreeTemplatePreviewRequest"}]}}}},"responses":{"200":{"description":"Generated base structure","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":true},"base_structure":{"type":"object"},"meta":{"type":"object"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Platform super admin access required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/ai/folder-tree-templates/preview":{"post":{"tags":["Templates"],"summary":"Preview an AI-generated folder tree template draft","description":"Preferred AI namespace. Requires Supabase access token in the Authorization bearer header. Body access_token is also accepted for Swagger testing. Generates a folder_tree_templates-compatible draft with Gemini 2.5 Flash Lite in the first configured European Vertex region. The draft is returned for frontend preview and is not saved. When localizationLanguages is provided, template.localized and node.localized entries are returned.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/SupabaseAuthBody"},{"$ref":"#/components/schemas/FolderTreeTemplatePreviewRequest"}]}}}},"responses":{"200":{"description":"Generated template draft","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FolderTreeTemplatePreviewResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/folder-tree-templates/preview":{"post":{"tags":["Templates"],"summary":"Preview an AI-generated folder tree template draft (legacy URL)","description":"Legacy alias for /api/ai/folder-tree-templates/preview. Kept for backwards compatibility.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/SupabaseAuthBody"},{"$ref":"#/components/schemas/FolderTreeTemplatePreviewRequest"}]}}}},"responses":{"200":{"description":"Generated template draft","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FolderTreeTemplatePreviewResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/folder-tree-templates/{id}":{"get":{"tags":["Templates"],"summary":"Get folder tree template","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Folder tree template","content":{"application/json":{"schema":{"type":"object"}}}},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/ServerError"}}},"patch":{"tags":["Templates"],"summary":"Update folder tree template","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FolderTreeTemplatePayload"}}}},"responses":{"200":{"description":"Updated template","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/classifier-schema-templates":{"get":{"tags":["Templates"],"summary":"List classifier schema templates","responses":{"200":{"description":"Classifier schema templates","content":{"application/json":{"schema":{"type":"object"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/classify/sort":{"post":{"tags":["Classify"],"summary":"Sort a previously classified file","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["fileId"],"properties":{"fileId":{"type":"string"}}}}}},"responses":{"200":{"description":"Sort result","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"fileId":{"type":"string"},"error":{"type":"string"}}}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/classify":{"post":{"tags":["Classify"],"summary":"Legacy Cloud Task classify worker alias","description":"Legacy alias for /api/classify/run. Keep only for deployed Cloud Tasks that still call the old route.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["fileId","userId"],"properties":{"fileId":{"type":"string"},"userId":{"type":"string"},"classificationPath":{"type":"string","nullable":true},"entityName":{"type":"string","nullable":true}}}}}},"responses":{"200":{"description":"Classification result or business error","content":{"application/json":{"schema":{"type":"object"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/sort":{"post":{"tags":["Classify"],"summary":"Legacy Cloud Task sort worker alias","description":"Legacy alias for /api/classify/sort. Keep only for deployed Cloud Tasks that still call the old route.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["fileId"],"properties":{"fileId":{"type":"string"}}}}}},"responses":{"200":{"description":"Sort result","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"fileId":{"type":"string"},"error":{"type":"string"}}}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/gdrive/refresh-token":{"post":{"tags":["Drive"],"summary":"Refresh a Drive connection token by organization id","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["orgId"],"properties":{"orgId":{"type":"string"}}}}}},"responses":{"200":{"description":"Refresh result","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/gdrive/move-file":{"post":{"tags":["Drive"],"summary":"Move one Google Drive file and update its saved path","description":"Authenticated frontend endpoint. Moves a tracked Google Drive file to the requested folder, scoped to the caller organization, then updates the file storage path, status, and timestamp.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["external_file_id"],"properties":{"external_file_id":{"type":"string","description":"Provider file id stored in files.external_file_id."},"target_path":{"type":"string","description":"Preferred mode. Full app folder path under the workspace Drive root. Backend validates the first segment, creates missing folders, moves the Drive file there, and saves the normalized path.","example":"04_Ausgaben/2025/08_August/x"},"new_external_parent_id":{"type":"string","description":"Existing provider folder id to move the file into. Use with new_path when the frontend already created or selected the target folder."},"new_path":{"type":"string","description":"Legacy mode. Application folder path to save in files.path after the Drive move succeeds. Backend trims trailing slashes, rejects unsafe paths, and when root template folders exist requires the first path segment to match an org root folder. Dynamic subfolders after the root are allowed.","example":"02_Ausgaben/2026/05"}}},"example":{"external_file_id":"1AbCdEfGhIjKlMnOpQr","target_path":"04_Ausgaben/2025/08_August/x"}}}},"responses":{"200":{"description":"File moved","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":true},"new_external_parent_id":{"type":"string"},"path":{"type":"string"},"status":{"type":"string","example":"moved"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/upload-url":{"post":{"tags":["GCS"],"summary":"Create a signed upload URL","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/SupabaseAuthBody"},{"type":"object","properties":{"fileName":{"type":"string"},"path":{"type":"string"},"contentType":{"type":"string","example":"application/pdf"},"expiresInSeconds":{"type":"number","example":600}}}]}}}},"responses":{"200":{"description":"Signed URL","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"url":{"type":"string"},"fileId":{"type":"string"},"expiresInSeconds":{"type":"number"}}}}}},"400":{"description":"fileName or path is required","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","example":"fileName or path is required"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/split":{"post":{"tags":["Split"],"summary":"Split/group an uploaded PDF","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["fileId"],"properties":{"fileId":{"type":"string","description":"GCS object path"}}}}}},"responses":{"200":{"description":"Split grouping result","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"Missing fileId","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"examples":{"missingFile":{"value":{"error":"fileId is required"}}}}}},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/mistral/health":{"get":{"tags":["Mistral OCR"],"summary":"Test Mistral API connection","security":[{"bearerAuth":[]}],"description":"Lightweight connectivity check for Mistral. This does not OCR a document; it calls the Mistral models API with a timeout and reports latency, HTTP status, and whether mistral-ocr-latest is listed.","parameters":[{"name":"timeoutMs","in":"query","required":false,"schema":{"type":"integer","minimum":1000,"maximum":30000,"default":8000},"description":"Request timeout in milliseconds. Values are clamped between 1000 and 30000."}],"responses":{"200":{"description":"Mistral is reachable","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":true},"data":{"type":"object","properties":{"ok":{"type":"boolean","example":true},"status":{"type":"integer","example":200},"durationMs":{"type":"integer","example":420},"timeoutMs":{"type":"integer","example":8000},"model":{"type":"string","example":"mistral-ocr-latest"},"modelAvailable":{"type":"boolean","example":true},"modelCount":{"type":"integer","example":25},"error":{"type":"string","nullable":true,"example":null}}}}}}}},"400":{"description":"Configuration error, usually missing Mistral API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"examples":{"missingKey":{"value":{"success":false,"error":"mistral_api_key_missing"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"503":{"description":"Mistral is not reachable, timed out, or returned a non-2xx status","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"data":{"type":"object","properties":{"ok":{"type":"boolean","example":false},"status":{"type":"integer","nullable":true,"example":429},"durationMs":{"type":"integer","example":8003},"timeoutMs":{"type":"integer","example":8000},"model":{"type":"string","example":"mistral-ocr-latest"},"modelAvailable":{"type":"boolean","example":false},"modelCount":{"type":"integer","example":0},"error":{"type":"string","example":"mistral_connection_timeout"}}}}},"examples":{"timeout":{"value":{"success":false,"data":{"ok":false,"status":null,"durationMs":8003,"timeoutMs":8000,"model":"mistral-ocr-latest","modelAvailable":false,"modelCount":0,"error":"mistral_connection_timeout"}}}}}}}}}},"/api/mistral/ocr":{"post":{"tags":["Mistral OCR"],"summary":"Run Mistral OCR on a document URL or base64 document","security":[{"bearerAuth":[]}],"description":"Requires Supabase access token in the Authorization bearer header. Body access_token is still accepted as a temporary fallback. Provide one of documentUrl, dataUrl, or fileBase64.","requestBody":{"required":true,"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/SupabaseAuthBody"},{"type":"object","properties":{"documentUrl":{"type":"string","description":"Public URL or signed URL to a PDF/image document"},"documentDriveId":{"type":"string","description":"Google Drive file id. The API converts it to https://drive.google.com/uc?export=download&id=FILE_ID."},"dataUrl":{"type":"string","description":"Full data URL, for example data:application/pdf;base64,..."},"fileBase64":{"type":"string","description":"Raw base64 document content"},"mimeType":{"type":"string","example":"application/pdf"},"documentName":{"type":"string"},"model":{"type":"string","default":"mistral-ocr-latest"},"includeImageBase64":{"type":"boolean","default":false,"description":"Include extracted image base64 payloads in the optional pages response."},"includePages":{"type":"boolean","default":false,"description":"Include raw OCR pages. Pages contain markdown from Mistral, so keep this false when you only want JSON extraction."},"pages":{"oneOf":[{"type":"string","example":"0-2"},{"type":"array","items":{"type":"number"},"example":[0,1,2]}]},"documentAnnotationPrompt":{"type":"string"},"documentAnnotationSchema":{"allOf":[{"$ref":"#/components/schemas/MistralOcrJsonSchema"}],"description":"Optional custom JSON schema for document_annotation_format. A default metadata schema is used when omitted."}}}]},"examples":{"url":{"value":{"access_token":"SUPABASE_ACCESS_TOKEN","documentUrl":"https://example.com/document.pdf","includePages":false}},"driveId":{"value":{"access_token":"SUPABASE_ACCESS_TOKEN","documentDriveId":"1abcDEFghiJKLmnoPQRstuVWxyz","includePages":false}},"base64":{"value":{"access_token":"SUPABASE_ACCESS_TOKEN","fileBase64":"JVBERi0xLjQK...","mimeType":"application/pdf"}}}}}},"responses":{"200":{"description":"Formatted Mistral OCR response","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"data":{"$ref":"#/components/schemas/MistralOcrResponse"}}}}}},"400":{"description":"OCR request failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"examples":{"missingDocument":{"value":{"success":false,"error":"Provide documentUrl, documentDriveId, dataUrl, or fileBase64."}},"mistralCapacity":{"value":{"success":false,"error":"API error occurred: Status 429 Body: Service tier capacity exceeded for this model."}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/mistral/ocr/drive-base64":{"post":{"tags":["Mistral OCR"],"summary":"Test Mistral OCR with Google Drive download converted to base64","security":[{"bearerAuth":[]}],"description":"Debug/test endpoint. Downloads the Drive file with the connected user Drive token, converts it to base64 inside the backend, and sends a data URL to Mistral. Use this to compare against Google Drive URL mode and avoid Drive public URL/redirect issues.","requestBody":{"required":true,"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/SupabaseAuthBody"},{"type":"object","required":["documentDriveId"],"properties":{"documentDriveId":{"type":"string","description":"Google Drive file id to download through the backend."},"maxSizeMb":{"type":"number","default":10,"description":"Safety limit for this test endpoint. Files larger than this are rejected before base64 conversion."},"mimeType":{"type":"string","example":"application/pdf"},"documentName":{"type":"string"},"model":{"type":"string","default":"mistral-ocr-latest"},"includeImageBase64":{"type":"boolean","default":false},"includePages":{"type":"boolean","default":false},"pages":{"oneOf":[{"type":"string","example":"0-2"},{"type":"array","items":{"type":"number"},"example":[0,1,2]}]},"documentAnnotationPrompt":{"type":"string"},"documentAnnotationSchema":{"allOf":[{"$ref":"#/components/schemas/MistralOcrJsonSchema"}]}}}]},"examples":{"driveBase64":{"value":{"access_token":"SUPABASE_ACCESS_TOKEN","documentDriveId":"1abcDEFghiJKLmnoPQRstuVWxyz","maxSizeMb":10,"includePages":false}}}}}},"responses":{"200":{"description":"Mistral OCR response using backend base64 upload","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":true},"data":{"type":"object","properties":{"source":{"type":"object","properties":{"mode":{"type":"string","example":"drive_base64"},"externalFileId":{"type":"string"},"name":{"type":"string","nullable":true},"mimeType":{"type":"string","nullable":true},"sizeBytes":{"type":"number"},"base64Bytes":{"type":"number"}}},"ocr":{"$ref":"#/components/schemas/MistralOcrResponse"}}}}}}}},"400":{"description":"Drive download or Mistral OCR failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"examples":{"missingDriveId":{"value":{"success":false,"error":"mistral_ocr_drive_id_required"}},"tooLarge":{"value":{"success":false,"error":"mistral_ocr_drive_file_too_large_for_base64_test"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/internal/email-dispatch":{"post":{"tags":["Email"],"deprecated":true,"summary":"Run legacy email dispatcher","description":"Deprecated legacy dispatcher. Use /api/internal/email/v2/dispatch for scheduled email rules; v2 uses normalized folder-path matching and supports nested folder rule paths.","requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"email_log_id":{"type":"string"}}}}}},"responses":{"200":{"description":"Dispatcher executed","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","example":"dispatcher_executed"},"deprecated":{"type":"boolean","example":true},"successor":{"type":"string","example":"/api/internal/email/v2/dispatch"}}}}}},"400":{"description":"Email log not found or invalid dispatch request","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","example":"EMAIL_LOG_NOT_FOUND"}}}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/internal/email":{"post":{"tags":["Email"],"summary":"Process one email worker task","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["rule_id","org_id","recipient_email","files"],"properties":{"rule_id":{"type":"string"},"org_id":{"type":"string"},"recipient_email":{"type":"string"},"email_log_id":{"type":"string"},"files":{"type":"array","items":{"type":"string"}}}}}}},"responses":{"200":{"description":"Worker processed","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","example":true},"filesCount":{"type":"number"}}}}}},"500":{"description":"Worker failed","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/drive/process":{"post":{"tags":["Legacy"],"summary":"Legacy Drive processing endpoint","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["fileId"],"properties":{"fileId":{"type":"string"},"accessToken":{"type":"string"},"mimeType":{"type":"string"},"prompt":{"type":"string"},"modelName":{"type":"string"}}}}}},"responses":{"200":{"description":"Legacy processing result","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":true},"aiResponse":{}}}}}},"400":{"description":"Missing fileId","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","example":"fileId is a required field."}}}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/drive/sample":{"get":{"tags":["Legacy"],"summary":"Legacy sample Drive processing endpoint","responses":{"200":{"description":"Sample processing result","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":true},"aiResponse":{"type":"string"},"durationMs":{"type":"number"},"attempts":{"type":"number"},"location":{"type":"string"}}}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/users/profile":{"post":{"tags":["Legacy"],"summary":"Debug endpoint for Supabase user/profile/drive rows","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["accessToken"],"properties":{"accessToken":{"type":"string"}}}}}},"responses":{"200":{"description":"Profile data","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":true},"user":{"type":"object"},"profile":{"type":"object"},"drive":{"type":"object"}}}}}},"400":{"description":"Missing accessToken","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","example":"accessToken and refreshToken is required"}}}}}},"500":{"$ref":"#/components/responses/ServerError"}}}}},"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT","description":"Supabase access token"}},"schemas":{"ApiError":{"type":"object","properties":{"success":{"type":"boolean","example":false},"error":{"type":"string","description":"Stable backend error code/message."}}},"SupabaseAuthBody":{"type":"object","properties":{"access_token":{"type":"string","description":"Deprecated fallback. Prefer the Authorization: Bearer token header."}}},"AiClassificationJson":{"type":"object","required":["file_id","classification","parties","document","money","evidence","warnings"],"properties":{"provider":{"type":"string","enum":["mistral","vertex"]},"model":{"type":"string","nullable":true},"file_id":{"type":"string"},"classification":{"type":"object","properties":{"folder_id":{"type":"string","example":"expenses"},"classification_path":{"type":"string","example":"02_Ausgaben"},"doc_type":{"type":"string","example":"receipt"},"reason":{"type":"string","example":"Restaurant receipt paid by the organization."},"confidence":{"type":"string","enum":["low","medium","high"]}}},"parties":{"type":"object","properties":{"issuer_name":{"type":"string","example":"Restaurant GmbH"},"recipient_name":{"type":"string","example":"PP Essen GmbH"},"entity_type":{"type":"string","enum":["EMPLOYEE","VENDOR","GOVERNMENT","NONE"]},"entity_name":{"type":"string","example":"Restaurant GmbH"},"entity_short_name":{"type":"string","example":"Restaurant"}}},"document":{"type":"object","properties":{"doc_id":{"type":"string","example":"201686"},"date":{"type":"string","example":"2026-01-01"},"language":{"type":"string","example":"de"}}},"money":{"type":"object","properties":{"direction":{"type":"string","enum":["ORG_RECEIVES_MONEY","ORG_PAYS_MONEY","NO_MONEY_DIRECTION","UNKNOWN"]},"currency":{"type":"string","nullable":true,"example":"EUR"},"total_gross":{"type":"number","nullable":true,"example":35.4},"total_net":{"type":"number","nullable":true},"tax_amount":{"type":"number","nullable":true},"tax_rate":{"type":"number","nullable":true},"payment_method":{"type":"string","nullable":true,"example":"card"}}},"evidence":{"type":"array","items":{"type":"string"}},"warnings":{"type":"array","items":{"type":"string"}},"backend":{"type":"object","description":"Backend-only operational fields used for Drive rename/move.","properties":{"filename_base":{"type":"string"},"classification_path":{"type":"string"},"final_filename":{"type":"string"},"final_path":{"type":"string"},"final_folder_id":{"type":"string"}}}}},"ClassifyWorkflowResponse":{"type":"object","properties":{"finalResult":{"type":"object","description":"Legacy internal result used during filename/path building. The saved files.classification JSON uses AiClassificationJson."},"fileId":{"type":"string"},"tree":{"type":"array","items":{"type":"object"}},"orgId":{"type":"string","format":"uuid"},"rootFolderId":{"type":"string"},"savedClassification":{"$ref":"#/components/schemas/AiClassificationJson"}}},"EmailRetryResponse":{"type":"object","properties":{"success":{"type":"boolean","example":true},"original_email_log_id":{"type":"string","format":"uuid"},"retry_count":{"type":"number","example":1},"batches":{"type":"array","items":{"type":"object","properties":{"email_log_id":{"type":"string","format":"uuid"},"resend_email_id":{"type":"string"},"rule_id":{"type":"string","format":"uuid"},"recipient_email":{"type":"string","format":"email"},"file_count":{"type":"number"},"status":{"type":"string","enum":["sent","failed"]},"error":{"type":"string"}}}}}},"EmailTestFailedLogResponse":{"type":"object","properties":{"success":{"type":"boolean","example":true},"email_log_id":{"type":"string","format":"uuid"},"file_ids":{"type":"array","items":{"type":"string"}},"recipient_email":{"type":"string","format":"email"},"status":{"type":"string","example":"failed"}}},"DriveFileInput":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"mimeType":{"type":"string","description":"Supported: application/pdf, image/*, text/plain, text/csv, application/csv, application/vnd.ms-excel (.xls), application/vnd.openxmlformats-officedocument.spreadsheetml.sheet (.xlsx). Excel files are converted to CSV before AI classification.","example":"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"},"size":{"type":"number"},"classificationPath":{"type":"string","nullable":true,"description":"Optional per-file classification path. When provided, this overrides the request-level classificationPath for this file only."},"entityName":{"type":"string","nullable":true,"description":"Optional per-file entity name. When provided, this overrides the request-level entityName for this file only."}}},"ClassificationQuota":{"type":"object","properties":{"allowed":{"type":"boolean"},"reason":{"type":"string","nullable":true,"example":"classification_limit_reached"},"limit":{"type":"number","nullable":true,"description":"Monthly classification limit. Null means unlimited."},"used":{"type":"number","description":"Classification requests already reserved for this month."},"remaining":{"type":"number","nullable":true,"description":"Remaining classification requests this month."},"requested":{"type":"number","description":"Number of new files checked or reserved by this request."},"requestedInScan":{"type":"number","description":"Total files requested in this scan request for the per-scan plan limit."},"maxFilesPerScan":{"type":"number","description":"Maximum files allowed in one scan request by the current plan.","example":20},"maxFileSizeMb":{"type":"number","description":"Maximum size allowed for one file by the current plan.","example":10},"maxFileSizeBytes":{"type":"number","description":"Maximum size allowed for one file in bytes.","example":10485760},"periodStart":{"type":"string","format":"date-time","nullable":true,"description":"Start of the usage period used for quota."},"periodEnd":{"type":"string","format":"date-time","nullable":true,"description":"End of the usage period used for quota."},"periodKey":{"type":"string","description":"Readable usage period key. Free plans use the organization timezone calendar month.","example":"2026-05"},"month":{"type":"string","deprecated":true,"description":"Deprecated alias of periodKey.","example":"2026-05"}}},"TeamMemberLimit":{"type":"object","properties":{"allowed":{"type":"boolean"},"reason":{"type":"string","nullable":true,"example":"team_member_limit_reached"},"limit":{"type":"number","nullable":true,"description":"Maximum active/pending team members allowed by the plan. Null means unlimited."},"used":{"type":"number","description":"Current active and pending team members in the organization."},"remaining":{"type":"number","nullable":true,"description":"Remaining team member slots."},"requested":{"type":"number","description":"Number of new members being checked."}}},"EmailRuleLimit":{"type":"object","properties":{"allowed":{"type":"boolean"},"reason":{"type":"string","nullable":true,"example":"email_rule_limit_reached"},"limit":{"type":"number","nullable":true,"description":"Maximum enabled email rules allowed by the plan. Null means unlimited."},"used":{"type":"number","description":"Current enabled email rules in the organization."},"remaining":{"type":"number","nullable":true,"description":"Remaining email rule slots."},"requested":{"type":"number","description":"Number of new email rules being checked."}}},"FolderTreeTemplatePayload":{"type":"object","required":["name","base_structure"],"properties":{"name":{"type":"string","example":"gastro"},"description":{"type":"string","nullable":true},"industry":{"type":"string","nullable":true},"localized":{"type":"object","description":"Optional template-level translations keyed by language code, for example en or de.","additionalProperties":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"},"industry":{"type":"string"}}}},"classifier_schema_template_id":{"type":"string","nullable":true},"prompt":{"type":"string"},"schema":{"type":"object"},"base_structure":{"type":"object","example":{"id":"gastro","name":"AI Sortify","path":"AI Sortify","behavior":"container","children":[{"id":"fallback","name":"00_Fallback","path":"00_Fallback","behavior":"fixed","children":[]}]}}}},"FolderTreeTemplatePreviewRequest":{"type":"object","properties":{"prompt":{"type":"string","description":"Preferred single free-text prompt describing the business and desired document organization. Alias for businessContext.","example":"German gastronomy company. Separate Einnahmen, Ausgaben, payroll, bank, taxes, contracts, and system folders."},"businessContext":{"type":"string","description":"Legacy alias for prompt. At least one of prompt or businessContext is required.","example":"German gastronomy company. Separate Einnahmen, Ausgaben, payroll, bank, taxes, contracts, and system folders."},"structureName":{"type":"string","example":"PP ESSEN GmbH"},"name":{"type":"string","example":"PP ESSEN GmbH"},"industry":{"type":"string","example":"gastronomy"},"country":{"type":"string","example":"DE"},"language":{"type":"string","example":"de"},"localizationLanguages":{"type":"array","items":{"type":"string"},"description":"Optional admin-only localization languages. When present, backend adds template.localized and node.localized entries for the requested language codes. Aliases accepted by backend: localizedLanguages, languages, locales, localization.languages.","example":["en","de"]},"classifierProvider":{"type":"string","enum":["vertex","mistral"],"default":"vertex"},"existingFolders":{"type":"array","items":{"type":"string"},"description":"Optional folder name hints. Backend generates ids, paths, filename patterns, system folders, and fallback.","example":["01_Einnahmen","02_Ausgaben"]},"folderHints":{"type":"array","items":{"type":"string"},"description":"Optional business folder, behavior, and keyword hints. Do not send ids or paths.","example":["Use German accounting folder names, German keyword synonyms, and year_month behavior for recurring accounting folders."]}}},"FolderTreeTemplatePreviewResponse":{"type":"object","properties":{"success":{"type":"boolean","example":true},"template":{"$ref":"#/components/schemas/FolderTreeTemplatePayload"},"base_structure":{"type":"object"},"classifier_schema_template":{"type":"object","nullable":true,"properties":{"id":{"type":"string"},"name":{"type":"string"},"provider":{"type":"string"},"version":{"type":"number"}}},"meta":{"type":"object","properties":{"model":{"type":"string","example":"gemini-3.1-flash-lite"},"location":{"type":"string","example":"eu"},"durationMs":{"type":"number"},"saved":{"type":"boolean","example":false}}}}},"MistralOcrJsonSchema":{"type":"object","required":["name","schemaDefinition"],"properties":{"name":{"type":"string","example":"document_metadata"},"description":{"type":"string"},"strict":{"type":"boolean","default":true},"schemaDefinition":{"type":"object","example":{"type":"object","additionalProperties":false,"required":["company_name","document_id","document_name"],"properties":{"company_name":{"type":"string","description":"Company, merchant, sender, or issuer name found in the document."},"document_id":{"type":"string","description":"Primary document identifier, invoice number, reminder number, customer number, or reference."},"document_name":{"type":"string","description":"Short human-readable document title."}}}}}},"MistralOcrResponse":{"type":"object","properties":{"annotation":{"oneOf":[{"type":"object"},{"type":"string"}],"nullable":true},"meta":{"type":"object","properties":{"model":{"type":"string"},"pageCount":{"type":"number"},"usage":{"type":"object","properties":{"pagesProcessed":{"type":"number"},"docSizeBytes":{"type":"number","nullable":true}}}}},"pages":{"description":"Only returned when includePages is true. Mistral OCR page content is markdown.","type":"array","items":{"type":"object","properties":{"index":{"type":"number"},"markdown":{"type":"string"},"header":{"type":"string","nullable":true},"footer":{"type":"string","nullable":true},"dimensions":{"type":"object","nullable":true},"images":{"type":"array","items":{"type":"object"}},"tables":{"type":"array","items":{"type":"object"}},"hyperlinks":{"type":"array","items":{"type":"string"}},"confidenceScores":{"type":"object","nullable":true}}}}}}},"responses":{"Unauthorized":{"description":"Missing or invalid Supabase access token","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","example":"edge_unauthorized"}}}}}},"BadRequest":{"description":"Invalid request payload or missing required field","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"NotFound":{"description":"Requested resource was not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"ServerError":{"description":"Unexpected server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}}