{"openapi":"3.1.0","info":{"title":"Synero API","version":"1.1.0","description":"Ask up to four leading AI models (GPT, Claude, Gemini, Grok) the same question and receive one consensus answer plus an agreement analysis (where the models agree and split), as server-sent events or JSON.","contact":{"url":"https://synero.ai/developers"}},"servers":[{"url":"https://synero.ai","description":"Production"}],"security":[{"bearerAuth":[]}],"paths":{"/api/query":{"post":{"operationId":"queryCouncil","summary":"Query the AI Council","description":"Send a prompt to the models you pick (models or preset; one per provider). Each answers independently, then a consensus answer and an agreement analysis are produced. Streamed as server-sent events (SSE) by default; format: json returns one JSON object. API-key responses carry X-RateLimit-* headers.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/QueryRequest"},"example":{"prompt":"What are the trade-offs between microservices and monoliths?"}}}},"responses":{"200":{"description":"SSE stream of council responses, or a JSON object when format is json","headers":{"X-RateLimit-Limit":{"schema":{"type":"integer","example":60}},"X-RateLimit-Remaining":{"schema":{"type":"integer"}},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Epoch seconds when the window resets"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/QueryJsonResponse"}},"text/event-stream":{"schema":{"type":"string","description":"Server-sent events stream. Each event has a type (event:) and JSON data (data:). Event types: advisor, advisor-complete, advisor-error, synthesis-start, synthesis, synthesis-complete, consensus, complete, error. A synthesis that fails or produces no text is reported as synthesis-complete with an error field (discard any synthesis tokens already received), not as a fatal error event; complete still follows. consensus usually arrives during synthesis but may arrive after complete, so read until the stream closes."}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"examples":{"missingPrompt":{"value":{"error":"Prompt is required","code":"missing_prompt","doc_url":"https://synero.ai/developers#missing_prompt"}},"tooLong":{"value":{"error":"Prompt too long (max 10,000 characters)","code":"prompt_too_long","doc_url":"https://synero.ai/developers#prompt_too_long"}},"invalidModels":{"value":{"error":"Unknown model \"foo\". See the model list in the API docs.","code":"invalid_models","doc_url":"https://synero.ai/developers#invalid_models"}},"invalidJson":{"value":{"error":"Request body must be valid JSON","code":"invalid_json","doc_url":"https://synero.ai/developers#invalid_json"}},"invalidSynthesisInstructions":{"value":{"error":"synthesisInstructions must be a string of at most 10,000 characters","code":"invalid_synthesis_instructions","doc_url":"https://synero.ai/developers#invalid_synthesis_instructions"}}}}}},"401":{"description":"Invalid API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"Invalid API key. Get one at https://synero.ai/app/api-keys","code":"invalid_api_key","doc_url":"https://synero.ai/developers#invalid_api_key"}}}},"402":{"description":"Payment required: the free calls are used and the balance is empty. Add credit (from $5, no subscription) or subscribe at https://synero.ai/app/billing","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BillingError"},"example":{"error":"Insufficient credits. Add credit at https://synero.ai/app/billing.","code":"no_credits","doc_url":"https://synero.ai/developers#no_credits"}}}},"404":{"description":"parent_query_id is not one of your queries","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"parent_query_id not found","code":"parent_not_found","doc_url":"https://synero.ai/developers#parent_not_found"}}}},"429":{"description":"Rate limit exceeded (60 requests/min per API key, rate_limit_exceeded), or questions already running have reserved the rest of the balance (runs_in_progress; no Retry-After, retry when they finish)","headers":{"Retry-After":{"schema":{"type":"integer"},"description":"Seconds until rate limit resets"},"X-RateLimit-Limit":{"schema":{"type":"integer","example":60}},"X-RateLimit-Remaining":{"schema":{"type":"integer","example":0}},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Epoch seconds when the window resets"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"Rate limit exceeded. Try again later.","code":"rate_limit_exceeded","doc_url":"https://synero.ai/developers#rate_limit_exceeded"}}}},"500":{"description":"Unexpected server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"Internal server error","code":"internal_error","doc_url":"https://synero.ai/developers#internal_error"}}}},"502":{"description":"Every model failed to answer (billed for tokens already used)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"All advisors failed to respond.","code":"all_advisors_failed","doc_url":"https://synero.ai/developers#all_advisors_failed"}}}},"503":{"description":"Billing service temporarily unavailable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"Billing service temporarily unavailable. Please try again.","code":"billing_unavailable","doc_url":"https://synero.ai/developers#billing_unavailable"}}}}}}},"/api/keys":{"post":{"operationId":"createApiKey","summary":"Create an API key","description":"Create a new API key. Requires browser session authentication (cannot create keys with an API key). The raw key is returned once and never stored, save it securely.","security":[{"cookieAuth":[]}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","description":"Display name for this key (max 100 characters)","default":"Default"}}}}}},"responses":{"201":{"description":"API key created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiKeyCreateResponse"}}}},"400":{"description":"Malformed JSON (invalid_json), a name over 100 characters (name_too_long), or 25 active keys already (key_limit_reached)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Not authenticated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"More than 10 key creations in a minute (rate_limit_exceeded)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"get":{"operationId":"listApiKeys","summary":"List API keys","description":"List all active (non-revoked) API keys for the authenticated user. Requires browser session.","security":[{"cookieAuth":[]}],"responses":{"200":{"description":"List of API keys","content":{"application/json":{"schema":{"type":"object","properties":{"keys":{"type":"array","items":{"$ref":"#/components/schemas/ApiKey"}}}}}}}}}},"/api/keys/{id}":{"delete":{"operationId":"revokeApiKey","summary":"Revoke an API key","description":"Permanently revoke an API key. The key will immediately stop working.","security":[{"cookieAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Key revoked","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"404":{"description":"No active key with this id (unknown, not yours, or already revoked)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The revoke could not be saved (internal_error). The key may still be active; retry.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}}},"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"API key authentication. Keys start with \"sk_live_\" and are 48 characters long. Get yours at https://synero.ai/app/api-keys"},"cookieAuth":{"type":"apiKey","in":"cookie","name":"sb-<project-ref>-auth-token","description":"Supabase SSR session cookie set by signing in at synero.ai (may be split into .0/.1 parts); dashboard operations only"}},"schemas":{"QueryRequest":{"type":"object","required":["prompt"],"properties":{"prompt":{"type":"string","maxLength":10000,"description":"The question or prompt to send to the AI council"},"models":{"type":"array","minItems":1,"maxItems":4,"items":{"$ref":"#/components/schemas/ModelId"},"description":"1 to 4 model ids, at most one per provider. Each provider rides a fixed slot (openai architect, anthropic philosopher, google explorer, xai maverick) with a neutral system prompt, as in the web chat. Replaces config; with template, only the template's synthesis instructions apply."},"preset":{"type":"string","enum":["solo","duel","council","deep","auto"],"description":"solo = 1 model, duel = 2, council = up to 4 balanced for speed, deep = up to 4 flagship models. Without models, the preset's default models run; with models, the count must fit the preset (auto with models: the models run as given). With neither models nor preset, the legacy auto-sized council runs. auto lets Synero pick the preset and models for the question (a fast classifier, usually under a second): solo for simple questions, duel when some judgment is involved, council when experts would disagree, deep when the stakes are high; Grok joins questions about what people are posting on X. The pick is reported as autoRouted, preset and routeReason in council-meta (and the JSON response); if the classifier is unavailable, council runs and preset is absent (also absent on free trial queries, which run the trial council)."},"web_search":{"type":"boolean","description":"Let each model search the web before answering. Omit it and search turns on automatically for time-sensitive questions (news, prices, \"latest\", this or last year, a URL), reported as webSearchAuto; true forces it on, false forces it off. Slower and adds search fees."},"config":{"$ref":"#/components/schemas/CouncilConfig"},"thread_id":{"type":"string","format":"uuid","description":"Thread ID that groups queries in the history (threadId is accepted too). Follow-ups inherit the parent's thread (a thread_id sent with parent_query_id is ignored), or start one keyed by the parent's queryId when it has none. Sent without parent_query_id, the query is filed as the newest turn of that thread (one chat in history); context still comes only from parent_query_id or threadHistory. A malformed id returns 400 invalid_thread_id."},"parent_query_id":{"type":"string","format":"uuid","description":"The queryId of one of your earlier queries (parentQueryId is accepted too). When threadHistory is not sent, the server rebuilds the conversation from the parent chain, up to 10 turns. Another user's or unknown id returns 404 parent_not_found; a malformed id returns 400 invalid_parent_query_id."},"threadHistory":{"type":"array","items":{"$ref":"#/components/schemas/ThreadEntry"},"description":"Previous Q&A pairs for conversation context. Only the last 10 entries are used (the last 2 on free-trial queries), each prompt and synthesis truncated to 8,000 characters; malformed entries are dropped."},"context":{"type":"array","maxItems":20,"description":"Attach documents or text as context for the council. Supports .md, .txt, .csv, .json content. Max 50k chars per item, 100k total, at most 20 items.","items":{"$ref":"#/components/schemas/ContextItem"}},"format":{"type":"string","enum":["json","stream"],"description":"Response format. 'json' returns structured JSON (ideal for agents). Default 'stream' returns SSE. In JSON, synthesis is null when synthesis was skipped or failed; a failed synthesis also sets synthesisError to the reason. JSON also carries consensus, the agreement analysis (see the consensus SSE event). JSON waits for it (about 4s, up to 20s; also graded when synthesis is false) and returns null when grading fails. A JSON run still going after about 60s answers 200 and sends newlines (valid leading JSON whitespace) until the object; a failure after that arrives in the body as { error, code, doc_url }, so check for code. Prefer SSE for deep or web_search runs."},"synthesis":{"type":"boolean","default":true,"description":"Set false to skip the server-side synthesizer and receive only the advisor responses (synthesis is null in the JSON response; the SSE stream emits no synthesis-start or synthesis token events, just synthesis-complete { skipped: true } then complete). For agent callers that reconcile the advisors themselves: cuts latency roughly in half and saves the synthesis token cost. Default true."},"private":{"type":"boolean","default":false,"description":"When true, the query is not saved to history (queryId is null in the JSON response). It is billed like any other query."},"synthesisInstructions":{"type":"string","maxLength":10000,"description":"Custom instructions for the synthesis step. A non-string or longer value returns 400 invalid_synthesis_instructions."}}},"ContextItem":{"type":"object","required":["name","content"],"properties":{"name":{"type":"string","maxLength":300,"description":"Label for this context (e.g. filename)","example":"research-notes.md"},"content":{"type":"string","description":"The actual text, inlined. The API cannot read file paths or URLs, so paste the file's contents here. For type \"image\", a base64 data URL (png, jpeg, webp, gif; max 3 images per query, 5MB each). Images are sent to every advisor as multimodal input."},"type":{"type":"string","enum":["file","text","image"],"description":"How the context was attached","default":"text"}}},"CouncilConfig":{"type":"object","description":"Legacy persona-council configuration (which model powers each advisor slot and the synthesizer). Ignored when models or preset is sent.","properties":{"advisorModels":{"type":"object","description":"Map of advisor slot to model ID","properties":{"architect":{"$ref":"#/components/schemas/ModelId"},"philosopher":{"$ref":"#/components/schemas/ModelId"},"explorer":{"$ref":"#/components/schemas/ModelId"},"maverick":{"$ref":"#/components/schemas/ModelId"}}},"synthesizerModel":{"$ref":"#/components/schemas/ModelId"},"enabledAdvisors":{"type":"object","description":"Map of advisor slot to enabled state. Omit or set to true to enable. Set to false to disable. At least 1 advisor must be enabled. When only 1 advisor responds, synthesis is automatically skipped.","properties":{"architect":{"type":"boolean","default":true},"philosopher":{"type":"boolean","default":true},"explorer":{"type":"boolean","default":true},"maverick":{"type":"boolean","default":true}}},"councilTier":{"type":"string","enum":["low","medium","high","custom","auto","instant"],"description":"Council tier this config maps to. low=fast & cheap, medium=balanced, high=flagship models from each provider (API-key callers run it as medium, reported as cappedFrom; use preset deep for flagship models), custom=hand-picked models, auto=the server routes each question to instant/low/medium/high before running (the resolved tier is returned in the council-meta SSE event or the JSON response; auto is the default; explicit advisorModels or synthesizerModel run a custom council instead, skipping auto and instant), instant=no council: a single fast model answers directly with no synthesis (cheapest and fastest, for trivial questions). When sent on /api/query, the resolved tier is recorded for analytics. Programmatic clients can use it as a per-query override without affecting the user's persisted preference."}}},"ModelId":{"type":"string","enum":["gpt-6.1-sol","gpt-6-luna","gpt-6-astra","gpt-5.6-sol","gpt-5.6-terra","gpt-5.6-luna","claude-opus-5-5","claude-sonnet-5-5","claude-fable-5-1","claude-opus-5","claude-sonnet-5","claude-haiku-5-5","gemini-3.8-flash","gemini-3.5-flash","gemini-3.5-flash-lite","gemini-3.1-pro-preview","grok-4.7","grok-4.6","grok-4.5","grok-4.3"],"description":"Available AI model identifiers"},"AdvisorSlotId":{"type":"string","enum":["architect","philosopher","explorer","maverick"],"description":"The four advisor personas. Architect: structured/analytical. Philosopher: nuanced/thorough. Explorer: creative/curious. Maverick: bold/unconventional."},"ThreadEntry":{"type":"object","required":["prompt","synthesis"],"properties":{"prompt":{"type":"string"},"synthesis":{"type":"string"}}},"SSEEvents":{"type":"object","description":"Server-sent event types streamed in the response. Note: synthesis-start and synthesis token events are only emitted when 2+ advisors are enabled AND synthesis was not disabled. When synthesis is skipped (single advisor, or synthesis: false in the request), the stream emits synthesis-complete { skipped: true } after the advisors finish, then complete. When synthesis fails or produces no text (refusal, provider error, or deadline cutoff before any text), synthesis-complete carries error instead (discard any synthesis tokens already received) and complete still follows. consensus usually arrives during synthesis but may arrive after complete, so read until the stream closes.","properties":{"council-meta":{"type":"object","description":"First event on every stream: the council the server resolved for this query. enabledAdvisors marks which slots will actually stream (instant runs a single advisor). autoRouted/routeReason are present when councilTier was 'auto' or preset was 'auto' (preset then names the pick).","properties":{"queryId":{"type":"string","format":"uuid","description":"The saved query's id, known before any token (absent in private mode or when saving failed)"},"tier":{"type":"string","enum":["low","medium","high","custom","instant"]},"advisorModels":{"type":"object","additionalProperties":{"type":"string"}},"enabledAdvisors":{"type":"object","additionalProperties":{"type":"boolean"}},"synthesizerModel":{"type":"string"},"autoRouted":{"type":"boolean"},"preset":{"type":"string","enum":["solo","duel","council","deep"],"description":"The preset Synero picked when preset was 'auto'"},"routeReason":{"type":"string"},"cappedFrom":{"type":"string","enum":["high"],"description":"An explicit high tier ran as medium (API callers are capped at medium)"},"webSearch":{"type":"boolean","description":"Web search is on for this query"},"webSearchAuto":{"type":"boolean","description":"Web search was turned on automatically because web_search was omitted and the question looked time-sensitive"}}},"advisor":{"type":"object","description":"Token from an advisor (streamed incrementally)","properties":{"id":{"$ref":"#/components/schemas/AdvisorSlotId"},"token":{"type":"string"},"status":{"type":"string","enum":["streaming"]}}},"advisor-complete":{"type":"object","description":"An advisor has finished responding","properties":{"id":{"$ref":"#/components/schemas/AdvisorSlotId"},"modelId":{"type":"string"},"inputTokens":{"type":"integer"},"outputTokens":{"type":"integer"},"latencyMs":{"type":"integer"},"truncated":{"type":"boolean","description":"The answer hit its output limit and may be cut off"},"searched":{"type":"boolean","description":"The model ran web searches (web_search)"},"sources":{"type":"array","items":{},"description":"Sources the model used (web_search)"}}},"advisor-error":{"type":"object","description":"An advisor failed or returned no text: a provider error (rate limit, authentication, outage), a refusal (safety filter), an exhausted output budget, or an empty answer. The other advisors continue.","properties":{"id":{"$ref":"#/components/schemas/AdvisorSlotId"},"error":{"type":"string"}}},"synthesis-start":{"type":"object","description":"The synthesizer is starting. Sent once, with an empty payload, only when synthesis runs.","properties":{}},"synthesis":{"type":"object","description":"Token from the synthesizer (streamed after all advisors complete)","properties":{"token":{"type":"string"},"status":{"type":"string","enum":["streaming"]}}},"synthesis-complete":{"type":"object","description":"Synthesis has finished. skipped: true means it never ran (single advisor, or synthesis: false in the request) and token counts are 0. error is present when synthesis failed or produced no text; discard any synthesis tokens already received. This is not fatal: complete still follows and the advisor responses stand.","properties":{"inputTokens":{"type":"integer"},"outputTokens":{"type":"integer"},"skipped":{"type":"boolean"},"reason":{"type":"string","enum":["requested_off","single_model","other_models_failed"],"description":"Set with skipped: the caller sent synthesis: false, only one model was asked, or only one answered because the others failed."},"error":{"type":"string","description":"Why synthesis failed, e.g. \"The synthesizer declined to answer this prompt (safety filter). The advisor responses are still available.\" or \"Synthesis failed. The advisor responses are still available.\""}}},"consensus":{"type":"object","description":"Agreement across the advisors, sent when 2+ advisors answered, also when synthesis is skipped. Usually arrives during the synthesis but may arrive after complete, so read until the stream closes. Not sent if grading fails.","properties":{"score":{"type":"integer","minimum":0,"maximum":100,"description":"100 = the advisors fully agree on substance"},"unanimous":{"type":"array","items":{"type":"string"},"description":"Points every advisor shares (max 4)"},"contested":{"type":"array","description":"Genuine splits (max 3)","items":{"type":"object","properties":{"topic":{"type":"string"},"positions":{"type":"array","items":{"type":"object","properties":{"slot":{"$ref":"#/components/schemas/AdvisorSlotId"},"stance":{"type":"string"}}}}}}},"dissenter":{"type":["string","null"],"description":"The slot that most departed from the others"},"resolution":{"type":"string","description":"Older runs only: which side of the splits was better supported. New analyses omit it; the synthesis is the verdict"},"takeaway":{"type":"string","description":"Two-sentence bottom line"}}},"complete":{"type":"object","description":"The answer is done. A late consensus event may still follow, so read until the stream closes.","properties":{"queryId":{"type":["string","null"],"format":"uuid","description":"Null when the query was not saved (private)"},"trialQueriesRemaining":{"type":"integer","description":"Free-trial queries left; sent only on free-trial runs"}}},"error":{"type":"object","description":"A fatal error occurred during processing, such as every advisor failing. Synthesis failures are reported on synthesis-complete instead. Tokens the advisors already used are still billed.","properties":{"message":{"type":"string"},"trialQueriesRemaining":{"type":"integer","description":"Free-trial queries left; sent only on free-trial runs"}}}}},"ApiKey":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"keyPrefix":{"type":"string","description":"First 12 characters of the key (e.g. \"sk_live_a1b2\")"},"lastUsedAt":{"type":["string","null"],"format":"date-time"},"createdAt":{"type":"string","format":"date-time"}}},"ApiKeyCreateResponse":{"type":"object","properties":{"key":{"$ref":"#/components/schemas/ApiKey"},"rawKey":{"type":"string","description":"The full API key. This is shown once and never stored, save it immediately."}}},"ErrorResponse":{"type":"object","required":["error","code"],"properties":{"error":{"type":"string"},"code":{"type":"string","description":"Stable machine-readable code"},"doc_url":{"type":"string","format":"uri"}}},"BillingError":{"type":"object","properties":{"error":{"type":"string"},"code":{"type":"string","enum":["subscription_inactive","no_credits","trial_exhausted"]},"doc_url":{"type":"string","format":"uri"}}},"QueryJsonResponse":{"type":"object","description":"Returned when format is json.","properties":{"queryId":{"type":["string","null"],"format":"uuid","description":"Null for private queries"},"councilTier":{"type":"string"},"cappedFrom":{"type":"string","enum":["high"]},"webSearch":{"type":"boolean","description":"Web search ran for this query"},"webSearchAuto":{"type":"boolean","description":"Web search was turned on automatically (web_search omitted, question looked time-sensitive)"},"threadId":{"type":"string","format":"uuid"},"autoRouted":{"type":"boolean"},"preset":{"type":"string","enum":["solo","duel","council","deep"],"description":"The preset Synero picked when preset was 'auto'"},"routeReason":{"type":"string"},"advisors":{"type":"object","description":"Answers keyed by slot (read model to tell them apart)","additionalProperties":{"type":"object","properties":{"response":{"type":"string"},"model":{"type":"string"},"latencyMs":{"type":"integer"},"inputTokens":{"type":"integer"},"outputTokens":{"type":"integer"}}}},"synthesis":{"type":["string","null"],"description":"The consensus answer; null when skipped or failed"},"synthesisError":{"type":"string"},"synthesisSkipReason":{"type":"string","enum":["requested_off","single_model","other_models_failed"],"description":"Why synthesis was skipped on purpose; absent when it ran or failed."},"failedAdvisors":{"type":"array","description":"Models that were asked but did not answer. The 502 all_advisors_failed body carries the same list.","items":{"type":"object","properties":{"slotId":{"type":"string"},"modelId":{"type":"string"},"error":{"type":"string"}}}},"synthesisModel":{"type":"string"},"consensus":{"type":["object","null"],"description":"Agreement analysis (see the consensus SSE event); slots name the models"},"tokenUsage":{"type":"object"}}}}}}