https://stream.ai.wizzo.media · view as JSONAll endpoints (except /api/health) require:
Authorization: <API_KEY>
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").
{
"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."
}
}curl -X POST https://stream.ai.wizzo.media/api/ai \
-H "Authorization: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"prompt":"כתוב כותרת לכתבה על מזג האוויר","model":"gpt-5-mini","settings":{"type":"headline"}}'const res = await fetch('https://stream.ai.wizzo.media/api/ai/json', {
method: 'POST',
headers: { 'Authorization': 'YOUR_API_KEY', 'Content-Type': 'application/json' },
body: JSON.stringify({
prompt_ref: 'headline_v2', // a prompt from your library
vars: { topic: 'מזג האוויר' }, // fills {$topic}
settings: { json: true }
})
});
const { data, wizzo_tokens } = await res.json();$ch = curl_init('https://stream.ai.wizzo.media/api/ai');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['Authorization: YOUR_API_KEY', 'Content-Type: application/json'],
CURLOPT_POSTFIELDS => json_encode([
'prompt' => 'סכם את הטקסט הבא בשלוש נקודות: ...',
'settings' => ['type' => 'summary', 'max_tokens' => 800]
])
]);
$res = json_decode(curl_exec($ch), true);
echo $res['data'];import requests
r = requests.post('https://stream.ai.wizzo.media/api/ai/json',
headers={'Authorization': 'YOUR_API_KEY'},
json={'prompt': 'Return {"ok":true}', 'settings': {'json': True}})
print(r.json()['data'])const res = await fetch('https://stream.ai.wizzo.media/api/ai/stream', {
method: 'POST',
headers: { 'Authorization': 'YOUR_API_KEY', 'Content-Type': 'application/json' },
body: JSON.stringify({ prompt: 'ספר לי בדיחה' })
});
const reader = res.body.getReader(); const dec = new TextDecoder(); let buf = '';
while (true) {
const { done, value } = await reader.read(); if (done) break;
buf += dec.decode(value, { stream: true });
let i; while ((i = buf.indexOf('\n\n')) !== -1) {
const line = buf.slice(0, i); buf = buf.slice(i + 2);
if (line.startsWith('data:')) { const ev = JSON.parse(line.slice(5)); if (ev.type === 'chunk') process.stdout.write(ev.content); }
}
}
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.
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.
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>".
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.
A prompt library with {$var} templating and versions. Call any endpoint with prompt_ref + vars instead of prompt.
Long-running server-side loops (model + builtin tools) with a step budget, a token budget, persisted steps, optional cron schedule and a finish webhook.
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.
{"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 eventReturns 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.
{
"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)"
}
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.
{
"data": {
"example_field": "parsed object/array - depends on the model output"
}
}
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).
POST /api/ai_image{
"data": [
"https://stream.ai.wizzo.media/uploads/ai_images/openai_173...png"
]
}
Returns an OpenAI embedding vector for the given text (defaults to text-embedding-3-small, 1536 dims).
{
"data": [
0.0123,
-0.0456,
"..."
]
}
Returns the active model catalog (id, name, provider, supports_tools, supports_vision, prices, ...).
GET /api/get_models · POST /api/get_models{
"data": {
"models": [
"..."
]
}
}
Creates a short-lived OpenAI Realtime session for browser WebRTC voice. Rate-limited to 5/minute per user.
{
"voice": "shimmer (default) | alloy | echo | ..."
}
Called by the client when a voice session ends. Body: { duration_ms: number }.
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.
{
"success": true,
"data": {
"text": "...",
"language": "he",
"duration": 12.4,
"segments": null,
"model": "gpt-transcribe",
"provider": "openai",
"wizzo_tokens": 5
}
}
No auth. Returns { status: "ok", timestamp }.
sysname ^[a-z0-9_]{2,64}$. A new version is created only when the content (whitespace-normalised) or the model changes.
{
"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"
}
]
}
}
{
"data": {
"id": 1,
"sysname": "headline_v2",
"content": "...",
"model": "gpt-5-mini",
"version": 3,
"comment": "",
"updated_at": "..."
}
}
{
"sysname": "headline_v2",
"content": "כתוב כותרת על {$topic}",
"model": "gpt-5-mini",
"comment": "optional"
}
{
"data": {
"id": 1,
"version": 1
}
}
{
"content": "?",
"model": "?",
"comment": "?",
"sysname": "?"
}
{
"data": {
"id": 1,
"version": 4,
"changed": true
}
}
{
"data": true
}
{
"data": {
"versions": [
{
"id": 9,
"version_number": 3,
"model": "",
"comment": "",
"is_current": true,
"created_at": "...",
"content": "..."
}
]
}
}
{
"version_number": 2
}
{
"data": {
"version": 4
}
}
{
"vars": {
"topic": "חורף"
}
}
{
"data": {
"content": "כתוב כותרת על חורף",
"model": "gpt-5-mini"
}
}
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.
{
"prompt_ref": "headline_v2",
"vars": {
"topic": "חורף"
}
}
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}.
{
"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
}
]
}
}
{
"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
}
{
"data": {
"id": 1
}
}
{
"prompt": "optional extra instruction",
"dry_run": false
}
{
"data": {
"run_id": 12,
"job_id": 340,
"status": "queued"
}
}
{
"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": "..."
}
]
}
}
{
"data": {
"run": {},
"steps": [
{
"step_num": 1,
"type": "tool",
"tool_name": "builtin:fetch_url",
"tool_args": "{...}",
"tool_result": "{...}",
"duration_ms": 120
}
]
}
}
{
"data": {
"status": "cancelled"
}
}
{
"kind": "playbook",
"mem_key": "style",
"mem_value": "כתוב קצר"
}
Lower-level API used by the CMS: you send the whole run definition, including MCP tool servers, and reconcile by polling.
{
"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"
}
{
"data": {
"run_id": 340,
"status": "queued"
}
}
{
"data": {
"run_id": 340,
"status": "done",
"steps_count": 4,
"tokens_in": 0,
"tokens_out": 0,
"wizzo_tokens": 0,
"summary": "...",
"error": null
}
}
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.
{
"success": true,
"data": {
"text": "...",
"language": "he",
"duration": 12.4,
"wizzo_tokens": 5
}
}
Returns public URLs under /uploads/ai_images that EXPIRE AFTER 24 HOURS. Copy the file if you need it longer.
{
"data": [
"https://stream.ai.wizzo.media/uploads/ai_images/....png"
]
}
{
"text": "...",
"type": "article_embedding",
"model": "text-embedding-3-small"
}
{
"data": [
0.01,
-0.02
],
"input_tokens": 12,
"wizzo_tokens": 1
}
5 sessions/minute per account. Report usage with POST /api/realtime/usage {duration_ms}; a session is billed up to 45 minutes.
{
"log_id": 1
}
{
"budget": 100000
}
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.
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)
4xx for client errors (prompt_not_set, monthly_limit, INVALID_TOOL_CALL, ...), 5xx for provider/server errors
{
"error": "short_code",
"message": "human readable Hebrew message"
}
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