Engine API
The loopback HTTP surface the Desktop engine exposes on 127.0.0.1
The Desktop engine is a small HTTP server on 127.0.0.1 โ the same surface the app uses, stable enough to wire an alternate UI or an integration against.
from praisonaiagents import Agent
agent = Agent(
name="Assistant",
instructions="You are a helpful assistant.",
)
# The Desktop app talks to an engine exactly like this one over 127.0.0.1.
agent.start("Hello")Every endpoint binds to 127.0.0.1, and requests from a browser origin the engine does not recognise are refused with 403. Nothing leaves your machine unless the model itself does.
Quick Start
- Find the port
The app prints
engine :PORTwhen the engine is healthy. That is the loopback port every route below uses. - Probe /health
GET /healthconfirms the engine is up and returns the true data directory.bash curl http://127.0.0.1:PORT/health - Call a route
Every response is JSON except
/chat(SSE) and/train/progress(SSE).
Routes
| Route | Purpose |
|---|---|
GET /health | Includes data_dir, version, shell_version, and agents_version |
GET /settings | Current settings; secrets returned masked as bullets |
POST /settings | Merge a settings patch; the response uses the same redaction as GET โ a write to any field returns the full settings dict with secrets masked |
GET /frameworks | Which agent frameworks this install can run, plus install hints for the ones it cannot. Returns {available, known, hints, default, error}. See Agent Framework. |
POST /chat | SSE stream (covered in Chat & Streaming) |
POST /approve/{approval_id} | Approve or deny a pending tool call; missing body means deny |
GET /mcp, POST /mcp | List and manage MCP servers |
GET /media/capabilities | What media generation is available; now returns video_models and a video_hint |
GET /media/video/models | The video model catalog with a per-model configured flag |
POST /media/video | Generate a video; optional model field defaults to replicate/minimax/video-01. See Video generation |
GET /search?q= | Full-text search across transcripts |
POST /fork/{cid}/{idx} | Fork a conversation at a message |
DELETE /messages/{cid}/{idx} | Delete a message |
POST /project/{cid} | Assign a conversation to a project |
DELETE /chats/{cid} | Delete a conversation |
POST /train/start | Start a fine-tune; 400 on invalid method or column set |
POST /train/stop/{run_id} | Kill the named run; stale-tab safe |
GET /train/progress?run=โฆ&cursor=N | Replay from a cursor then follow; 400 on a malformed cursor |
GET /train/status | The live run and a tail of its metrics |
GET /train/runs | Finished runs, bounded to the last 50 |
GET /frameworks response shape
{
"available": ["praisonai"],
"known": ["praisonai", "crewai", "autogen", "langgraph", "agno", "google-adk", "openai-agents", "pydantic-ai"],
"hints": {
"crewai": "pip install \"praisonai-frameworks[crewai]\"",
"โฆ": "โฆ"
},
"default": "praisonai",
"error": null
}available is always at least ["praisonai"]. When the praisonai wrapper is missing from the engine venv the endpoint still answers 200 with an error string explaining why, so the UI can report capability instead of failing the request.
Media routes
The /media/* routes back the Video tab. Generation is a single synchronous POST /media/video โ there is no job store or polling. Full walkthrough: Video generation.
GET /media/video/models lists the catalog with a per-model configured flag:
{
"ok": true,
"models": [
{ "id": "replicate/minimax/video-01", "display_name": "MiniMax video-01 (Replicate)", "provider": "replicate", "configured": true },
{ "id": "openai/sora-2", "display_name": "OpenAI Sora 2", "provider": "openai", "configured": false }
]
}POST /media/video accepts prompt and an optional model. The Replicate path returns a hosted url; SDK paths (Sora, Veo) return a data_url and a null url:
// Request
{ "prompt": "A cat playing with yarn", "model": "openai/sora-2" }
// SDK response
{ "id": "vid_โฆ", "path": ".../media-output/videos/vid_โฆ.mp4",
"url": null, "data_url": "data:video/mp4;base64,โฆ", "model": "openai/sora-2", "prompt": "โฆ" }GET /media/capabilities includes video_models and a video_hint alongside the image fields:
{ "ok": true, "video": true,
"video_models": [ { "id": "replicate/minimax/video-01", "configured": true } ],
"video_hint": "Set a provider key โฆ (REPLICATE_API_TOKEN for MiniMax, OPENAI_API_KEY for Sora, GEMINI_API_KEY for Veo)." }Browser Origin Gate
The engine refuses browser origins it does not recognise, because loopback keeps other machines out but not other pages a user has open.
A loopback address keeps other machines out, but any page open in the user’s browser can reach 127.0.0.1 with a two-line fetch. Matching is on the parsed hostname, so lookalike hosts never slip through.
| Origin | Verdict |
|---|---|
tauri://localhost (macOS, Linux webview) | Allowed |
http://tauri.localhost (Windows, Android webview) | Allowed |
http://localhost:<port> / http://127.0.0.1:<port> (dev server) | Allowed |
No Origin header (CLI, curl, tests, local processes) | Allowed |
Any other Origin (a real web page) | 403 |
Origin: null (sandboxed iframe, file://) | 403 |
Lookalike hosts (http://tauri.localhost.evil.com, http://localhost.evil.com) | 403 |
Bogus scheme (javascript:โฆ) | 403 |
Allowed responses echo the caller’s own Origin (with Vary: Origin) instead of โ Access-Control-Allow-Origin: no longer appears anywhere on the surface.
A refusal carries no CORS headers, so the calling page cannot read the reply either. OPTIONS is gated the same way โ a refused preflight returns 403, which is what stops the follow-up request in a real browser.
Status Codes
The engine returns a status that matches the cause, so a client shows the right thing instead of a bare drop.
| Code | Meaning |
|---|---|
200 | Success |
400 | Bad request โ missing fields, malformed /train/progress?cursor=, or a rejected run id |
403 | Browser origin refused (the engine does not recognise it) |
404 | No such run, chat, or pending approval |
409 | A training run is already live โ one GPU runs one job |
500 | The runs directory could not be created |
Bounded Responses
The training routes read from bounded buffers, so a long-running engine cannot grow without limit.
| Route | Bound |
|---|---|
GET /train/runs | Last 50 finished runs (MAX_HISTORY=50) |
GET /train/status | Up to the last 500 metric points; the series itself is capped at MAX_METRICS=5000 |
The /health Response
/health reports the true data directory, honouring PRAISONAI_DESKTOP_HOME, XDG_DATA_HOME, and APPDATA โ so the app can copy the real path rather than reproduce a default.
{
"ok": true,
"version": 2,
"shell_version": "unknown",
"agents_version": "1.7.2",
"data_dir": "/Users/you/Library/Application Support/PraisonAI"
}Best Practices
Read data_dir from /health, not the default
The path can be overridden by an environment variable. Ask /health for data_dir instead of assuming the platform default.
Stop by run id
POST /train/stop/{run_id} names the run to cancel, so a stale client cannot cancel a newer run. Always include the id.
Reconnect with a cursor
GET /train/progress replays from cursor=N, so a dropped connection resumes without missing events. A malformed cursor returns 400.
Treat a missing approval body as deny
POST /approve/{approval_id} defaults to deny when the body is absent or malformed โ never rely on an empty body to allow.
Send an Origin the engine trusts
Third-party clients should call from a webview served on localhost (dev server) or the Tauri webview origin; a browser tab on any other origin gets 403. A local process (CLI, script, curl) with no Origin header is allowed.