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: what the call cost your account (input, cached input, output and any web search), counted at the model prices GET /api/models lists for your account. 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. An account marked unlimited by the operator (general.user.unlimited = true) has no budget and no per-minute call limit; its usage is still logged.
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 features and the price of each model for your account, in wizzo tokens. 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, features, defaults) priced for YOUR account in wizzo tokens: tokens_input / tokens_output / tokens_cached_input per 1M tokens, tokens_per_call, tokens_per_minute, tokens_per_unit for media models.
GET /api/get_models · POST /api/get_models{
"data": {
"models": [
{
"id": 1,
"name": "gpt-5-mini",
"provider": "openai",
"tokens_input": 75000,
"tokens_output": 600000,
"features_images": false,
"default_model": true
}
],
"price_unit": "wizzo tokens ..."
}
}
Mints a short-lived OpenAI Realtime client secret (GA API) for browser WebRTC voice: client_secret.value, expires_at, model, sdp_url. 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 }.
The same account/usage tools as the WizzoCMS admin panel (ai_dashboard, ai_usage_logs, ai_log_detail, ai_models, ai_test_flight), for any MCP client — including the Wizzo Market gateway (mcp.wizzo.market). Auth: "Authorization: <wizzoai api_key>" (or "Bearer <api_key>"), OR a Wizzo Market MCP token ("Bearer wzm_…" / "Bearer wza_…"), resolved via the market's mcp_introspect and mapped to this account by market_id. Methods: initialize, tools/list, tools/call, notifications/* (202).
GET /mcp (self-description, no auth)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"
}
}
{
"on": true
}
{
"message": "הסיכום ארוך מדי, תן רק 3 סיפורים",
"fresh": false
}
{
"data": {
"ok": true,
"run_id": 13
}
}
{
"messages": [
{
"role": "user",
"content": "סוכן שמסכם לי כל בוקר חדשות טכנולוגיה"
}
],
"draft": {}
}
{
"on": false
}
{
"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"
]
}
Body: {model, prompt (video: the scene; speech: the text; avatar: the script), settings, webhook_url?}. Video (fal.ai: fal-ai/veo3.1, fal-ai/veo3.1/fast, fal-ai/kling-video/v3/pro/...): settings.duration (seconds), aspect_ratio, resolution, generate_audio (default true), image_url for image-to-video. Speech (ElevenLabs: eleven_v3, eleven_v4, eleven_v4_turbo, Hebrew included): settings.voice_id, language_code, output_format; finished inside the call. Avatar (HeyGen: heygen/avatar-iii|iv|v with settings.avatar_id, heygen/photo-avatar-iv with settings.image_url and voice_id). Answers 202 with the job; read it with GET /api/media/jobs/{id} until status is done or failed. Billed per the model unit (second of video, 1,000 characters, minute of avatar); a failed job costs nothing. A prepaid account must hold the estimate (402 otherwise). Files are ours, kept 7 days (expires_at). webhook_url (https) gets one POST {event: "media.job.finished", job} when it ends.
{
"data": {
"id": 12,
"kind": "video",
"model": "fal-ai/veo3.1/fast",
"status": "running",
"result_url": null,
"unit": "second",
"units": 0,
"estimated_units": 4,
"wizzo_tokens": 0
}
}
status queued|running|done|failed; when done: result_url (our copy), mime, units billed, wizzo_tokens, expires_at. GET /api/media/jobs?status=&limit= lists the account's recent jobs.
{
"data": {
"id": 12,
"status": "done",
"result_url": "https://stream.ai.wizzo.media/uploads/ai_media/video_....mp4",
"unit": "second",
"units": 4,
"wizzo_tokens": 60000
}
}
Lets a video still waiting in the provider queue go: it ends failed with error "cancelled" and costs nothing (cancelled: true). A render the provider already started cannot be stopped: it ends done and is billed as usual, and the answer is the job as it is with cancelled: false. Avatars and speech are never cancelled.
{
"data": {
"id": 12,
"status": "failed",
"error": "cancelled",
"units": 0,
"cost_usd": 0
},
"cancelled": true
}
{
"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.
account 600 requests per minute per account across every endpoint (a raw key, a JWT and a browser token all count against the account), 429 rate_limit with a Retry-After header; a higher ceiling can be set per account
ip 1200 requests per minute per client address, authenticated or not, 429 rate_limit; nginx caps bursts in front of it
bad_credentials 10 bad keys or tokens from one address within 10 minutes block that address for 30 minutes, 429 ip_blocked; an address that also authenticated with a valid credential in the last 24 hours is blocked only past 10 times that count
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 The monthly wizzo-token budget is checked on every billable call against the spend logged so far this month; once reached every billable call returns 403 monthly_limit (limit_reached from the auth layer) until the next month or a budget change. Not for an unlimited account (general.user.unlimited), which also has no per-minute limit
spend_brake An account whose spend in the last hour jumps far above its own average (or above an absolute hourly ceiling) is frozen at once and the operator is alerted; every call then answers 401 until a person releases it
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, account or address (Retry-After says when)
ip_blocked 429: this address sent too many bad credentials and is blocked for a while
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