{"openapi":"3.1.0","info":{"title":"AICharli customer API","version":"1.1.1","summary":"HTTP contract for the same backend the live dashboard already calls","description":"HTTP routes for a custom frontend. They run the same server code as the live dashboard, which calls these functions directly:\n\n| Dashboard | HTTP |\n| --- | --- |\n| `login()` | `POST /api/v1/auth/login` |\n| `logout()` | `POST /api/v1/auth/logout` |\n| `createPodRequest()` | `POST /api/v1/assistants/claim` |\n| `getUserActiveAssignments()` | `GET /api/v1/assistants` |\n| `upgradeToPrivate()` | `POST /api/v1/assistants/upgrade` |\n| `extendSession()` | `POST /api/v1/assistants/extend` |\n| `releaseMyPod()` | `POST /api/v1/assistants/release` |\n\nSession cookie auth. Mutating `/api/v1` routes, `/api/register` and login also need CSRF: `GET /api/csrf` (it also sets a `csrf_token` cookie, so send cookies back), then the returned token in `x-csrf-token`. The `/api/conversations` routes do not check CSRF; the session cookie is SameSite=strict.\nDo not send GPU names or API keys. `POST /api/v1/assistants/chat` talks to your READY assistant (JSON or SSE); `POST /api/conversations` stores history only and does not call the model."},"servers":[{"url":"https://aicharli.com","description":"Production dashboard"}],"tags":[{"name":"Auth","description":"CSRF, register, session"},{"name":"Assistants","description":"Shared vs Private claim, session, launch"},{"name":"History","description":"Conversation history stored on the account. Works without an active assistant, does not need CSRF, and is readable after a session ends. File upload is not available to customers yet."},{"name":"Billing","description":"Credit top-ups through Stripe Checkout and payment history"},{"name":"Health","description":"Liveness and readiness"},{"name":"Inference","description":"Managed OpenAI-compatible proxy on api.aicharli.com"}],"paths":{"/api/csrf":{"get":{"tags":["Auth"],"operationId":"getCsrf","summary":"Issue a CSRF token","responses":{"200":{"description":"Token plus the header name to send on mutating requests","content":{"application/json":{"schema":{"type":"object","required":["token","headerName"],"properties":{"token":{"type":"string"},"headerName":{"type":"string","example":"x-csrf-token"}}}}}}}}},"/api/register":{"post":{"tags":["Auth"],"operationId":"register","summary":"Create an account","security":[{"csrf":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RegisterRequest"}}}},"responses":{"201":{"description":"Account created and active; log in straight away.","content":{"application/json":{"schema":{"type":"object","required":["success","message"],"properties":{"success":{"type":"boolean"},"message":{"type":"string"}}}}}},"400":{"$ref":"#/components/responses/Error"},"403":{"$ref":"#/components/responses/Error"},"409":{"$ref":"#/components/responses/Error"},"429":{"$ref":"#/components/responses/Error"},"503":{"$ref":"#/components/responses/Error"}}}},"/api/v1/auth/login":{"post":{"tags":["Auth"],"operationId":"login","summary":"Sign in and set the session cookie","description":"Same code as the dashboard's login() function. Checks the username or email and password and sets the session cookie.","security":[{"csrf":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginRequest"}}}},"responses":{"200":{"description":"Session cookie set","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginResponse"}}}},"400":{"$ref":"#/components/responses/Error"},"401":{"$ref":"#/components/responses/Error"},"403":{"$ref":"#/components/responses/Error"}}}},"/api/v1/auth/password-reset/request":{"post":{"tags":["Auth"],"operationId":"requestPasswordReset","summary":"Ask for a password reset email","description":"Send email or username. The answer is always the same 200, whether or not an account matches, and mail is only sent when the server has SMTP configured. The link in the mail is valid for 1 hour. Limited to 10 requests per hour per IP and 3 per hour per identifier.","security":[{"csrf":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PasswordResetRequest"}}}},"responses":{"200":{"description":"Request accepted","content":{"application/json":{"schema":{"type":"object","required":["success","message"],"properties":{"success":{"type":"boolean"},"message":{"type":"string"}}}}}},"400":{"$ref":"#/components/responses/Error"},"403":{"$ref":"#/components/responses/Error"},"429":{"$ref":"#/components/responses/Error"}}}},"/api/v1/auth/password-reset/confirm":{"post":{"tags":["Auth"],"operationId":"confirmPasswordReset","summary":"Set a new password with a reset token","description":"Send the token from the reset link and the new password. A successful reset signs out every older session.","security":[{"csrf":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PasswordResetConfirm"}}}},"responses":{"200":{"description":"Password changed","content":{"application/json":{"schema":{"type":"object","required":["success","message"],"properties":{"success":{"type":"boolean"},"message":{"type":"string"}}}}}},"400":{"$ref":"#/components/responses/Error"},"403":{"$ref":"#/components/responses/Error"},"500":{"$ref":"#/components/responses/Error"}}}},"/api/v1/auth/verify-email":{"post":{"tags":["Auth"],"operationId":"verifyEmail","summary":"Verify the email address with a token","description":"Send the token from the verification link. A token works once and is valid for 24 hours.","security":[{"csrf":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VerifyEmailRequest"}}}},"responses":{"200":{"description":"Email verified","content":{"application/json":{"schema":{"type":"object","required":["success","message"],"properties":{"success":{"type":"boolean"},"message":{"type":"string"}}}}}},"400":{"$ref":"#/components/responses/Error"},"403":{"$ref":"#/components/responses/Error"},"500":{"$ref":"#/components/responses/Error"}}}},"/api/v1/auth/verify-email/resend":{"post":{"tags":["Auth"],"operationId":"resendEmailVerification","summary":"Send a new verification email","description":"Sends a new verification link to the signed-in user when their email still needs verification. Limited to 3 per hour per user.","security":[{"session":[],"csrf":[]}],"responses":{"200":{"description":"Request accepted","content":{"application/json":{"schema":{"type":"object","required":["success","message"],"properties":{"success":{"type":"boolean"},"message":{"type":"string"}}}}}},"401":{"$ref":"#/components/responses/Error"},"403":{"$ref":"#/components/responses/Error"},"429":{"$ref":"#/components/responses/Error"}}}},"/api/v1/auth/logout":{"post":{"tags":["Auth"],"operationId":"logout","summary":"Clear the session cookie","description":"Clears the session cookie.","security":[{"session":[],"csrf":[]}],"responses":{"200":{"description":"Signed out","content":{"application/json":{"schema":{"type":"object","required":["success"],"properties":{"success":{"type":"boolean"}}}}}}}}},"/api/v1/me":{"get":{"tags":["Auth"],"operationId":"getMe","summary":"Current account","security":[{"session":[]}],"responses":{"200":{"description":"Authenticated user","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Me"}}}},"401":{"$ref":"#/components/responses/Error"}}}},"/api/v1/billing/checkout":{"post":{"tags":["Billing"],"operationId":"createCheckout","summary":"Start a credit top-up","description":"Creates a Stripe Checkout session for one credit pack and returns its url; send the customer there to pay. Credits are added once Stripe confirms the payment, never from the redirect back. Answers 503 with code PAYMENTS_DISABLED until payments are enabled, and 403 for admin accounts and for unverified email addresses when verification is required.","security":[{"session":[],"csrf":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CheckoutRequest"}}}},"responses":{"200":{"description":"Checkout session created","content":{"application/json":{"schema":{"type":"object","required":["url"],"properties":{"url":{"type":"string"}}}}}},"400":{"$ref":"#/components/responses/Error"},"401":{"$ref":"#/components/responses/Error"},"403":{"$ref":"#/components/responses/Error"},"429":{"$ref":"#/components/responses/Error"},"500":{"$ref":"#/components/responses/Error"},"502":{"$ref":"#/components/responses/Error"},"503":{"$ref":"#/components/responses/Error"}}}},"/api/v1/billing/payments":{"get":{"tags":["Billing"],"operationId":"listPayments","summary":"Top-up packs and payment history","description":"Returns whether payments are enabled, the pack amounts you can buy, the currency, and your 50 most recent payments, newest first.","security":[{"session":[]}],"responses":{"200":{"description":"Billing overview","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BillingOverview"}}}},"401":{"$ref":"#/components/responses/Error"}}}},"/api/v1/assistants":{"get":{"tags":["Assistants"],"operationId":"getAssistants","summary":"Current assistant, plans, and credits","description":"Returns the plans you can claim and your current assistant, if any. Poll until current.status is READY, then open current.launchPath. READY always includes a non-null expiresAt; while the pod is healthy but the billing clock has not started yet, status stays STARTING and expiresAt stays null. While STARTING, current.provisioning reports the pod's own status, when it was claimed, and warmupDeadlineAt, the point by which the claim is fully refunded and the pod released if it still has not come up. Do not call release or upgrade while current.status is STARTING before provisioning.warmupDeadlineAt. FAILED is terminal: the claim could not be set up and error says why. The charge is refunded automatically; a refund that fails is retried every monitoring cycle until it succeeds. It stays visible for about 10 minutes, then current becomes null.","security":[{"session":[]}],"responses":{"200":{"description":"Customer assistant snapshot","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssistantsSnapshot"}}}},"401":{"$ref":"#/components/responses/Error"},"403":{"$ref":"#/components/responses/Error"}}}},"/api/v1/assistants/claim":{"post":{"tags":["Assistants"],"operationId":"claimAssistant","summary":"Claim Shared or Private","description":"The server picks a free assistant of the requested tier. Send only tier and durationHours. The request returns at once with status STARTING once the charge and assignment commit; the workspace is prepared in the background. Poll GET /api/v1/assistants until current.status is READY (or FAILED, whose charge is refunded automatically). Do not retry blindly: retrying a claim that already succeeded returns 409 with a clear already-held message and never charges twice.","security":[{"session":[],"csrf":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ClaimRequest"}}}},"responses":{"200":{"description":"Claim accepted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ClaimResponse"}}}},"400":{"$ref":"#/components/responses/Error"},"401":{"$ref":"#/components/responses/Error"},"403":{"$ref":"#/components/responses/Error"},"409":{"$ref":"#/components/responses/Error"},"503":{"$ref":"#/components/responses/Error"}}}},"/api/v1/assistants/release":{"post":{"tags":["Assistants"],"operationId":"releaseAssistant","summary":"End the current session and refund unused time","description":"Ends the caller's current session and refunds unused time. The server finds the session, so the client sends no id.","security":[{"session":[],"csrf":[]}],"responses":{"200":{"description":"Released","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReleaseResponse"}}}},"401":{"$ref":"#/components/responses/Error"},"403":{"$ref":"#/components/responses/Error"},"409":{"description":"Release conflict. code CLAIM_IN_PROGRESS with Retry-After means the caller's own claim is still being set up; retry after that many seconds. Any other body is a plain release failure.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/assistants/extend":{"post":{"tags":["Assistants"],"operationId":"extendAssistant","summary":"Add hours to the current session","description":"Adds hours to the caller's current session and charges credits for them.","security":[{"session":[],"csrf":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExtendRequest"}}}},"responses":{"200":{"description":"Extended","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExtendResponse"}}}},"400":{"$ref":"#/components/responses/Error"},"401":{"$ref":"#/components/responses/Error"},"409":{"$ref":"#/components/responses/Error"}}}},"/api/v1/assistants/upgrade":{"post":{"tags":["Assistants"],"operationId":"upgradeAssistant","summary":"Move Shared to Private on the same account","description":"Memory stays on the account. Unused shared time is refunded first. The request can take up to ~3 minutes while the private pod is prepared: use a client timeout of at least 180 s. If your client times out anyway, the upgrade still completes; do not retry blindly, instead GET /api/v1/assistants to read the resulting assignment.","security":[{"session":[],"csrf":[]}],"responses":{"200":{"description":"Upgraded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ClaimResponse"}}}},"401":{"$ref":"#/components/responses/Error"},"409":{"description":"Upgrade conflict. code CLAIM_IN_PROGRESS with Retry-After means the caller's own claim on the current pod is still being set up; retry after that many seconds. Any other body is a plain upgrade failure.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"$ref":"#/components/responses/Error"}}}},"/api/v1/assistants/chat":{"post":{"tags":["Assistants"],"operationId":"assistantChat","summary":"Chat with your assistant","description":"Sends messages to your own READY assistant and returns the answer. Needs the session cookie and the x-csrf-token header; the server holds the inference key, so none is ever sent to or returned to the browser. Set stream to true for token-by-token OpenAI-style server-sent events (data: lines ending with data: [DONE]); otherwise one JSON chat completion is returned. Any model, tools or other field you send is ignored: the model is fixed to your assistant's. The inference service raises max_tokens below 4096 to 4096. Errors before the answer starts carry error and code; 409 means no assistant or one that is not READY yet (poll GET /api/v1/assistants), 429 includes Retry-After, 503 is retryable. If a stream breaks after it started, a final data event with an error object and data: [DONE] close it. Closing the connection cancels the request. Nothing is stored; use POST /api/conversations/{id}/messages for history.","security":[{"session":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssistantChatRequest"}}}},"responses":{"200":{"description":"Chat completion, or an event stream when stream is true","content":{"application/json":{"schema":{"type":"object","description":"OpenAI chat completion object"}},"text/event-stream":{"schema":{"type":"string","description":"OpenAI chat completion chunks as data: lines, ending with data: [DONE]"}}}},"400":{"$ref":"#/components/responses/Error"},"401":{"$ref":"#/components/responses/Error"},"402":{"$ref":"#/components/responses/Error"},"403":{"$ref":"#/components/responses/Error"},"409":{"$ref":"#/components/responses/Error"},"429":{"$ref":"#/components/responses/Error"},"502":{"$ref":"#/components/responses/Error"},"503":{"$ref":"#/components/responses/Error"},"504":{"$ref":"#/components/responses/Error"}}}},"/api/agent-zero/launch":{"get":{"tags":["Assistants"],"operationId":"launchAssistant","summary":"Open the managed assistant (browser redirect)","description":"Same URL the live dashboard Open assistant button uses. Requires a session cookie. Returns 302 to the auto-login entrypoint when READY, or an HTML wait page while starting. Use from a top-level navigation or window.open, not as JSON.","security":[{"session":[]}],"responses":{"302":{"description":"Redirect to the assistant"},"403":{"description":"No active assignment"},"503":{"description":"Still starting"}}}},"/api/conversations":{"get":{"tags":["History"],"operationId":"listConversations","summary":"List account conversations","description":"Ordered by updatedAt, newest first, so the conversation with the latest message comes first. updatedAt moves when a message is added (conversations that have had no message since they were created keep their creation time). Returns at most limit (up to 100) conversations, with no paging past that. Metadata only; read a conversation's messages with GET /api/conversations/{id}/messages. Delete one with DELETE /api/conversations/{id}. Admin accounts get 403.","security":[{"session":[]}],"parameters":[{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":30}}],"responses":{"200":{"description":"Conversation list","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConversationList"}}}},"401":{"$ref":"#/components/responses/Error"},"403":{"$ref":"#/components/responses/Error"},"500":{"$ref":"#/components/responses/Error"}}},"post":{"tags":["History"],"operationId":"createConversation","summary":"Create a conversation record","description":"Does not start a model reply. A JSON body is required; send {} to create a conversation without a title, which is stored as null. There is no rename yet.","security":[{"session":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"title":{"type":"string","maxLength":120}}}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Conversation"}}}},"400":{"$ref":"#/components/responses/Error"},"401":{"$ref":"#/components/responses/Error"},"403":{"$ref":"#/components/responses/Error"},"500":{"$ref":"#/components/responses/Error"}}}},"/api/conversations/{id}/messages":{"get":{"tags":["History"],"operationId":"listMessages","summary":"List messages in a conversation","description":"Newest first: the first message in the list is the latest. Returns the newest limit messages (up to 200); pass order=asc to list them oldest first instead. hasMore is true when older messages exist, and nextBefore is then the id to pass as before to load the next older page (it is null on the last page). before returns the limit messages just before that message, in the same order setting. An unknown before id, or an order other than asc or desc, gets 400. Each message lists its attached files and generated artifacts.","security":[{"session":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":200,"default":50}},{"name":"before","in":"query","description":"Id of a message in this conversation. Returns the messages just before it. Use nextBefore from the previous response.","schema":{"type":"string"}},{"name":"order","in":"query","description":"Order of messages inside the page: desc lists the newest first (default), asc the oldest first.","schema":{"type":"string","enum":["desc","asc"],"default":"desc"}}],"responses":{"200":{"description":"Messages","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MessageList"}}}},"400":{"$ref":"#/components/responses/Error"},"401":{"$ref":"#/components/responses/Error"},"403":{"$ref":"#/components/responses/Error"},"404":{"$ref":"#/components/responses/Error"}}},"post":{"tags":["History"],"operationId":"appendMessage","summary":"Store a message","description":"Persists history. It does not call the GPU model. content is trimmed before it is stored and must not be empty after trimming. Leave podId and uploadedFileIds out: the customer API exposes neither pod ids nor file upload yet; both are only used by the built-in dashboard. 403 covers admin accounts, a pod that is not assigned to the caller, and unavailable files.","security":[{"session":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AppendMessageRequest"}}}},"responses":{"201":{"description":"Stored","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StoredMessage"}}}},"400":{"$ref":"#/components/responses/Error"},"401":{"$ref":"#/components/responses/Error"},"403":{"$ref":"#/components/responses/Error"},"404":{"$ref":"#/components/responses/Error"}}}},"/api/conversations/{id}":{"delete":{"tags":["History"],"operationId":"deleteConversation","summary":"Delete a conversation","description":"Deletes one of your conversations together with its messages. Files you attached stay available but are no longer linked to it. 404 covers an unknown id and a conversation that belongs to someone else. Admin accounts get 403.","security":[{"session":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeleteConversationResponse"}}}},"401":{"$ref":"#/components/responses/Error"},"403":{"$ref":"#/components/responses/Error"},"404":{"$ref":"#/components/responses/Error"},"500":{"$ref":"#/components/responses/Error"}}}},"/api/proxy/health":{"get":{"tags":["Health"],"operationId":"proxyHealth","summary":"Auth-proxy health (dashboard poll)","description":"Reports whether the inference proxy is reachable.","responses":{"200":{"description":"Proxy status JSON"}}}},"/api/pods/{id}/resume":{"post":{"tags":["Assistants"],"operationId":"resumeAssistantByPodId","summary":"Resume a stopped assistant runtime","description":"Used by the built-in dashboard. {id} is the internal pod id, which the customer API does not return, so a custom frontend should not call this; poll GET /api/v1/assistants after claim instead.","security":[{"session":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Internal pod id. The live dashboard still sends this."}],"responses":{"200":{"description":"Resumed"},"202":{"description":"Resume in progress"},"401":{"$ref":"#/components/responses/Error"},"403":{"$ref":"#/components/responses/Error"}}}},"/api/user":{"get":{"tags":["Auth"],"operationId":"getUser","summary":"Current account (legacy path)","description":"Existing dashboard JSON route. Prefer GET /api/v1/me, which adds credits.","security":[{"session":[]}],"responses":{"200":{"description":"User"},"401":{"$ref":"#/components/responses/Error"}}}},"/api/health/live":{"get":{"tags":["Health"],"operationId":"live","summary":"Process liveness","responses":{"200":{"description":"Alive"}}}},"/api/health/ready":{"get":{"tags":["Health"],"operationId":"ready","summary":"Accepts traffic when the database is up","responses":{"200":{"description":"Ready"},"503":{"description":"Not ready"}}}},"/api/openapi.json":{"get":{"tags":["Health"],"operationId":"openapi","summary":"This OpenAPI document","responses":{"200":{"description":"OpenAPI 3.1 JSON"}}}},"/v1/chat/completions":{"servers":[{"url":"https://api.aicharli.com"}],"post":{"tags":["Inference"],"operationId":"chatCompletions","summary":"OpenAI-compatible chat completions","description":"Managed assistant inference. Authorization is a Bearer key minted for the claimed assistant, not a customer-supplied key. Prefer launchPath for the product chat UI.","security":[{"inferenceBearer":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["messages"],"properties":{"model":{"type":"string"},"messages":{"type":"array","items":{"type":"object"}},"stream":{"type":"boolean","default":false}}}}}},"responses":{"200":{"description":"Completion or SSE stream"},"401":{"$ref":"#/components/responses/Error"}}}},"/v1/models":{"servers":[{"url":"https://api.aicharli.com"}],"get":{"tags":["Inference"],"operationId":"listModels","summary":"List managed models","security":[{"inferenceBearer":[]}],"responses":{"200":{"description":"Model list"}}}},"/v1/memory/search":{"servers":[{"url":"https://api.aicharli.com"}],"post":{"tags":["Inference"],"operationId":"searchMemory","summary":"Search the account's conversation memory","security":[{"inferenceBearer":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["query"],"properties":{"query":{"type":"string"},"limit":{"type":"integer","minimum":1,"maximum":50,"default":5}}}}}},"responses":{"200":{"description":"Memory hits"}}}}},"components":{"securitySchemes":{"session":{"type":"apiKey","in":"cookie","name":"session","description":"HTTP-only JWT session cookie from login"},"csrf":{"type":"apiKey","in":"header","name":"x-csrf-token","description":"Token from GET /api/csrf"},"inferenceBearer":{"type":"http","scheme":"bearer","description":"Managed key for api.aicharli.com. Not customer-supplied."}},"schemas":{"Error":{"type":"object","description":"error is always present. code and retryable appear only on errors that define them (for example GPU_ADMISSION_CLOSED and CLAIM_IN_PROGRESS); a plain 401 is {\"error\":\"Unauthorized\"}.","required":["error"],"properties":{"error":{"type":"string"},"code":{"type":"string"},"retryable":{"type":"boolean"}}},"LoginRequest":{"type":"object","required":["username","password"],"properties":{"username":{"type":"string","description":"Username or email"},"password":{"type":"string"}}},"LoginResponse":{"type":"object","required":["success","role"],"properties":{"success":{"type":"boolean"},"role":{"type":"string","enum":["USER","ADMIN"]}}},"RegisterRequest":{"type":"object","required":["username","email","password"],"properties":{"username":{"type":"string"},"email":{"type":"string","format":"email"},"password":{"type":"string","minLength":8},"acceptTerms":{"type":"boolean","description":"Acceptance of the Terms of Service and Privacy Policy. Web sign-ups require it and always send true; when true the acceptance time is stored. Optional for API clients."}}},"PasswordResetRequest":{"type":"object","properties":{"email":{"type":"string"},"username":{"type":"string"}}},"PasswordResetConfirm":{"type":"object","required":["token","password"],"properties":{"token":{"type":"string"},"password":{"type":"string","minLength":8}}},"VerifyEmailRequest":{"type":"object","required":["token"],"properties":{"token":{"type":"string"}}},"CheckoutRequest":{"type":"object","required":["amount"],"properties":{"amount":{"type":"integer","description":"Pack size in currency units; one of BillingOverview.amounts"}}},"Payment":{"type":"object","required":["id","kind","status","amount","currency","credits","createdAt","paidAt"],"properties":{"id":{"type":"string"},"kind":{"type":"string","enum":["TOPUP","SUBSCRIPTION"]},"status":{"type":"string","enum":["PENDING","PAID","FAILED","EXPIRED","REFUNDED"]},"amount":{"type":"number","description":"Price in currency units"},"currency":{"type":"string"},"credits":{"type":"number"},"createdAt":{"type":"string","format":"date-time"},"paidAt":{"type":["string","null"],"format":"date-time"}}},"BillingOverview":{"type":"object","required":["enabled","amounts","currency","payments"],"properties":{"enabled":{"type":"boolean"},"amounts":{"type":"array","items":{"type":"integer"}},"currency":{"type":"string"},"payments":{"type":"array","items":{"$ref":"#/components/schemas/Payment"}}}},"Me":{"type":"object","required":["id","username","role","credits","emailVerified"],"properties":{"id":{"type":"string"},"username":{"type":"string"},"email":{"type":["string","null"]},"role":{"type":"string","enum":["USER","ADMIN"]},"credits":{"type":"number"},"emailVerified":{"type":"boolean"}}},"Plan":{"type":"object","required":["tier","available","creditRate"],"properties":{"tier":{"type":"string","enum":["PRIVATE","SHARED"]},"available":{"type":"boolean"},"creditRate":{"type":"number"}}},"Assistant":{"type":"object","required":["assignmentId","tier","status","creditRate","expiresAt","durationHours","launchPath","provisioning"],"properties":{"assignmentId":{"type":"string"},"tier":{"type":"string","enum":["PRIVATE","SHARED"]},"status":{"type":"string","enum":["READY","STARTING","FAILED"]},"creditRate":{"type":"number"},"expiresAt":{"type":["string","null"],"format":"date-time","description":"Session end time stamped by the billing worker. Always non-null when status is READY; null while STARTING."},"durationHours":{"type":["integer","null"]},"launchPath":{"type":"string","example":"/api/agent-zero/launch"},"error":{"type":"string","description":"Present only when status is FAILED"},"provisioning":{"type":["object","null"],"description":"Set while status is STARTING and the billing clock has not started, including a RUNNING pod still waiting on its first billing tick; null once expiresAt is set.","required":["podStatus","claimedAt","warmupDeadlineAt"],"properties":{"podStatus":{"type":"string","enum":["IDLE","ERROR","PENDING","HIBERNATED","RUNNING"]},"claimedAt":{"type":"string","format":"date-time"},"warmupDeadlineAt":{"type":"string","format":"date-time"}}}}},"AssistantsSnapshot":{"type":"object","required":["credits","plans","current"],"properties":{"credits":{"type":"number"},"plans":{"type":"array","items":{"$ref":"#/components/schemas/Plan"}},"current":{"oneOf":[{"$ref":"#/components/schemas/Assistant"},{"type":"null"}]}}},"ClaimRequest":{"type":"object","required":["tier","durationHours"],"properties":{"tier":{"type":"string","enum":["PRIVATE","SHARED"]},"durationHours":{"type":"integer","minimum":1,"maximum":168}}},"ClaimResponse":{"type":"object","required":["success","assistant"],"properties":{"success":{"type":"boolean"},"assistant":{"$ref":"#/components/schemas/Assistant"},"remainingCredits":{"type":"number"},"message":{"type":"string"}}},"ExtendRequest":{"type":"object","required":["durationHours"],"properties":{"durationHours":{"type":"integer","minimum":1,"maximum":168}}},"ExtendResponse":{"type":"object","required":["success","remainingCredits","expiresAt","durationHours"],"properties":{"success":{"type":"boolean"},"remainingCredits":{"type":"number"},"expiresAt":{"type":["string","null"],"format":"date-time"},"durationHours":{"type":"integer"}}},"ReleaseResponse":{"type":"object","required":["success","refunded"],"properties":{"success":{"type":"boolean"},"refunded":{"type":"number"}}},"AppendMessageRequest":{"type":"object","required":["role","content"],"properties":{"role":{"type":"string","enum":["SYSTEM","USER","ASSISTANT","TOOL"]},"content":{"type":"string","minLength":1,"maxLength":32000},"podId":{"type":"string","description":"Internal; customers leave it out."},"uploadedFileIds":{"type":"array","items":{"type":"string"},"description":"Internal; customers leave it out."}}},"Conversation":{"type":"object","required":["id","title","createdAt","updatedAt"],"properties":{"id":{"type":"string"},"title":{"type":["string","null"]},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}},"ConversationSummary":{"type":"object","required":["id","title","createdAt","updatedAt","messageCount"],"properties":{"id":{"type":"string"},"title":{"type":["string","null"]},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"messageCount":{"type":"integer"}}},"AssistantChatRequest":{"type":"object","required":["messages"],"properties":{"messages":{"type":"array","minItems":1,"maxItems":200,"items":{"type":"object","required":["role","content"],"properties":{"role":{"type":"string","enum":["system","user","assistant"]},"content":{"type":"string","maxLength":32000}}}},"stream":{"type":"boolean","description":"Default false. true returns server-sent events."},"max_tokens":{"type":"integer","minimum":1,"maximum":32768,"description":"The inference service raises values below 4096 to 4096."},"temperature":{"type":"number","minimum":0,"maximum":2}}},"DeleteConversationResponse":{"type":"object","required":["success"],"properties":{"success":{"type":"boolean"}}},"ConversationList":{"type":"object","required":["conversations"],"properties":{"conversations":{"type":"array","items":{"$ref":"#/components/schemas/ConversationSummary"}}}},"MessageFile":{"type":"object","required":["id","originalName","mimeType","sizeBytes","status"],"properties":{"id":{"type":"string"},"originalName":{"type":"string"},"mimeType":{"type":"string"},"sizeBytes":{"type":"integer"},"status":{"type":"string","enum":["PENDING","AVAILABLE","FAILED","DELETED"]}}},"MessageArtifact":{"type":"object","required":["id","type","mimeType","sizeBytes","createdAt"],"properties":{"id":{"type":"string"},"type":{"type":"string","enum":["TEXT","JSON","IMAGE","FILE"]},"mimeType":{"type":"string"},"sizeBytes":{"type":"integer"},"createdAt":{"type":"string","format":"date-time"}}},"StoredMessage":{"type":"object","required":["id","role","content","podId","createdAt"],"properties":{"id":{"type":"string"},"role":{"type":"string","enum":["SYSTEM","USER","ASSISTANT","TOOL"]},"content":{"type":"string"},"podId":{"type":["string","null"]},"createdAt":{"type":"string","format":"date-time"}}},"Message":{"type":"object","required":["id","role","content","podId","createdAt","uploadedFiles","artifacts"],"properties":{"id":{"type":"string"},"role":{"type":"string","enum":["SYSTEM","USER","ASSISTANT","TOOL"]},"content":{"type":"string"},"podId":{"type":["string","null"]},"createdAt":{"type":"string","format":"date-time"},"uploadedFiles":{"type":"array","items":{"$ref":"#/components/schemas/MessageFile"}},"artifacts":{"type":"array","items":{"$ref":"#/components/schemas/MessageArtifact"}}}},"MessageList":{"type":"object","required":["messages","hasMore","nextBefore"],"properties":{"messages":{"type":"array","items":{"$ref":"#/components/schemas/Message"}},"hasMore":{"type":"boolean","description":"True when older messages exist beyond this page."},"nextBefore":{"type":["string","null"],"description":"Id to pass as before to load the next older page; null when hasMore is false."}}}},"responses":{"Error":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}