SANTO VIDEO for developers
Create AI videos from your own code with the REST API, or let an AI assistant such as Claude, ChatGPT or Cursor make them for you through the SANTO MCP server. Both use your SANTO credits and the same models, prices and refunds as the app.
MCP server
For AI assistants. Paste one URL, sign in, approve. No code and no API key.
REST API
For your servers and scripts. JSON over HTTPS with an API key.
Same credits
Every video is charged at the price shown in the app and refunded if it fails.
https://mcp.santovideo.io/mcphttps://api.santovideo.io/v1Quickstart: your first video in 3 steps
- Create an API key. In SANTO open Developers → Keys and apps, name the key, choose its access and daily credit limit, and press Create key. Copy it right away: it is shown only once.
- Store it as a secret on your server, for example in an environment variable:
export SANTO_API_KEY="svk_live_…" - Start a video and wait for it. The first request starts the generation; the second waits up to 25 seconds per call until it is done.
# 1. Start a 5-second 720p video (1,800 credits)
curl https://api.santovideo.io/v1/generations \
-H "Authorization: Bearer $SANTO_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"model":"santo-video","prompt":"A drone shot over Tel Aviv at sunset","duration":5,"resolution":"720p"}'
# 2. Check it, waiting up to 25 seconds. Repeat until "status" is "succeeded" or "failed".
curl "https://api.santovideo.io/v1/generations/GENERATION_ID?wait=25" \
-H "Authorization: Bearer $SANTO_API_KEY"// Node.js 18+ (or Deno, Bun). Keep the key on the server.
const API = 'https://api.santovideo.io/v1';
const auth = { Authorization: `Bearer ${process.env.SANTO_API_KEY}` };
async function call(path, init = {}) {
const res = await fetch(API + path, { ...init, headers: { ...auth, ...init.headers } });
const body = await res.json();
if (!res.ok) throw new Error(`${body.error.code}: ${body.error.message} (${body.error.request_id})`);
return body;
}
// 1. Start a video. The Idempotency-Key makes a retry of this request safe.
let gen = await call('/generations', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Idempotency-Key': crypto.randomUUID() },
body: JSON.stringify({ model: 'santo-video', prompt: 'A drone shot over Tel Aviv at sunset', duration: 5, resolution: '720p' })
});
console.log('Started', gen.id, 'for', gen.cost_credits, 'credits');
// 2. Wait until it is done (each call waits up to 25 seconds on the server).
while (gen.status === 'queued' || gen.status === 'processing') {
gen = await call(`/generations/${gen.id}?wait=25`);
}
console.log(gen.status, gen.video_url ?? gen.error);# Python 3.8+ with requests (pip install requests). Keep the key on the server.
import os, uuid, requests
API = "https://api.santovideo.io/v1"
s = requests.Session()
s.headers["Authorization"] = f"Bearer {os.environ['SANTO_API_KEY']}"
def call(method, path, **kw):
r = s.request(method, API + path, timeout=60, **kw)
body = r.json()
if not r.ok:
e = body["error"]
raise RuntimeError(f"{e['code']}: {e['message']} ({e['request_id']})")
return body
# 1. Start a video. The Idempotency-Key makes a retry of this request safe.
gen = call("POST", "/generations",
json={"model": "santo-video", "prompt": "A drone shot over Tel Aviv at sunset", "duration": 5, "resolution": "720p"},
headers={"Idempotency-Key": str(uuid.uuid4())})
print("Started", gen["id"], "for", gen["cost_credits"], "credits")
# 2. Wait until it is done (each call waits up to 25 seconds on the server).
while gen["status"] in ("queued", "processing"):
gen = call("GET", f"/generations/{gen['id']}", params={"wait": 25})
print(gen["status"], gen["video_url"] or gen["error"])The video_url is a private download link that works for 6 hours. Ask for the generation again to get a fresh link; the video also stays in your SANTO library.
Authentication
API keys (REST API)
Send the key in the Authorization header on every request: Authorization: Bearer svk_live_…. Keys in a URL or query string are refused.
| Access | Permissions | Use it for |
|---|---|---|
| Create and read | generations:write generations:read account:read models:read | Servers that make videos |
| Read only | generations:read account:read models:read | Dashboards, monitoring, price checks. It can never spend credits. |
Each key has its own daily credit limit (default 40,000) and an expiry (30 days, 90 days, 1 year or never). You can have up to 10 active keys. A request with a revoked or expired key fails right away with 401.
OAuth 2.1 (MCP server)
Assistants connect with OAuth 2.1 and PKCE. You never paste a key into them: the assistant opens a SANTO page, you sign in and press Allow, and it receives a short-lived token that works only on the MCP server. Each connected app has its own daily credit limit (default 16,000) and you can disconnect it at any time in Developers → Keys and apps.
For MCP clients that cannot open a browser (for example a server-side agent), send an API key in the Authorization header instead.
Connect an AI assistant (MCP)
The SANTO MCP server uses the Streamable HTTP transport and OAuth sign-in. Use this URL in any MCP client:
https://mcp.santovideo.io/mcpClaude (claude.ai and Claude Desktop)
- Open Settings → Connectors and choose Add custom connector.
- Name it SANTO, paste the server URL and press Add.
- Press Connect, sign in to SANTO and press Allow access.
ChatGPT
- Open Settings → Apps and connectors → Advanced settings and turn on Developer mode.
- Choose Create, paste the server URL and pick OAuth as the authentication.
- Sign in to SANTO and press Allow access.
Claude Code
claude mcp add --transport http santo https://mcp.santovideo.io/mcp
Then type /mcp in Claude Code, choose santo and sign in.
Cursor
Settings → MCP → Add new MCP server, or add this to ~/.cursor/mcp.json:
{
"mcpServers": {
"santo": { "url": "https://mcp.santovideo.io/mcp" }
}
}
VS Code (GitHub Copilot)
Add this to .vscode/mcp.json and press Start above the server:
{
"servers": {
"santo": { "type": "http", "url": "https://mcp.santovideo.io/mcp" }
}
}
Other clients and testing
Any client that supports remote MCP servers with OAuth works. To try the server by hand, run npx @modelcontextprotocol/inspector, choose Streamable HTTP and paste the URL. Clients without OAuth can send Authorization: Bearer svk_live_….
/.well-known/oauth-protected-resource (RFC 9728) and answers unauthenticated requests with 401 and a WWW-Authenticate header, so clients find the SANTO sign-in page on their own. Dynamic client registration and PKCE (S256) are supported.MCP tools
| Tool | What it does | Credits |
|---|---|---|
list_services | Every SANTO service (video, image, edit, face swap, extend, motion control, lip-sync, talking video, upscale, dub, music, voice-over, sound effects…) with the inputs it needs, durations and prices per tier. | Free |
list_models | Models with resolutions, durations, aspect ratios and prices. | Free |
get_balance | Credit balance and how much of the app's daily limit is used. | Free |
estimate_cost | Exact cost before making anything. Same arguments as create_generation (with service) or create_video (with model). | Free |
create_video | Starts a video. Arguments: prompt (required), model, duration, resolution, aspect_ratio, audio. Returns the generation. | Spends credits |
create_generation | Starts any service. Arguments: service (required), tier, prompt, duration, aspect_ratio, audio, input_urls ([{"role","url"}], public https files) and params. Returns the generation. | Spends credits |
get_generation | Status and the result link when ready. Waits up to wait_seconds (0–25, default 20). | Free |
list_generations | Most recent generations, newest first (limit 1–50). | Free |
Read-only tools are marked with readOnlyHint, so clients can run them without asking. The server tells the assistant to show you the cost with estimate_cost before it calls create_video or create_generation. Try: "Using SANTO, how much would a 10-second 1080p video cost? Then make it: a red sports car on a coastal road at golden hour."
REST API endpoints
Base URL: https://api.santovideo.io/v1. Requests and responses are JSON (Content-Type: application/json). Every response has an X-Request-Id header.
/generations
Starts a video and charges its cost. Permission: generations:write. Returns 201 with the generation, or 200 with Idempotent-Replayed: true when the same Idempotency-Key is sent again.
| Field | Type | Description |
|---|---|---|
model | string, required | A model id from GET /models, for example santo-video. |
prompt | string, required | What the video shows. Up to 5,000 characters. |
duration | integer | Seconds, within the model's range. Default: the model's shortest. |
resolution | string | One of the model's resolutions, for example 720p. Default: the model's lowest. |
aspect_ratio | string | 16:9 (default), 9:16, 1:1, 4:3, 3:4 or 21:9, as the model allows. |
audio | boolean | Native audio where the model supports it. Default false. |
Unknown fields are refused with 400, so a typo never silently changes what you pay for.
curl https://api.santovideo.io/v1/generations \
-H "Authorization: Bearer $SANTO_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 7c1d0d3e-5f7a-4b51-9a8e-2f0c1b7d9e44" \
-d '{"model":"santo-video","prompt":"Macro shot of coffee pouring, slow motion","duration":5,"resolution":"720p","aspect_ratio":"9:16"}'
Any SANTO service
Instead of model, send service to use any service from GET /services: edit, face swap, extend, motion control, lip-sync, talking video, upscale, dubbing, images, music, voice-over and sound effects. Prices, caps, idempotency and refunds work exactly as above.
| Field | Type | Description |
|---|---|---|
service | string, required | A service id, for example video, face-swap, lipsync, image, music. |
tier | string | standard (default) or pro. |
prompt | string | What to make or change. Required for services that take no input file. |
duration | integer | Seconds, within the service's range (per-second services only). Default: the shortest. |
aspect_ratio | string | For services that list aspect ratios. |
audio | boolean | Native audio where supported. Default false. |
inputs | array | Up to 8 files: {"role": "video", "path": "uploads/…"} from POST /uploads, or {"role": "face", "url": "https://…"}. URLs must be public https addresses (no IP addresses, ports or private hosts) serving an image, video or audio file up to 200 MB; we copy them into your storage. Roles: first_frame, last_frame, middle_frame, ref_image, face, product, person, garment, video, audio. |
params | object | Service options, for example script (voice-over), effect, language (dub), voiceId. |
curl https://api.santovideo.io/v1/generations \
-H "Authorization: Bearer $SANTO_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 0b9f3c1e-2a44-4d1b-8f0e-6c5a7d2e9b10" \
-d '{"service":"face-swap","tier":"standard","duration":5,
"inputs":[{"role":"video","path":"uploads/…/clip.mp4"},{"role":"face","url":"https://example.com/me.jpg"}]}'
/generations/{id}
Returns one generation. Permission: generations:read. Add ?wait=N (0–25) to hold the request for up to N seconds until the generation finishes, which saves polling. Unknown ids, and ids of other accounts, return 404.
/generations
Your generations, newest first, including those made in the app. Permission: generations:read. Query: limit (1–100, default 20) and starting_after (the last id of the previous page).
{ "object": "list", "data": [ { "id": "api_…", "object": "generation", … } ], "has_more": true }
/estimates
Prices a generation without starting it. Same body as POST /generations, either form (URL inputs are not downloaded). Permission: models:read.
{ "object": "estimate", "model": "santo-video", "duration": 5, "resolution": "720p", "aspect_ratio": "16:9",
"audio": false, "cost_credits": 1800, "balance": 12000, "enough_credits": true }
/models
Video models with their resolutions, durations, aspect ratios and price per second. Permission: models:read.
/services
Every SANTO service with its kind, unit (second, item or kchar = 1,000 characters), duration range, aspect ratios, required_inputs (each inner list: at least one file with one of these roles), accepted input_roles, and per tier credits_per_unit, resolution and live. Permission: models:read.
{ "id": "face-swap", "object": "service", "name": "Face Swap", "kind": "video", "unit": "second",
"duration_seconds": { "min": 3, "max": 15 }, "required_inputs": [["video"], ["face", "ref_image"]],
"tiers": { "standard": { "credits_per_unit": 564, "resolution": "720p", "live": true }, "pro": { … } } }
/uploads
A one-time URL to upload an input file (counts toward your storage quota). Body: filename, content_type (image/*, video/* or audio/*) and size in bytes. PUT the file to upload_url with its Content-Type within expires_in seconds, then pass path in inputs. Permission: generations:write.
{ "object": "upload", "path": "uploads/…/m3k2-1a2b3c4d-clip.mp4", "upload_url": "https://…", "method": "PUT",
"headers": { "Content-Type": "video/mp4" }, "expires_in": 7200 }
/account
Your credit balance and plan, this key's daily limit and what it has spent today. Permission: account:read.
{ "object": "account", "credits": 12000, "plan": "pro", "daily_credit_limit": 5000, "credits_used_today": 500,
"caller": { "type": "api_key", "scopes": ["account:read", "generations:read", "generations:write", "models:read"] } }
The generation object
{
"id": "api_6f1c2a9b0e4d4b7c8a1f3e5d2c9b8a71",
"object": "generation",
"status": "succeeded", // queued | processing | succeeded | failed
"progress": 100,
"model": "santo-video",
"prompt": "A drone shot over Tel Aviv at sunset",
"duration": 5,
"resolution": "720p",
"aspect_ratio": "16:9",
"audio": false,
"cost_credits": 500,
"credits_status": "charged", // reserved | charged | refunded | on_hold
"video_url": "https://…", // when succeeded; valid until video_url_expires_at
"video_url_expires_at": "2026-10-30T18:00:00.000Z",
"service": "video", // the SANTO service (see GET /services)
"tier": "standard",
"kind": "video", // video | image | audio
"output_url": "https://…", // the result of any service (same link as video_url)
"output_url_expires_at": "2026-10-30T18:00:00.000Z",
"error": null, // a short reason when failed
"source": "api", // app | api | mcp
"created_at": "2026-10-30T12:00:00.000Z"
}
Videos usually take 1 to 5 minutes. When a generation fails, credits_status becomes refunded on its own. on_hold means our team is checking the job with the model provider; it ends as charged or refunded.
Models and prices
The price is credits per second × duration. GET /models always has the current list.
| Model id | Resolutions (credits per second) | Duration | Aspect ratios |
|---|---|---|---|
santo-video | 720p Standard (360), 1080p Pro (860) | 3–30 s | 16:9, 9:16, 1:1, 21:9 |
Errors
Errors use standard HTTP status codes and always have the same JSON shape. Program against error.code; the message is for people and may change. Quote request_id when you contact support.
{
"error": {
"type": "invalid_request",
"code": "invalid_request",
"message": "\"duration\" for santo-video must be 3–30 seconds.",
"request_id": "req_4b1f0c8e2d7a49e3b6c51a0f",
"docs": "https://santovideo.io/developers/"
}
}
| Status | code | What to do |
|---|---|---|
| 400 | invalid_request, unknown_model, model_unavailable, unsupported_aspect, price_unknown | Fix the request. The message names the field. |
| 401 | authentication_required, invalid_key, invalid_token | Send a valid, unrevoked key in the Authorization header. |
| 402 | insufficient_credits | Top up credits or upgrade the plan. |
| 403 | insufficient_scope, account_suspended | Use a key with the needed access. |
| 404 | not_found | Check the id and the path. |
| 409 | idempotency_mismatch | The Idempotency-Key was used for a different body. Use a new key. |
| 413 / 415 | invalid_request | Keep the body under 64 KB and send it as application/json. |
| 429 | rate_limited, app_daily_limit, daily_limit, too_many_active | Wait for Retry-After seconds, raise the key's daily limit, or let running videos finish. |
| 500 | server_error | Retry with the same Idempotency-Key. Nothing is charged twice. |
| 503 | maintenance, capacity, generation_paused | Retry in a few minutes. |
Limits and billing
| Limit | Value |
|---|---|
| Requests | 120 a minute per API key or connected app |
| New generations | 10 a minute per account |
| Daily credits | Per key (default 40,000) and per connected app (default 16,000). Set it in Developers → Keys and apps. |
| Your plan | The same running-job and daily limits as the app |
| Request body | 64 KB |
Billing: the cost is taken from your balance when a generation starts (credits_status: "reserved"), becomes charged when the video is ready, and is returned automatically if it fails (refunded). Use POST /estimates or estimate_cost to see a price first.
Safe retries (Idempotency-Key)
Networks fail. To retry POST /generations without paying twice, send a unique Idempotency-Key header (for example a UUID) and reuse it on every retry of that request. The same key with the same body always returns the original generation (200, Idempotent-Replayed: true) and charges nothing more. The same key with a different body returns 409, so use a new key for every new video.
Retry on network errors, 429 (after Retry-After), 500 and 503, with exponential backoff. Do not retry other 4xx errors unchanged.
Security
- Keep keys on the server. Never put a key in a web page, mobile app, URL, log or Git repository. Use your platform's secret store or environment variables.
- One key per integration, with the least access it needs (read only where possible) and a daily credit limit that matches its job.
- Rotate: create a new key, deploy it, then revoke the old one. Revoking takes effect on the next request.
- Leaked key? Revoke it in Developers → Keys and apps right away. Keys start with
svk_live_so secret scanners can spot them. - SANTO stores only a keyed hash of each key, never the key itself, and shows it once.
- MCP sign-in uses OAuth 2.1 with PKCE. Tokens are short-lived, are bound to the SANTO MCP server, and stop working when you disconnect the app. Approve only connections you started yourself.
- All traffic is HTTPS and responses are never cached. Browsers cannot call the API from other websites (no CORS), which keeps keys out of front-end code.
- Each key's and app's last requests are listed under Recent activity in Developers.
Versioning
The API is versioned in the path (/v1). Within v1 we only add things: new endpoints, new optional fields and new error codes. Your code should ignore fields it does not know. Breaking changes get a new version and advance notice. The machine-readable description is at /developers/openapi.json (OpenAPI 3.1).