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. 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 Authorization: Bearer sk_live_….

    Create a key →
  2. 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. 3

    Render

    Film the story you picked. The lines you read in step 2 are the lines it speaks. Pass a webhookUrl and 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. 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. 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. 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. 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"
      }'
  4. 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"}'
  5. 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
  6. 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/..."}
  7. 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/...)
  8. 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/storieskey

    Every story your data supports — numbers to the relational engine, prose to Arc. Returns a ranked list with the evidence behind each.

  • POST/estimatekey

    What a render costs at a given look + fidelity.

  • GET/templates

    Catalog, credit packs, top-up bounds and bonus tiers, payment rails.

  • GET/looks

    All 33 looks with real computed prices. No key needed.

  • GET/presets

    Beat-sheet preset catalog.

Render

Order a film and collect it. A quote is a ceiling; you are billed from the shot plan.

  • POST/renderkey

    Order a film. Bearer key + credits.

  • GET/jobs/:jobId

    Status, progress, queue position, result URL.

  • GET/jobs/:jobId/link

    A 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/checkout

    Card checkout — {kind:"render"} for one film, {kind:"credits", usd} to top up.

  • GET/checkout/:orderId

    Poll 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/clipskey

    Pull 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/batchkey

    Up 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/clipskey

    Your clips — only ever yours — with the tier's limits beside the rows.

  • PATCH/clips/:clipIdkey

    Write the label yourself: one line, what it shows / when to use it. Free, and your words always overwrite a scan's.

  • POST/clips/scankey

    A 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/:clipIdkey

    Row 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=key

    One 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/keys

    Create a key — returns the secret and the webhook signing secret, once. Send `email` to make it recoverable.

  • GET/keys/me

    Balance, tier, wholesale rate, usage.

  • POST/keys/recover

    Email yourself a recovery link. Always 202 — we will not confirm whether an address has a key.

  • POST/keys/recover/confirm

    Spend the link's token. ROTATES the secret on your existing key: credits, feeds, triggers and Buffer all survive, the old secret dies.

  • POST/promo/redeemkey

    Turn a promo code into credits on your key.

  • GET/keys/me/providerskey

    Which of your own provider keys we hold (fal, OpenRouter, ElevenLabs).

  • PUT/keys/me/providerskey

    Store one of your own provider keys — your render, your quota.

  • DELETE/keys/me/providerskey

    Remove 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/keys

    Your account's keys — labels, timestamps and a mask, never a secret. Send the Supabase token as `Authorization: Bearer <token>`.

  • POST/auth/keys

    Mint 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/reveal

    Show one key's secret again, deliberately — its own POST (body: {token}) so the secret only travels when you ask for it.

  • DELETE/auth/keys/:keyId

    Revoke 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/feedskey

    Register a data source: url, poll interval, and what the columns MEAN.

  • GET/feedskey

    Your feeds, with last poll, last change and last error.

  • PATCH/feeds/:feedIdkey

    The kill switch. Stops a feed dead — reach for this first, diagnose second.

  • POST/feeds/:feedId/triggerskey

    Fire a render when a condition on the feed is met. The template must be a story template.

  • GET/holdskey

    Films the gate declined to publish on its own authority.

  • POST/holds/:jobId/approvekey

    Release a held film.

  • POST/holds/:jobId/rejectkey

    Bin it.

  • PUT/keys/me/bufferkey

    Store your Buffer token, encrypted and scoped to your key.

  • GET/keys/me/bufferkey

    Whether it is configured, plus every channel it reaches with its service — that is how we know which socials you have.

  • DELETE/keys/me/bufferkey

    Disconnect.

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.