API
Hands-on access to the same render engine. Base URL below points at the Hetzner Studio service; api.monkeygun.com will alias the same binary.
Agent-ready: /llms.txt · /llms-full.txt · openapi.yaml
Base
https://api.monkeygun.com/v1
Quickstart
Four steps from nothing to a finished film. Every call after the first is the same pattern: bearer key, JSON in, JSON out.
- 1
Get your key
A key is the account — no signup, no password. Create one on the account page, fund it there, and it works everywhere below as
Create a key →Authorization: Bearer sk_live_…. - 2
Find your stories
Send your numbers (or prose, or a URL) and get back every story the data supports, ranked, with the evidence behind each. You pick one.
curl -X POST $BASE/stories \ -H "authorization: Bearer $KEY" -H "content-type: application/json" \ -d '{ "business": "Freightline", "fields": [ {"key":"deposits","role":"stock","unit":"usd","noun":"in deposits"}, {"key":"sellers","role":"population","agents":"sellers"} ], "metrics": {"deposits": 48000, "sellers": 120} }' - 3
Render
Film the story you picked. The lines you read in step 2 are the lines it speaks. Pass a
webhookUrland we tell you when it lands instead of making you poll.curl -X POST $BASE/render \ -H "authorization: Bearer $KEY" -H "content-type: application/json" \ -d '{ "template": "monkeygun-story", "storyId": "arc_9f2c1d40", "options": {"look":"noir","fidelity":"standard"}, "webhookUrl": "https://yours.example/hook" }' - 4
Collect
Poll the job — status, progress, queue position, and the result URL when it is done.
curl $BASE/jobs/$JOB_ID \ -H "authorization: Bearer $KEY" # → {"status":"done","progress":1,"result":{"mediaUrl":"https://…"}} # URL expired? GET $BASE/jobs/$JOB_ID/link re-signs it ({"url":…}) — the file is kept 90 days
The flow
Discover, price, render, get told. Each step is a separate call so you can stop after any of them.
- 1
Find the stories
POST /stories with your columns and rows, or with text or a URL. Numbers go to the relational engine — thirteen scored relations over your fields. Prose goes to Arc, which finds the sequence. Two independent dials shape a prose story: angle is the stance (hype, proud, straight, skeptical, roast — which moments make the cut and who wins) and tone is the voice (how the lines sound). Roast needs selfSubject: true — your own film only. Both engines return a ranked list; you pick.
curl -X POST $BASE/stories \ -H "authorization: Bearer $KEY" -H "content-type: application/json" \ -d '{ "business": "Freightline", "fields": [ {"key":"deposits","role":"stock","unit":"usd","noun":"in deposits"}, {"key":"sellers","role":"population","agents":"sellers"} ], "metrics": {"deposits": 48000, "sellers": 120} }' - 2
Price it
POST /estimate with a look and a fidelity. The number is a ceiling — the render bills min(quoted, actual), so a shot dropped by the prompt guard or a cached narration line makes it cheaper, never dearer.
curl -X POST $BASE/estimate \ -H "authorization: Bearer $KEY" -H "content-type: application/json" \ -d '{"fidelity":"standard","look":"noir"}' - 3
Render it
POST /render with the storyId you picked, on the monkeygun-story template. The film argues that exact story — the lines you read in step 1 are the lines it speaks, because the story is stored when you discover it rather than rebuilt when you film it. Pass a webhookUrl and we will tell you when it lands instead of making you poll.
curl -X POST $BASE/render \ -H "authorization: Bearer $KEY" -H "content-type: application/json" \ -d '{ "template": "monkeygun-story", "storyId": "arc_9f2c1d40", "options": {"look":"noir","fidelity":"standard"}, "webhookUrl": "https://yours.example/hook" }' - 3b
Or film a chain
Some films are formats rather than engine compositions. chain-metrics argues one ratio — stablecoins parked on a chain against TVL working in its DeFi, benchmarked against Ethereum. It reads two ways and both are true: a tight ratio says the money here works, a wide one says the capital is waiting for builders. Figures are pulled live from DefiLlama at render time and cited on screen with the date; if that read fails we refuse rather than ship last month's liquidity.
curl -X POST $BASE/render \ -H "authorization: Bearer $KEY" -H "content-type: application/json" \ -d '{"template":"chain-metrics","chainName":"aptos"}' - 3c
Or cut it from your own footage
Pull spans of your own video into a per-key clip library — POST /clips with attest: true per item, because we will not download footage you have not confirmed is yours. Label clips free at ingest or with a PATCH; or POST /clips/scan to have a model look and write the labels — quoted first (2 credits per ≤10s clip), spent only on confirm: true, and any label not written is refunded. For stills, GET /images/search answers from four pools — your library, your site, Pexels, the open web — each candidate carrying its license in plain words. Picked images ride into the render as a top-level assets object with rightsConfirmed: true; anything from the web pool also needs its own attest: true or the render refuses.
# find candidates across the four pools curl "$BASE/images/search?q=warehouse+robots&pools=library,site,pexels,web" \ -H "authorization: Bearer $KEY" # → candidates tagged {pool, url, license, attribution?, requiresAttestation} # render with the images you picked — assets ride at the TOP LEVEL of the body curl -X POST $BASE/render \ -H "authorization: Bearer $KEY" -H "content-type: application/json" \ -d '{ "template": "monkeygun-story", "storyId": "arc_9f2c1d40", "assets": { "assets": [ {"url": "https://yours.example/product.jpg"}, {"url": "https://found.example/shot.jpg", "source": "web", "attest": true} ], "rightsConfirmed": true } }' # rightsConfirmed must be the literal true, and every source:"web" image needs # its own attest: true — the render refuses otherwise, before any money moves - 4
Buy credits
Any amount from $10 to $5,000 per charge — the packs on /templates are quick-picks on the same ladder, not the only things for sale. Bonus tiers are published in the catalog so you never have to hardcode them: 10% from $15, 15% from $40, 25% from $100. Credits never expire.
curl -X POST $BASE/checkout \ -H "authorization: Bearer $KEY" -H "content-type: application/json" \ -d '{ "kind": "credits", "usd": 250, "successUrl": "https://yours.example/ok", "cancelUrl": "https://yours.example/no" }' # → {"orderId":"...","checkoutUrl":"https://checkout.stripe.com/..."} - 5
Publish it forever
Register a feed, set a trigger, connect Buffer. When the feed changes and the condition fires, we discover a story from its latest numbers, film it, score it, and post it. Anything below the confidence threshold — or any feed you marked review-only — holds in /holds instead of publishing. PATCH a feed with {killed:true} to stop it dead.
# 1. the data source, and what its columns MEAN curl -X POST $BASE/feeds -H "authorization: Bearer $KEY" \ -H "content-type: application/json" -d '{ "url": "https://yours.example/metrics.json", "pollIntervalSec": 3600, "fieldMap": [{"key":"deposits","role":"stock","unit":"usd"}] }' # 2. what should make a film curl -X POST $BASE/feeds/fd_123/triggers -H "authorization: Bearer $KEY" \ -H "content-type: application/json" -d '{ "condition": {"key":"deposits","op":"increase_pct","value":20}, "template": "monkeygun-story", "options": {"look":"noir","channelId":"YOUR_BUFFER_CHANNEL"} }' # 3. where it goes curl -X PUT $BASE/keys/me/buffer -H "authorization: Bearer $KEY" \ -H "content-type: application/json" -d '{"token":"YOUR_BUFFER_TOKEN"}' curl $BASE/keys/me/buffer -H "authorization: Bearer $KEY" # → every channel the token reaches, with its service (twitter/linkedin/...) - 6
Verify the webhook
Every delivery carries X-Monkeygun-Signature as t=<unix>,v1=<hmac>. The HMAC is SHA-256 over `${t}.${rawBody}` with your webhook secret. The timestamp is inside the signed material, so a captured delivery cannot be replayed later. Compare in constant time and reject anything older than five minutes.
const [t, v1] = header.split(",").map((p) => p.split("=")[1]); const expected = crypto.createHmac("sha256", secret) .update(t + "." + rawBody).digest("hex"); const ok = crypto.timingSafeEqual( Buffer.from(expected, "hex"), Buffer.from(v1, "hex"), ) && Math.abs(Date.now() / 1000 - Number(t)) < 300;
Endpoints
41 endpoints. Everything marked key wants Authorization: Bearer sk_live_….
Discover
What your data supports, and what it would cost, before you spend anything.
- POST
/storieskeyEvery story your data supports — numbers to the relational engine, prose to Arc. Returns a ranked list with the evidence behind each.
- POST
/estimatekeyWhat a render costs at a given look + fidelity.
- GET
/templatesCatalog, credit packs, top-up bounds and bonus tiers, payment rails.
- GET
/looksAll 33 looks with real computed prices. No key needed.
- GET
/presetsBeat-sheet preset catalog.
Render
Order a film and collect it. A quote is a ceiling; you are billed from the shot plan.
- POST
/renderkeyOrder a film. Bearer key + credits.
- GET
/jobs/:jobIdStatus, progress, queue position, result URL.
- GET
/jobs/:jobId/linkA fresh download URL for a past render. Signed URLs last 7 days; the file is kept 90, so come back and re-sign rather than re-render.
- POST
/checkoutCard checkout — {kind:"render"} for one film, {kind:"credits", usd} to top up.
- GET
/checkout/:orderIdPoll a paid order for its jobIds.
Clip library
Your own footage, pulled into a per-key store so films can be cut from it. Every pull needs the literal attest: true — you are confirming, per item, that the footage is yours to use, and we will not download it without that. Retail keys hold 20 clips for 30 days; the GET envelope restates your limits so you never hardcode them.
- POST
/clipskeyPull a span (default: the first 15s, max 120s) of a YouTube or Vimeo video into your store. Body: {youtubeUrl, attest: true, startSec?, endSec?, label?}. A label at ingest is free — you know what your footage shows.
- POST
/clips/batchkeyUp to 20 items in one call, each carrying its OWN attest: true — a batch-level flag is never accepted, because N downloads is N consents. Per-item results in the order sent; one refusal never takes its neighbours down.
- GET
/clipskeyYour clips — only ever yours — with the tier's limits beside the rows.
- PATCH
/clips/:clipIdkeyWrite the label yourself: one line, what it shows / when to use it. Free, and your words always overwrite a scan's.
- POST
/clips/scankeyA model looks at your unlabeled clips and writes the labels. Quote-first: the first call prices it (2 credits per ≤10s clip) and charges nothing — resend with confirm: true to spend. The quote is a ceiling; any label that was not written is refunded.
- DELETE
/clips/:clipIdkeyRow and bytes together. Frees the slot immediately.
Images
Find stills for image-hungry looks, then send the ones you picked into the render as a top-level assets object: {assets: [{url, source?, attest?}], rightsConfirmed: true}. rightsConfirmed must be the literal true, checked before any money moves.
- GET
/images/search?q=keyOne query across four pools — library (your stored clips), site (your own pages), pexels, web. Each candidate carries its license in plain words, an off-image attribution line when the license wants one, and requiresAttestation. The web pool is always requiresAttestation: true — a web image renders only if you send it with source:"web" and its own attest: true, and the render refuses otherwise. A pool that fails answers {pool, error} beside the ones that worked.
Keys and credits
A key is the account. There are no users, sessions or passwords — send an email at creation and you can get a lost key back.
- POST
/keysCreate a key — returns the secret and the webhook signing secret, once. Send `email` to make it recoverable.
- GET
/keys/meBalance, tier, wholesale rate, usage.
- POST
/keys/recoverEmail yourself a recovery link. Always 202 — we will not confirm whether an address has a key.
- POST
/keys/recover/confirmSpend the link's token. ROTATES the secret on your existing key: credits, feeds, triggers and Buffer all survive, the old secret dies.
- POST
/promo/redeemkeyTurn a promo code into credits on your key.
- GET
/keys/me/providerskeyWhich of your own provider keys we hold (fal, OpenRouter, ElevenLabs).
- PUT
/keys/me/providerskeyStore one of your own provider keys — your render, your quota.
- DELETE
/keys/me/providerskeyRemove one.
Account keys
The optional front door for teams: a Supabase login that holds several API keys. Auth here is the Supabase access token, not an sk_live key — in the Authorization header on the GET (a GET carries no body), in the body as `token` everywhere else, so it never lands in an access log. Once linked, you use your key like everyone else.
- GET
/auth/keysYour account's keys — labels, timestamps and a mask, never a secret. Send the Supabase token as `Authorization: Bearer <token>`.
- POST
/auth/keysMint an additional key. Body: {token, label?}. The plaintext crosses the wire exactly once, in this response — after that it is reveal or nothing.
- POST
/auth/keys/:keyId/revealShow one key's secret again, deliberately — its own POST (body: {token}) so the secret only travels when you ask for it.
- DELETE
/auth/keys/:keyIdRevoke one key. Body: {token}. Refused in plain words when it is your last.
Publish without a human
Register a data source, say what should trigger a film, and connect Buffer. Everything published passes a confidence gate first, and anything below it holds for review instead.
- POST
/feedskeyRegister a data source: url, poll interval, and what the columns MEAN.
- GET
/feedskeyYour feeds, with last poll, last change and last error.
- PATCH
/feeds/:feedIdkeyThe kill switch. Stops a feed dead — reach for this first, diagnose second.
- POST
/feeds/:feedId/triggerskeyFire a render when a condition on the feed is met. The template must be a story template.
- GET
/holdskeyFilms the gate declined to publish on its own authority.
- POST
/holds/:jobId/approvekeyRelease a held film.
- POST
/holds/:jobId/rejectkeyBin it.
- PUT
/keys/me/bufferkeyStore your Buffer token, encrypted and scoped to your key.
- GET
/keys/me/bufferkeyWhether it is configured, plus every channel it reaches with its service — that is how we know which socials you have.
- DELETE
/keys/me/bufferkeyDisconnect.
Data adapters
Snapshots we can read for you, when you would rather not fetch them yourself.
- GET
/adapters/markets?symbol=Stock / ticker snapshot.
- GET
/adapters/github?repo=GitHub repo snapshot.
Token films in one call
Token templates skip discovery — the subject is an address, not a story.
# 1) Catalog
curl -s https://api.monkeygun.com/v1/templates | jq '.templates[].id'
# 2) Card checkout (browser return URL)
curl -s -X POST https://api.monkeygun.com/v1/checkout \
-H 'content-type: application/json' \
-d '{
"kind": "render",
"template": "spotlight-clip",
"chain": "solana",
"tokenAddress": "<MINT>",
"successUrl": "https://monkeygun.com/studio?order={ORDER_ID}",
"cancelUrl": "https://monkeygun.com/studio"
}'
# 3) Or with credits
curl -s -X POST https://api.monkeygun.com/v1/render \
-H "authorization: Bearer sk_live_…" \
-H 'content-type: application/json' \
-d '{
"template": "spotlight-clip",
"chain": "solana",
"tokenAddress": "<MINT>"
}'Trust rail
AI beds never invent prices or market claims. Overlays and VO bind to adapter snapshots (chain / markets / GitHub). Banned-claims validation runs on generative prompts before fal.
