[ docs ]Build on SnooblEverything the studio does, you can do from code: make a brand, brief a project, get a storyboard, render a film. Same credits, same rules, no separate plan.
Quickstart: brief to film in five callsCreate an API key in Settings → API & webhooks, export it as SNOOBL_API_KEY, and run these in order. Swap the example ids for the ones each call returns.
Make a brand — the product world your films live in. Keep the id it returns.
1 · POST /v1/brands
curl -X POST "https://snoobl.com/v1/brands" \ -H "Authorization: Bearer $SNOOBL_API_KEY" \ -H "Content-Type: application/json" \ -d '{"name":"Midnight Repair","type":"Skincare · DTC"}'
Start a project inside that brand, with a brief in plain words.
2 · POST /v1/projects
curl -X POST "https://snoobl.com/v1/projects" \ -H "Authorization: Bearer $SNOOBL_API_KEY" \ -H "Content-Type: application/json" \ -d '{"brandId":"0197d2c4-8a1e-7c3b-9f20-5b6e1a2c3d4e","title":"Night serum — launch film","brief":"A moody 10-second launch film for the night serum. Glass bottle, dark marble, one slow push-in."}'
Ask the Director for a storyboard. It's free, and it runs as a job — poll the job until it's done to get the version id.
3 · POST /v1/projects/{id}/storyboard
curl -X POST "https://snoobl.com/v1/projects/$ID/storyboard" \ -H "Authorization: Bearer $SNOOBL_API_KEY" \ -H "Content-Type: application/json" \ -d '{"guide":"After-hours, glossy, minimal. Keep the bottle heroic.","creativity":42,"webResearch":false}'
Render it. This is the step that spends credits — the response tells you exactly how many.
4 · POST /v1/renders
curl -X POST "https://snoobl.com/v1/renders" \ -H "Authorization: Bearer $SNOOBL_API_KEY" \ -H "Content-Type: application/json" \ -d '{"projectId":"0197d2c5-11f0-7a44-8e02-9c1d2e3f4a5b","versionId":"0197d2c6-3b7a-7d15-a4c9-0e1f2a3b4c5d","aspectRatio":"16:9","durationSeconds":10,"resolution":"720p","tier":"standard","generateAudio":true,"videoCreativity":30,"count":1}'
Poll the render (or skip polling and use a webhook). When it's completed, videoUrl is your film.
5 · GET /v1/renders/{id}
curl "https://snoobl.com/v1/renders/$ID" \ -H "Authorization: Bearer $SNOOBL_API_KEY"
Every endpoint, with a live console →
API keys and scopesSend your key as a bearer token on every request. A key belongs to a workspace, not a person: it sees only that workspace and spends its shared credits. You see the key once, when you create it — we only keep a hash.
curl
curl "https://snoobl.com/v1/credits" \ -H "Authorization: Bearer $SNOOBL_API_KEY"
read
Look, don't touch: list and fetch brands, projects, renders, credits.
render
Everything in read, plus creating brands, projects, storyboards and renders. This is the scope that can spend credits.
manage
Everything in render, plus managing webhook endpoints.
Keep keys on a server. Anyone holding one can spend the workspace's credits — never ship a key in an app or a web page.
ErrorsEvery failure has the same shape: a stable code to branch on and a message written for a human. If credits were going to be spent and something broke, they weren't.
402
{ "error": { "code": "insufficient_credits", "message": "That render needs 12 credits and the workspace has 3." } }
400
invalid_request
The request is malformed — the message names the field and what's wrong with it.
401
unauthenticated
The API key is missing, wrong or revoked.
402
insufficient_credits
The workspace can't afford this. Nothing was charged.
403
forbidden
The key lacks the scope this needs, or the resource belongs to another workspace.
404
not_found
No such resource.
409
provider_unavailable
The model this needs is down right now. Nothing was charged — try again shortly.
429
rate_limited
Too many requests for this key this minute. `Retry-After` says how long to wait.
Rate limitsEach key gets 60 requests a minute. Every response carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset (seconds until the window resets). Go over and you get a 429 with Retry-After — wait that long and carry on. Need more headroom? Tell us at hello@snoobl.com.
WebhooksRenders take minutes, so don't poll if you don't have to. Add an endpoint in Settings → API & webhooks (or with POST /v1/webhooks) and we'll POST to it when a render completes or fails.
render.completed
POST https://your-server.example/hooks/snoobl X-Snoobl-Signature: t=1760000000,v1=5f2b…c91e Content-Type: application/json { "id": "evt_0197d2c9-…", "type": "render.completed", "created": 1760000000, "data": { "take": { "id": "0197d2c7-…", "status": "completed", "creditsCost": 12, "videoUrl": "https://…signed, good for an hour…" }, "project": { "id": "0197d2c5-…", "title": "Night serum — launch film" }, "brand": { "id": "0197d2c4-…", "name": "Midnight Repair" } } }
Answer with any 2xx within 10 seconds. Anything else and we try again after 1 minute, 5 minutes, 30 minutes and 2 hours, then give up. The event id stays the same across retries, so use it to ignore duplicates.Check the signatureEvery delivery is signed with your endpoint's secret (shown once, when you create it). The header carries a timestamp and an HMAC-SHA256 of the timestamp, a dot, and the raw body.
node
import { createHmac, timingSafeEqual } from "node:crypto"; // rawBody must be the exact bytes we sent — verify BEFORE you JSON.parse. export const isFromSnoobl = (rawBody, header, secret) => { const parts = Object.fromEntries( header.split(",").map((part) => part.trim().split("=")), ); const timestamp = Number(parts.t); // reject anything older than five minutes (replay protection). if (Math.abs(Date.now() / 1000 - timestamp) > 300) return false; const expected = createHmac("sha256", secret) .update(`${timestamp}.${rawBody}`) .digest("hex"); const given = Buffer.from(parts.v1 ?? ""); return ( given.length === expected.length && timingSafeEqual(given, Buffer.from(expected)) ); };
Your films are yours. Commercial use included — no asterisk.
© 2026 Snoobl · hello@snoobl.com