{"name":"Wizzo AI Streaming API","version":"1.0.0","base_url":"https://stream.ai.wizzo.media","auth":{"type":"API key","header":"Authorization: <API_KEY>","note":"Send the raw wizzoai API key in the Authorization header, WITHOUT the \"Bearer\" prefix. \"Bearer <jwt>\" is reserved for short-lived public tokens you sign server-side (see public_jwt_mode). PHP CORE sites get the key via market_service::api_key(\"wizzoai\")."},"endpoints":[{"method":"POST","path":"/api/ai/stream","title":"Streaming chat (Server-Sent Events)","description":"Returns the AI answer token-by-token as an SSE stream. Use this when you want to render the answer live as it is generated.","content_type":"text/event-stream","sse_events":["{\"type\":\"chunk\",\"content\":\"...\"}     - one or more, with the next text fragment","{\"type\":\"action\",\"subtype\":\"tool_calls\",\"calls\":[...]} - emitted once if the model called a tool","{\"type\":\"done\",\"input_tokens\":N,\"output_tokens\":N,\"wizzo_tokens\":N} - terminal success event","{\"type\":\"error\",\"message\":\"...\",\"code\":\"...\"} - terminal error event"]},{"method":"POST","path":"/api/ai","title":"Non-streaming chat (legacy PHP-compat)","description":"Returns the full AI answer as a STRING in `data`, matching the legacy PHP m_ai::call() shape. When settings.json=true the string is cleaned (markdown fences/Hebrew quote escapes removed) and is safe to pass through json_decode() on the client. Use /api/ai/json when you want the response already parsed.","response_example":{"data":"The full answer as a string (raw text, or a JSON string when settings.json=true, or {type:\"tool_calls\",calls:[...]} when the model called a tool)"}},{"method":"POST","path":"/api/ai/json","title":"Non-streaming chat - parsed JSON","description":"Like /api/ai but forces settings.json=true and returns the answer ALREADY PARSED as an object/array in `data`. Use this when you want to skip the json_decode() step on the client.","response_example":{"data":{"example_field":"parsed object/array - depends on the model output"}}},{"method":"POST","path":"/api/ai/image","aliases":["POST /api/ai_image"],"title":"Image generation / editing","description":"Generate an image (or edit one when settings.image_input is provided). Returns an array of public URLs hosted on stream.ai.wizzo.media (valid for 24 hours).","response_example":{"data":["https://stream.ai.wizzo.media/uploads/ai_images/openai_173...png"]}},{"method":"POST","path":"/api/embed","title":"Text embedding vector","description":"Returns an OpenAI embedding vector for the given text (defaults to text-embedding-3-small, 1536 dims).","response_example":{"data":[0.0123,-0.0456,"..."]}},{"method":"GET","path":"/api/models","aliases":["GET /api/get_models","POST /api/get_models"],"title":"List available models","description":"Returns the active model catalog (id, name, provider, supports_tools, supports_vision, prices, ...).","response_example":{"data":{"models":["..."]}}},{"method":"GET","path":"/api/realtime/session","title":"OpenAI Realtime ephemeral session","description":"Creates a short-lived OpenAI Realtime session for browser WebRTC voice. Rate-limited to 5/minute per user.","query":{"voice":"shimmer (default) | alloy | echo | ..."}},{"method":"POST","path":"/api/realtime/usage","title":"Report Realtime session usage (billing)","description":"Called by the client when a voice session ends. Body: { duration_ms: number }."},{"method":"POST","path":"/api/transcribe","title":"Speech-to-text transcription","description":"Transcribes an audio file. Send multipart/form-data with a `file` field (up to 64MB; OpenAI models cap at 25MB), or JSON with base64 `audio_input` + `mime`. Optional fields: model (default: the flagged transcription default), language (ISO-639-1 hint, e.g. \"he\"), prompt (vocabulary hint), response_format (json | text | srt | vtt | verbose_json | diarized_json), timestamps (\"segment\" | \"word\" | \"both\", whisper-1 only), diarize (\"1\" for speaker labels via a *-diarize model). Billed per audio minute (price_per_minute) or by tokens for token-billed models. API-key auth only. Rate-limited to 20/minute per user.","response_example":{"success":true,"data":{"text":"...","language":"he","duration":12.4,"segments":null,"model":"gpt-transcribe","provider":"openai","wizzo_tokens":5}}},{"method":"GET","path":"/api/health","title":"Health check","description":"No auth. Returns { status: \"ok\", timestamp }.","no_auth":true}],"common_request_body":{"model":"string (optional) - e.g. \"gpt-5\", \"gemini-2.5-pro\". Defaults to provider default.","provider":"string (optional) - \"openai\" | \"gemini\" | \"anthropic\" | \"xai\". Defaults to \"openai\".","prompt":"string (required) - the user message.","prompt_ref":"string (optional) - sysname of a library prompt to use instead of prompt (rendered with vars).","vars":"object (optional) - values for the {$var} placeholders of prompt_ref.","settings":{"type":"string - log/billing tag (e.g. \"article_generation\").","system_msg":"string - system prompt.","history":"array - previous turns: [{role:\"user\"|\"assistant\"|\"tool\", content|name|tool_call_id|...}].","max_tokens":"number - response length limit (default 15000).","temperature":"number | null - sampling temperature.","top_p":"number | null - nucleus sampling (0-1).","presence_penalty":"number | null - penalize new tokens based on presence in text so far (-2 to 2). OpenAI/xAI only.","frequency_penalty":"number | null - penalize new tokens based on frequency in text so far (-2 to 2). OpenAI/xAI only.","json":"bool - if true, the answer is parsed as JSON before being returned.","json_schema":"string|object - optional JSON schema for structured output.","image_input":"string | string[] - URL / data-URI / base64 image(s) for vision or image editing. URLs are downloaded server-side with anti-bot fallbacks.","tools":"array - tool/function definitions (OpenAI-style) for tool-calling.","web_search":"bool - enable provider web-search tool when supported."}},"error_format":{"http_status":"4xx for client errors (prompt_not_set, monthly_limit, INVALID_TOOL_CALL, ...), 5xx for provider/server errors","body":{"error":"short_code","message":"human readable Hebrew message"}},"getting_started":{"title":"Getting started","steps":["Get your api key from the WizzoAI dashboard (or market_service::api_key(\"wizzoai\") in a PHP CORE site).","Send it as-is in the Authorization header on every request: \"Authorization: <API_KEY>\" (no Bearer prefix).","POST /api/ai with { prompt } for a full answer, /api/ai/json for a parsed JSON answer, /api/ai/stream for SSE.","Every success is { data: ... } plus usage fields; every failure is { error, message } with a 4xx/5xx status."],"examples":{"curl":"curl -X POST https://stream.ai.wizzo.media/api/ai \\\n  -H \"Authorization: YOUR_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"prompt\":\"כתוב כותרת לכתבה על מזג האוויר\",\"model\":\"gpt-5-mini\",\"settings\":{\"type\":\"headline\"}}'","javascript":"const res = await fetch('https://stream.ai.wizzo.media/api/ai/json', {\n  method: 'POST',\n  headers: { 'Authorization': 'YOUR_API_KEY', 'Content-Type': 'application/json' },\n  body: JSON.stringify({\n    prompt_ref: 'headline_v2',          // a prompt from your library\n    vars: { topic: 'מזג האוויר' },      // fills {$topic}\n    settings: { json: true }\n  })\n});\nconst { data, wizzo_tokens } = await res.json();","php":"$ch = curl_init('https://stream.ai.wizzo.media/api/ai');\ncurl_setopt_array($ch, [\n    CURLOPT_POST => true,\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_HTTPHEADER => ['Authorization: YOUR_API_KEY', 'Content-Type: application/json'],\n    CURLOPT_POSTFIELDS => json_encode([\n        'prompt' => 'סכם את הטקסט הבא בשלוש נקודות: ...',\n        'settings' => ['type' => 'summary', 'max_tokens' => 800]\n    ])\n]);\n$res = json_decode(curl_exec($ch), true);\necho $res['data'];","python":"import requests\nr = requests.post('https://stream.ai.wizzo.media/api/ai/json',\n    headers={'Authorization': 'YOUR_API_KEY'},\n    json={'prompt': 'Return {\"ok\":true}', 'settings': {'json': True}})\nprint(r.json()['data'])","streaming":"const res = await fetch('https://stream.ai.wizzo.media/api/ai/stream', {\n  method: 'POST',\n  headers: { 'Authorization': 'YOUR_API_KEY', 'Content-Type': 'application/json' },\n  body: JSON.stringify({ prompt: 'ספר לי בדיחה' })\n});\nconst reader = res.body.getReader(); const dec = new TextDecoder(); let buf = '';\nwhile (true) {\n  const { done, value } = await reader.read(); if (done) break;\n  buf += dec.decode(value, { stream: true });\n  let i; while ((i = buf.indexOf('\\n\\n')) !== -1) {\n    const line = buf.slice(0, i); buf = buf.slice(i + 2);\n    if (line.startsWith('data:')) { const ev = JSON.parse(line.slice(5)); if (ev.type === 'chunk') process.stdout.write(ev.content); }\n  }\n}"}},"concepts":{"wizzo_tokens":"The billing unit. Every call returns wizzo_tokens: the provider price of the call (input, cached input, output) converted at the account token rate. The dashboard shows monthly usage in wizzo tokens.","budget":"Each account has a monthly wizzo-token budget (change_budget). When it is exhausted the API answers 403 { error: \"monthly_limit\" } until the next month or a budget change. Listing/dashboard endpoints keep working.","types":"settings.type is a free tag stored on every log row (e.g. \"headline\", \"summary\"). Use it to break usage down per feature in the dashboard. prompt_ref sets it to \"prompt:<sysname>\", agents to \"agent:<slug>\".","models":"GET /api/models lists the catalog with prices and features. Omit model to get the provider default. The server falls back to another provider automatically on outages unless settings.allowed_providers fences it.","prompts":"A prompt library with {$var} templating and versions. Call any endpoint with prompt_ref + vars instead of prompt.","agents":"Long-running server-side loops (model + builtin tools) with a step budget, a token budget, persisted steps, optional cron schedule and a finish webhook."},"sections":{"prompts":{"title":"Prompts (library with {$var} templating and versions)","description":"sysname ^[a-z0-9_]{2,64}$. A new version is created only when the content (whitespace-normalised) or the model changes.","endpoints":[{"method":"GET","path":"/api/prompts","title":"List prompts","response_example":{"data":{"prompts":[{"id":1,"sysname":"headline_v2","model":"gpt-5-mini","version":3,"comment":"","updated_at":"2026-09-07 10:00:00","preview":"first 120 chars"}]}}},{"method":"GET","path":"/api/prompts/:sysname","title":"Get a prompt by sysname","response_example":{"data":{"id":1,"sysname":"headline_v2","content":"...","model":"gpt-5-mini","version":3,"comment":"","updated_at":"..."}}},{"method":"POST","path":"/api/prompts","title":"Create","body":{"sysname":"headline_v2","content":"כתוב כותרת על {$topic}","model":"gpt-5-mini","comment":"optional"},"response_example":{"data":{"id":1,"version":1}},"errors":["409 exists","400 invalid_sysname"]},{"method":"PUT","path":"/api/prompts/:id","title":"Update (partial)","body":{"content":"?","model":"?","comment":"?","sysname":"?"},"response_example":{"data":{"id":1,"version":4,"changed":true}}},{"method":"DELETE","path":"/api/prompts/:id","title":"Delete (cascades versions)","response_example":{"data":true}},{"method":"GET","path":"/api/prompts/:id/versions","title":"Version history, newest first","response_example":{"data":{"versions":[{"id":9,"version_number":3,"model":"","comment":"","is_current":true,"created_at":"...","content":"..."}]}}},{"method":"POST","path":"/api/prompts/:id/restore","title":"Restore a version (creates a new version with the old content)","body":{"version_number":2},"response_example":{"data":{"version":4}}},{"method":"POST","path":"/api/prompts/:sysname/render","title":"Render with vars","body":{"vars":{"topic":"חורף"}},"response_example":{"data":{"content":"כתוב כותרת על חורף","model":"gpt-5-mini"}}},{"method":"POST","path":"/api/ai | /api/ai/json | /api/ai/stream","title":"Shortcut: prompt_ref + vars","description":"Send prompt_ref: \"<sysname>\" and vars: {} instead of prompt. The stored model is used when the request has none and settings.type defaults to \"prompt:<sysname>\". 404 prompt_not_found.","body":{"prompt_ref":"headline_v2","vars":{"topic":"חורף"}}}]},"agents":{"title":"Agents (definitions, runs, memory, scheduler)","description":"tools: [\"builtin:fetch_url\", \"builtin:web_search\"] (only builtin:<name> references are accepted; anything else -> 400 invalid_tools). trigger_type schedule needs a 5-field crontime (Asia/Jerusalem); invalid -> 400 bad_cron. limits {max_steps 1..120, max_tokens 1000..2000000}. When a run finishes and webhook_url is set, it receives POST {agent, run_id, status, summary, error, tokens}.","endpoints":[{"method":"GET","path":"/api/agents","title":"List with weekly stats","response_example":{"data":{"agents":[{"id":1,"slug":"daily_report","title":"","trigger_type":"schedule","crontime":"0 8 * * *","next_run":"...","last_run":"...","status":"scheduled","runs_week_ok":5,"runs_week_failed":0}]}}},{"method":"GET","path":"/api/agents/:id","title":"Get definition"},{"method":"POST","path":"/api/agents","title":"Create","body":{"title":"דוח יומי","slug":"daily_report","description":"","goal":"אסוף את החדשות של אתמול וסכם","system_prompt":"...","tools":["builtin:fetch_url","builtin:web_search"],"model":"gpt-5","trigger_type":"schedule","crontime":"0 8 * * *","limits":{"max_steps":30,"max_tokens":300000},"webhook_url":"https://example.com/hooks/agent","is_active":1},"response_example":{"data":{"id":1}}},{"method":"PUT","path":"/api/agents/:id","title":"Update (partial)"},{"method":"DELETE","path":"/api/agents/:id","title":"Delete (runs and memory too)"},{"method":"POST","path":"/api/agents/:id/run","title":"Run now","body":{"prompt":"optional extra instruction","dry_run":false},"response_example":{"data":{"run_id":12,"job_id":340,"status":"queued"}}},{"method":"POST","path":"/api/agents/:id/test","title":"Test run (dry_run: tools get __dry_run: true)"},{"method":"GET","path":"/api/agents/:id/runs","title":"Runs, newest first (status synced from the runtime)","response_example":{"data":{"runs":[{"id":12,"job_id":340,"trigger_src":"manual","dry_run":0,"status":"done","summary":"...","error":null,"steps_count":4,"tokens_in":0,"tokens_out":0,"wizzo_tokens":0,"created_at":"...","started_at":"...","finished_at":"..."}]}}},{"method":"GET","path":"/api/agents/runs/:run_id","title":"Run detail with the step timeline","response_example":{"data":{"run":{},"steps":[{"step_num":1,"type":"tool","tool_name":"builtin:fetch_url","tool_args":"{...}","tool_result":"{...}","duration_ms":120}]}}},{"method":"POST","path":"/api/agents/runs/:run_id/cancel","title":"Cancel","response_example":{"data":{"status":"cancelled"}}},{"method":"GET","path":"/api/agents/:id/memory","title":"Memory rows (playbook + insight rows are appended to the system prompt on every run)"},{"method":"POST","path":"/api/agents/:id/memory","body":{"kind":"playbook","mem_key":"style","mem_value":"כתוב קצר"},"title":"Add memory"},{"method":"DELETE","path":"/api/agents/:id/memory/:mid","title":"Delete memory row"}]},"agent_runtime":{"title":"Agent runtime (bring your own control plane)","description":"Lower-level API used by the CMS: you send the whole run definition, including MCP tool servers, and reconcile by polling.","endpoints":[{"method":"POST","path":"/api/agent/run","title":"Enqueue a run","body":{"system_prompt":"...","prompt":"?","model":"?","limits":{"max_steps":30,"max_tokens":300000},"idempotency_key":"?","tool_servers":[{"name":"cms","url":"https://.../mcp","token":"..."}],"allowed_tools":["cms:save_record","builtin:fetch_url"],"report_url":"?","run_secret":"?","dry_run":false,"type":"agent"},"response_example":{"data":{"run_id":340,"status":"queued"}}},{"method":"GET","path":"/api/agent/run/:id","title":"Run status","response_example":{"data":{"run_id":340,"status":"done","steps_count":4,"tokens_in":0,"tokens_out":0,"wizzo_tokens":0,"summary":"...","error":null}}},{"method":"POST","path":"/api/agent/run/:id/cancel","title":"Cancel a run"}]},"media":{"title":"Images, embeddings, speech","endpoints":[{"method":"POST","path":"/api/transcribe","title":"Speech-to-text","description":"multipart/form-data `file` (up to 64MB) or JSON {audio_input (base64/data URL), mime}. Fields: model, language (\"he\"), prompt, response_format (json|text|srt|vtt|verbose_json|diarized_json), timestamps, diarize. API key only. 300 requests/minute per account.","response_example":{"success":true,"data":{"text":"...","language":"he","duration":12.4,"wizzo_tokens":5}}},{"method":"POST","path":"/api/ai/image","title":"Image generation / editing","description":"Returns public URLs under /uploads/ai_images that EXPIRE AFTER 24 HOURS. Copy the file if you need it longer.","response_example":{"data":["https://stream.ai.wizzo.media/uploads/ai_images/....png"]}},{"method":"POST","path":"/api/embed","title":"Embedding vector","body":{"text":"...","type":"article_embedding","model":"text-embedding-3-small"},"response_example":{"data":[0.01,-0.02],"input_tokens":12,"wizzo_tokens":1}},{"method":"GET","path":"/api/realtime/session","title":"OpenAI Realtime ephemeral session (voice)","description":"5 sessions/minute per account. Report usage with POST /api/realtime/usage {duration_ms}; a session is billed up to 45 minutes."}]},"account":{"title":"Account and dashboard (api key only)","endpoints":[{"method":"GET","path":"/api/general","title":"Account info + monthly usage"},{"method":"GET","path":"/api/get_dashboard_data","title":"Stats for a date_range (month_offset_0 ...)"},{"method":"GET","path":"/api/get_usage_logs","title":"Paginated prompt history (page, per_page, search, type, model)"},{"method":"POST","path":"/api/get_log_detail","title":"One log row with its settings","body":{"log_id":1}},{"method":"POST","path":"/api/change_budget","title":"Set the monthly budget","body":{"budget":100000}},{"method":"GET","path":"/api/models","title":"Model catalog"}]}},"public_jwt_mode":{"title":"Public JWT mode (browser-facing calls)","description":"Never ship your api key to a browser. Sign a short-lived HS256 token server-side with the shared WIZZO_PUBLIC_JWT_SECRET and send it as \"Authorization: Bearer <token>\". Claims: site_id (your account id), feature (\"public_chat\"), optional origin (must equal the browser Origin header), optional grants {model, provider, settings:{...}, client_keys:[...]}, exp. JWT calls are clamped: max_tokens <= 2000 unless granted, no system_msg, no image input, no dashboard endpoints, no image generation, no transcription. Errors: 401 token_expired / invalid_token, 403 origin_mismatch."},"rate_limits":{"realtime_session":"5 per minute per account (GET /api/realtime/session), 429 rate_limit","realtime_max_session":"45 minutes billed per session (POST /api/realtime/usage caps duration_ms)","transcribe":"300 per minute per account (POST /api/transcribe), 429 rate_limit","monthly_budget":"When the monthly wizzo-token budget is reached every billable call returns 403 monthly_limit (limit_reached from the auth layer) until the next month or a budget change","agent_steps":"agents are bounded by limits.max_steps and limits.max_tokens","agent_concurrency":"3 running agent jobs per account, 8 globally; extra jobs wait in the queue","image_urls":"Generated image URLs expire after 24 hours","payload":"JSON bodies up to 50MB; transcription files up to 64MB (OpenAI models 25MB)"},"error_codes":{"auth error":"401: missing/invalid api key, or inactive account","token_expired":"401: JWT expired","invalid_token":"401: JWT malformed or missing claims","forbidden":"403: endpoint needs an api key (JWT not allowed)","origin_mismatch":"403: JWT origin claim differs from the Origin header","limit_reached":"403: monthly budget reached (auth layer)","monthly_limit":"403: monthly budget reached (during a call)","rate_limit":"429: too many requests for this endpoint","prompt_not_set":"400: prompt missing","prompt_not_found":"404: prompt_ref does not exist","INVALID_TOOL_SCHEMA":"400: settings.tools definition invalid","INVALID_TOOL_CALL":"400: the model produced a tool call that failed validation","provider_not_allowed":"400: the model resolved to a provider outside settings.allowed_providers","not_found":"404: row not found","exists":"409: unique name/slug/key already used","invalid_sysname":"400: sysname/slug must match ^[a-z0-9_]{2,64}$","invalid_tools":"400: agent tools must be builtin:<name> references","invalid_webhook":"400: agent webhook_url must be https and public","bad_cron":"400: crontime is not a valid 5-field cron expression","content_required":"400: content missing","INVALID_RUN_REQUEST":"400: agent run body invalid","transcribe_not_supported":"400: model cannot transcribe","file_too_large":"400: audio exceeds the limit","server_error":"500: unexpected server error"}}