Wizzo AI Streaming API

v1.0.0
Base URL: https://stream.ai.wizzo.media  ·  view as JSON

Authentication

All 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").

Common request body (for /api/ai, /api/ai/json and /api/ai/stream)

{
  "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."
  }
}

Getting started

  1. Get your api key from the WizzoAI dashboard (or market_service::api_key("wizzoai") in a PHP CORE site).
  2. Send it as-is in the Authorization header on every request: "Authorization: <API_KEY>" (no Bearer prefix).
  3. POST /api/ai with { prompt } for a full answer, /api/ai/json for a parsed JSON answer, /api/ai/stream for SSE.
  4. Every success is { data: ... } plus usage fields; every failure is { error, message } with a 4xx/5xx status.
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); }
  }
}

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.

Core endpoints

POST /api/ai/stream

Streaming chat (Server-Sent Events)

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
POST /api/ai

Non-streaming chat (legacy PHP-compat)

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.

{
  "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)"
}
POST /api/ai/json

Non-streaming chat - parsed JSON

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"
  }
}
POST /api/ai/image

Image generation / editing

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).

aliases: POST /api/ai_image
{
  "data": [
    "https://stream.ai.wizzo.media/uploads/ai_images/openai_173...png"
  ]
}
POST /api/embed

Text embedding vector

Returns an OpenAI embedding vector for the given text (defaults to text-embedding-3-small, 1536 dims).

{
  "data": [
    0.0123,
    -0.0456,
    "..."
  ]
}
GET /api/models

List available models

Returns the active model catalog (id, name, provider, supports_tools, supports_vision, prices, ...).

aliases: GET /api/get_models · POST /api/get_models
{
  "data": {
    "models": [
      "..."
    ]
  }
}
GET /api/realtime/session

OpenAI Realtime ephemeral session

Creates a short-lived OpenAI Realtime session for browser WebRTC voice. Rate-limited to 5/minute per user.

{
  "voice": "shimmer (default) | alloy | echo | ..."
}
POST /api/realtime/usage

Report Realtime session usage (billing)

Called by the client when a voice session ends. Body: { duration_ms: number }.

POST /api/transcribe

Speech-to-text transcription

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
  }
}
GET /api/health no auth

Health check

No auth. Returns { status: "ok", timestamp }.

Prompts (library with {$var} templating and versions)

sysname ^[a-z0-9_]{2,64}$. A new version is created only when the content (whitespace-normalised) or the model changes.

GET /api/prompts

List prompts

{
  "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"
      }
    ]
  }
}
GET /api/prompts/:sysname

Get a prompt by sysname

{
  "data": {
    "id": 1,
    "sysname": "headline_v2",
    "content": "...",
    "model": "gpt-5-mini",
    "version": 3,
    "comment": "",
    "updated_at": "..."
  }
}
POST /api/prompts

Create

{
  "sysname": "headline_v2",
  "content": "כתוב כותרת על {$topic}",
  "model": "gpt-5-mini",
  "comment": "optional"
}
{
  "data": {
    "id": 1,
    "version": 1
  }
}
Errors: 409 exists 400 invalid_sysname
PUT /api/prompts/:id

Update (partial)

{
  "content": "?",
  "model": "?",
  "comment": "?",
  "sysname": "?"
}
{
  "data": {
    "id": 1,
    "version": 4,
    "changed": true
  }
}
DELETE /api/prompts/:id

Delete (cascades versions)

{
  "data": true
}
GET /api/prompts/:id/versions

Version history, newest first

{
  "data": {
    "versions": [
      {
        "id": 9,
        "version_number": 3,
        "model": "",
        "comment": "",
        "is_current": true,
        "created_at": "...",
        "content": "..."
      }
    ]
  }
}
POST /api/prompts/:id/restore

Restore a version (creates a new version with the old content)

{
  "version_number": 2
}
{
  "data": {
    "version": 4
  }
}
POST /api/prompts/:sysname/render

Render with vars

{
  "vars": {
    "topic": "חורף"
  }
}
{
  "data": {
    "content": "כתוב כותרת על חורף",
    "model": "gpt-5-mini"
  }
}
POST /api/ai | /api/ai/json | /api/ai/stream

Shortcut: prompt_ref + vars

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": "חורף"
  }
}

Agents (definitions, runs, memory, scheduler)

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}.

GET /api/agents

List with weekly stats

{
  "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
      }
    ]
  }
}
GET /api/agents/:id

Get definition

POST /api/agents

Create

{
  "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
  }
}
PUT /api/agents/:id

Update (partial)

DELETE /api/agents/:id

Delete (runs and memory too)

POST /api/agents/:id/run

Run now

{
  "prompt": "optional extra instruction",
  "dry_run": false
}
{
  "data": {
    "run_id": 12,
    "job_id": 340,
    "status": "queued"
  }
}
POST /api/agents/:id/test

Test run (dry_run: tools get __dry_run: true)

GET /api/agents/:id/runs

Runs, newest first (status synced from the runtime)

{
  "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": "..."
      }
    ]
  }
}
GET /api/agents/runs/:run_id

Run detail with the step timeline

{
  "data": {
    "run": {},
    "steps": [
      {
        "step_num": 1,
        "type": "tool",
        "tool_name": "builtin:fetch_url",
        "tool_args": "{...}",
        "tool_result": "{...}",
        "duration_ms": 120
      }
    ]
  }
}
POST /api/agents/runs/:run_id/cancel

Cancel

{
  "data": {
    "status": "cancelled"
  }
}
GET /api/agents/:id/memory

Memory rows (playbook + insight rows are appended to the system prompt on every run)

POST /api/agents/:id/memory

Add memory

{
  "kind": "playbook",
  "mem_key": "style",
  "mem_value": "כתוב קצר"
}
DELETE /api/agents/:id/memory/:mid

Delete memory row

Agent runtime (bring your own control plane)

Lower-level API used by the CMS: you send the whole run definition, including MCP tool servers, and reconcile by polling.

POST /api/agent/run

Enqueue a run

{
  "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"
  }
}
GET /api/agent/run/:id

Run status

{
  "data": {
    "run_id": 340,
    "status": "done",
    "steps_count": 4,
    "tokens_in": 0,
    "tokens_out": 0,
    "wizzo_tokens": 0,
    "summary": "...",
    "error": null
  }
}
POST /api/agent/run/:id/cancel

Cancel a run

Images, embeddings, speech

POST /api/transcribe

Speech-to-text

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
  }
}
POST /api/ai/image

Image generation / editing

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"
  ]
}
POST /api/embed

Embedding vector

{
  "text": "...",
  "type": "article_embedding",
  "model": "text-embedding-3-small"
}
{
  "data": [
    0.01,
    -0.02
  ],
  "input_tokens": 12,
  "wizzo_tokens": 1
}
GET /api/realtime/session

OpenAI Realtime ephemeral session (voice)

5 sessions/minute per account. Report usage with POST /api/realtime/usage {duration_ms}; a session is billed up to 45 minutes.

Account and dashboard (api key only)

GET /api/general

Account info + monthly usage

GET /api/get_dashboard_data

Stats for a date_range (month_offset_0 ...)

GET /api/get_usage_logs

Paginated prompt history (page, per_page, search, type, model)

POST /api/get_log_detail

One log row with its settings

{
  "log_id": 1
}
POST /api/change_budget

Set the monthly budget

{
  "budget": 100000
}
GET /api/models

Model catalog

Public JWT mode (browser-facing calls)

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 and budgets

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 format

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"
}

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