SANTO VIDEODEVELOPERS

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.

MCP SERVER URLhttps://mcp.santovideo.io/mcp
REST API BASE URLhttps://api.santovideo.io/v1

Quickstart: your first video in 3 steps

  1. 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.
  2. Store it as a secret on your server, for example in an environment variable: export SANTO_API_KEY="svk_live_…"
  3. 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.

AccessPermissionsUse it for
Create and readgenerations:write generations:read account:read models:readServers that make videos
Read onlygenerations:read account:read models:readDashboards, 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:

MCP SERVER URLhttps://mcp.santovideo.io/mcp

Claude (claude.ai and Claude Desktop)

  1. Open Settings → Connectors and choose Add custom connector.
  2. Name it SANTO, paste the server URL and press Add.
  3. Press Connect, sign in to SANTO and press Allow access.

ChatGPT

  1. Open Settings → Apps and connectors → Advanced settings and turn on Developer mode.
  2. Choose Create, paste the server URL and pick OAuth as the authentication.
  3. 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_….

The server publishes its sign-in details at /.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

ToolWhat it doesCredits
list_servicesEvery 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_modelsModels with resolutions, durations, aspect ratios and prices.Free
get_balanceCredit balance and how much of the app's daily limit is used.Free
estimate_costExact cost before making anything. Same arguments as create_generation (with service) or create_video (with model).Free
create_videoStarts a video. Arguments: prompt (required), model, duration, resolution, aspect_ratio, audio. Returns the generation.Spends credits
create_generationStarts 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_generationStatus and the result link when ready. Waits up to wait_seconds (0–25, default 20).Free
list_generationsMost 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.

POST

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

FieldTypeDescription
modelstring, requiredA model id from GET /models, for example santo-video.
promptstring, requiredWhat the video shows. Up to 5,000 characters.
durationintegerSeconds, within the model's range. Default: the model's shortest.
resolutionstringOne of the model's resolutions, for example 720p. Default: the model's lowest.
aspect_ratiostring16:9 (default), 9:16, 1:1, 4:3, 3:4 or 21:9, as the model allows.
audiobooleanNative 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.

FieldTypeDescription
servicestring, requiredA service id, for example video, face-swap, lipsync, image, music.
tierstringstandard (default) or pro.
promptstringWhat to make or change. Required for services that take no input file.
durationintegerSeconds, within the service's range (per-second services only). Default: the shortest.
aspect_ratiostringFor services that list aspect ratios.
audiobooleanNative audio where supported. Default false.
inputsarrayUp 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.
paramsobjectService 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"}]}'
GET

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

GET

/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 }
POST

/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 }
GET

/models

Video models with their resolutions, durations, aspect ratios and price per second. Permission: models:read.

GET

/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": { … } } }
POST

/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 }
GET

/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 idResolutions (credits per second)DurationAspect ratios
santo-video720p Standard (360), 1080p Pro (860)3–30 s16: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/"
  }
}
StatuscodeWhat to do
400invalid_request, unknown_model, model_unavailable, unsupported_aspect, price_unknownFix the request. The message names the field.
401authentication_required, invalid_key, invalid_tokenSend a valid, unrevoked key in the Authorization header.
402insufficient_creditsTop up credits or upgrade the plan.
403insufficient_scope, account_suspendedUse a key with the needed access.
404not_foundCheck the id and the path.
409idempotency_mismatchThe Idempotency-Key was used for a different body. Use a new key.
413 / 415invalid_requestKeep the body under 64 KB and send it as application/json.
429rate_limited, app_daily_limit, daily_limit, too_many_activeWait for Retry-After seconds, raise the key's daily limit, or let running videos finish.
500server_errorRetry with the same Idempotency-Key. Nothing is charged twice.
503maintenance, capacity, generation_pausedRetry in a few minutes.

Limits and billing

LimitValue
Requests120 a minute per API key or connected app
New generations10 a minute per account
Daily creditsPer key (default 40,000) and per connected app (default 16,000). Set it in Developers → Keys and apps.
Your planThe same running-job and daily limits as the app
Request body64 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).