{"openapi":"3.1.0","info":{"title":"Memethropic API","description":"Inject contextually-placed memes into blog posts, newsletters, or articles — via a simple HTTP API. Single-meme generation is included, but content injection is the main event.\n\n## Getting started\n\n1. Sign up at https://memethropic.com — free plan includes 15 lifetime credits, no card.\n2. Create a key at https://memethropic.com/dashboard/keys.\n3. `POST /v1/generate/auto` with a topic.\n4. Poll `GET /v1/jobs/{jobId}` every 1–2s until `status === \"completed\"`.\n\n```bash\ncurl -X POST https://api.memethropic.com/v1/generate/auto \\\n  -H \"X-API-Key: $MEMETHROPIC_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"topic\":\"deploying on Friday\",\"tone\":\"casual\"}'\n```\n\nMost jobs finish in 3–8 seconds.\n\n## Authentication\n\nSend `X-API-Key: <key>` on every `/v1/*` request. Session cookies (`better-auth.session_token`) authenticate `/account/*` dashboard routes — you rarely call those from code.\n\n## Rate limits (per IP, per endpoint)\n\n| Endpoint | Limit |\n|---|---|\n| `GET /v1/templates` | 60 req/min |\n| `GET /v1/jobs/*` | 120 req/min |\n| `POST /v1/generate/auto` | 10 req/min |\n| `POST /v1/generate/inject` | 6 req/min |\n\nWhen exceeded, the response is `429` with body `{ \"error\": \"Too many requests\", \"retryAfter\": 60 }` and a `Retry-After: 60` header.\n\n## Error shape\n\nAll errors return JSON: `{ \"error\": \"human-readable message\", ... }`. Some errors include extra fields, e.g. `{ \"error\": \"Insufficient credits\", \"creditsRequired\": 13, \"creditsRemaining\": 5 }`.\n\n## Polling\n\nNo webhooks today — poll `GET /v1/jobs/{jobId}`. Recommended cadence: every 1s for the first 10 tries, then back off to 2s. Typical jobs complete in 3–8s. Responses are scoped to the authenticated user.\n\n## Streaming (inject only)\n\nAdd `?stream=true` to `POST /v1/generate/inject` to receive Server-Sent Events. Event types:\n\n- `content` — initial content with meme placeholders (`memesTotal`)\n- `meme` — one meme finished rendering (`position`, `url`, `template`, `captions`, updated `content`)\n- `done` — pipeline complete (`totalMemes`, `creditsCharged`)\n- `error` — failure (`message`)\n\n## Template selection guide\n\nMatch content patterns to template structures:\n\n| Content pattern | Structure | Description |\n|---|---|---|\n| Comparing two things | `comparison` | Two things side by side |\n| Favoring one option | `preference` | Reject one, choose another |\n| Steps going wrong | `escalation` / `plan-backfire` | Each step intensifies, or plans backfire |\n| Contradiction | `denial` / `reveal` | Calm despite chaos, or a twist |\n| Bold claim shut down | `correction` / `reaction` | Wrong take corrected, or emotional reaction |\n| Torn between options | `temptation` | Inner conflict |\n| Things secretly the same | `identity` | Two things that are identical |\n| Ranking | `hierarchy` | Tiers or layers |\n\nFilter templates by structure, zone count, or search:\n\n```\nGET /v1/templates?structure=comparison&zoneCount=2\nGET /v1/templates?search=drake\n```\n\n## Content Injection (paid plans only)\n\nInject memes into articles, blog posts, or newsletters. Send content directly — or a URL to fetch — and receive it back with memes placed inline.\n\n**Credit costs:**\n\n| Content length | Base | Per meme |\n|---|---|---|\n| ≤ 1,500 words | 5 | +1 |\n| 1,500 – 3,000 | 8 | +1 |\n| 3,000 – 5,000 | 12 | +1 |\n| 5,000 – 10,000 | 18 | +1 |\n\nExample: 2,000-word article with 5 memes = 8 + 5 = 13 credits.\n\n## Available Templates\n\n### collision\n\n| Slug | Name | Zones | Preview |\n|---|---|---|---|\n| `a-train-hitting-a-school-bus` | A Train Hitting A School Bus | 2 | [preview](https://api.memethropic.com/meme-templates/a-train-hitting-a-school-bus.jpg) |\n\n### comparison\n\n| Slug | Name | Zones | Preview |\n|---|---|---|---|\n| `be-like-bill` | Be Like Bill | 4 | [preview](https://api.memethropic.com/meme-templates/be-like-bill.jpg) |\n| `big-book-small-book` | Big Book Small Book | 2 | [preview](https://api.memethropic.com/meme-templates/big-book-small-book.jpg) |\n| `buff-doge-vs-cheems` | Buff Doge vs Cheems | 4 | [preview](https://api.memethropic.com/meme-templates/buff-doge-vs-cheems.jpg) |\n| `epic-handshake` | Epic Handshake | 3 | [preview](https://api.memethropic.com/meme-templates/epic-handshake.jpg) |\n\n### correction\n\n| Slug | Name | Zones | Preview |\n|---|---|---|---|\n| `bart-simpson-chalkboard` | Bart Simpson   Chalkboard | 1 | [preview](https://api.memethropic.com/meme-templates/bart-simpson-chalkboard.jpg) |\n| `batman-slapping-robin` | Batman Slapping Robin | 2 | [preview](https://api.memethropic.com/meme-templates/batman-slapping-robin.jpg) |\n\n### denial\n\n| Slug | Name | Zones | Preview |\n|---|---|---|---|\n| `3-spiderman-pointing` | 3 Spiderman Pointing | 4 | [preview](https://api.memethropic.com/meme-templates/3-spiderman-pointing.jpg) |\n| `a-center-for-ants` | A Center For Ants | 1 | [preview](https://api.memethropic.com/meme-templates/a-center-for-ants.jpg) |\n| `am-i-a-joke-to-you` | Am i a joke to you | 1 | [preview](https://api.memethropic.com/meme-templates/am-i-a-joke-to-you.jpg) |\n| `ancient-aliens` | Ancient Aliens | 2 | [preview](https://api.memethropic.com/meme-templates/ancient-aliens.jpg) |\n| `are-we-the-baddies` | Are We The Baddies | 2 | [preview](https://api.memethropic.com/meme-templates/are-we-the-baddies.jpg) |\n| `bad-luck-brian` | Bad Luck Brian | 2 | [preview](https://api.memethropic.com/meme-templates/bad-luck-brian.jpg) |\n| `batman-smiles` | Batman Smiles | 2 | [preview](https://api.memethropic.com/meme-templates/batman-smiles.jpg) |\n| `bird-box` | Bird Box | 1 | [preview](https://api.memethropic.com/meme-templates/bird-box.jpg) |\n\n### escalation\n\n| Slug | Name | Zones | Preview |\n|---|---|---|---|\n| `20-minute-adventure-rick-morty` | 20 Minute Adventure Rick Morty | 4 | [preview](https://api.memethropic.com/meme-templates/20-minute-adventure-rick-morty.jpg) |\n| `5-panel-gru-meme` | 5 Panel Gru Meme | 5 | [preview](https://api.memethropic.com/meme-templates/5-panel-gru-meme.jpg) |\n| `aaaaand-its-gone` | Aaaaand Its Gone | 2 | [preview](https://api.memethropic.com/meme-templates/aaaaand-its-gone.jpg) |\n| `american-chopper-argument` | American Chopper Argument | 5 | [preview](https://api.memethropic.com/meme-templates/american-chopper-argument.jpg) |\n| `beginnings-of-human-evolution` | Beginnings Of Human Evolution | 1 | [preview](https://api.memethropic.com/meme-templates/beginnings-of-human-evolution.jpg) |\n| `bernie-i-am-once-again-asking-for-your-support` | Bernie I Am Once Again Asking For Your Support | 1 | [preview](https://api.memethropic.com/meme-templates/bernie-i-am-once-again-asking-for-your-support.jpg) |\n| `bill-gates-giant-ping-pong-paddle` | Bill gates giant ping pong paddle | 2 | [preview](https://api.memethropic.com/meme-templates/bill-gates-giant-ping-pong-paddle.jpg) |\n\n### hierarchy\n\n| Slug | Name | Zones | Preview |\n|---|---|---|---|\n| `always-you-three` | Always You Three | 4 | [preview](https://api.memethropic.com/meme-templates/always-you-three.jpg) |\n| `bell-curve` | Bell Curve | 3 | [preview](https://api.memethropic.com/meme-templates/bell-curve.jpg) |\n\n### identity\n\n| Slug | Name | Zones | Preview |\n|---|---|---|---|\n| `avengers-thor-because-that-s-what-heroes-do` | Avengers thor because that_s what heroes do | 1 | [preview](https://api.memethropic.com/meme-templates/avengers-thor-because-that-s-what-heroes-do.jpg) |\n| `baby-godfather` | Baby Godfather | 1 | [preview](https://api.memethropic.com/meme-templates/baby-godfather.jpg) |\n| `big-ego-man` | Big Ego Man | 2 | [preview](https://api.memethropic.com/meme-templates/big-ego-man.jpg) |\n\n### isolation\n\n| Slug | Name | Zones | Preview |\n|---|---|---|---|\n| `am-i-the-only-one-around-here` | Am I The Only One Around Here | 1 | [preview](https://api.memethropic.com/meme-templates/am-i-the-only-one-around-here.jpg) |\n\n### labeling\n\n| Slug | Name | Zones | Preview |\n|---|---|---|---|\n| `angry-man-pointing-at-hand` | Angry Man Pointing At Hand | 1 | [preview](https://api.memethropic.com/meme-templates/angry-man-pointing-at-hand.jpg) |\n\n### plan-backfire\n\n| Slug | Name | Zones | Preview |\n|---|---|---|---|\n| `bike-fall` | Bike Fall | 3 | [preview](https://api.memethropic.com/meme-templates/bike-fall.jpg) |\n\n### reaction\n\n| Slug | Name | Zones | Preview |\n|---|---|---|---|\n| `aeroplane-reversing` | Aeroplane_reversing | 2 | [preview](https://api.memethropic.com/meme-templates/aeroplane-reversing.jpg) |\n| `aj-styles-undertaker` | Aj Styles  Undertaker | 2 | [preview](https://api.memethropic.com/meme-templates/aj-styles-undertaker.jpg) |\n| `and-everybody-loses-their-minds` | And Everybody Loses Their Minds | 2 | [preview](https://api.memethropic.com/meme-templates/and-everybody-loses-their-minds.jpg) |\n| `and-i-took-that-personally` | And I Took That Personally | 1 | [preview](https://api.memethropic.com/meme-templates/and-i-took-that-personally.jpg) |\n| `angry-gamer-girl` | Angry Gamer Girl | 1 | [preview](https://api.memethropic.com/meme-templates/angry-gamer-girl.jpg) |\n| `arthur-fist` | Arthur Fist | 1 | [preview](https://api.memethropic.com/meme-templates/arthur-fist.jpg) |\n| `bad-pun-dog` | Bad Pun Dog | 2 | [preview](https://api.memethropic.com/meme-templates/bad-pun-dog.jpg) |\n| `barney-stinson-win` | Barney Stinson Win | 1 | [preview](https://api.memethropic.com/meme-templates/barney-stinson-win.jpg) |\n| `black-guy-crying-and-black-guy-laughing` | Black Guy Crying And Black Guy Laughing | 2 | [preview](https://api.memethropic.com/meme-templates/black-guy-crying-and-black-guy-laughing.jpg) |\n| `woman-yelling-at-cat` | Woman Yelling at Cat | 2 | [preview](https://api.memethropic.com/meme-templates/woman-yelling-at-cat.jpg) |\n\n### reveal\n\n| Slug | Name | Zones | Preview |\n|---|---|---|---|\n| `2nd-term-obama` | 2nd Term Obama | 2 | [preview](https://api.memethropic.com/meme-templates/2nd-term-obama.jpg) |\n| `alan-greenspan` | Alan Greenspan | 1 | [preview](https://api.memethropic.com/meme-templates/alan-greenspan.jpg) |\n| `angela-scared-dwight` | Angela Scared Dwight | 2 | [preview](https://api.memethropic.com/meme-templates/angela-scared-dwight.jpg) |\n| `back-to-the-future` | Back To The Future | 2 | [preview](https://api.memethropic.com/meme-templates/back-to-the-future.jpg) |\n| `hide-the-pain-harold` | Hide the Pain Harold | 2 | [preview](https://api.memethropic.com/meme-templates/hide-the-pain-harold.jpg) |\n\n### temptation\n\n| Slug | Name | Zones | Preview |\n|---|---|---|---|\n| `afraid-to-ask-andy` | Afraid To Ask Andy | 1 | [preview](https://api.memethropic.com/meme-templates/afraid-to-ask-andy.jpg) |\n| `bilbo-why-shouldnt-i-keep-it` | Bilbo   Why Shouldnt I Keep It | 2 | [preview](https://api.memethropic.com/meme-templates/bilbo-why-shouldnt-i-keep-it.jpg) |\n| `distracted-boyfriend` | Distracted Boyfriend | 3 | [preview](https://api.memethropic.com/meme-templates/distracted-boyfriend.jpg) |\n| `evil-kermit` | Evil Kermit | 2 | [preview](https://api.memethropic.com/meme-templates/evil-kermit.jpg) |\n| `two-buttons` | Two Buttons | 3 | [preview](https://api.memethropic.com/meme-templates/two-buttons.jpg) |\n\n","version":"1.0.0"},"servers":[{"url":"https://api.memethropic.com","description":"Production"},{"url":"http://localhost:3000","description":"Local development"}],"components":{"securitySchemes":{"apiKey":{"type":"apiKey","in":"header","name":"X-API-Key","description":"Create one in the dashboard at /dashboard/keys."},"sessionCookie":{"type":"apiKey","in":"cookie","name":"better-auth.session_token","description":"Set automatically after dashboard sign-in."}}},"paths":{"/v1/templates":{"get":{"summary":"List templates","description":"Fetch published meme templates. No auth required. Rate-limited by IP.","tags":["Templates"],"x-codeSamples":[{"lang":"curl","label":"curl","source":"curl \"https://api.memethropic.com/v1/templates?structure=comparison&limit=5\""},{"lang":"javascript","label":"JavaScript","source":"const res = await fetch(\n  'https://api.memethropic.com/v1/templates?structure=comparison&limit=5'\n)\nconst { templates, total, page, totalPages } = await res.json()"},{"lang":"python","label":"Python","source":"import requests\n\nr = requests.get(\n    \"https://api.memethropic.com/v1/templates\",\n    params={\"structure\": \"comparison\", \"limit\": 5},\n)\ndata = r.json()\ntemplates = data[\"templates\"]"}],"parameters":[{"name":"page","in":"query","schema":{"type":"integer","default":1},"description":"Page number (1-indexed)."},{"name":"limit","in":"query","schema":{"type":"integer","default":24,"maximum":48},"description":"Results per page. Max 48."},{"name":"structure","in":"query","schema":{"type":"string"},"description":"Filter by structure (e.g. preference, comparison)."},{"name":"zoneCount","in":"query","schema":{"type":"integer"},"description":"Filter by exact caption zone count."},{"name":"search","in":"query","schema":{"type":"string"},"description":"Substring match on template name."},{"name":"exclude","in":"query","schema":{"type":"string"},"description":"Exclude a specific slug."}],"responses":{"200":{"description":"Paginated template list.","content":{"application/json":{"schema":{"type":"object","properties":{"templates":{"type":"array","items":{"type":"object","properties":{"slug":{"type":"string","example":"distracted-boyfriend"},"name":{"type":"string","example":"Distracted Boyfriend"},"structure":{"type":"string","nullable":true,"example":"preference"},"previewUrl":{"type":"string","example":"https://api.memethropic.com/meme-templates/distracted-boyfriend.jpg"},"zoneCount":{"type":"integer","example":3}}}},"total":{"type":"integer"},"page":{"type":"integer"},"totalPages":{"type":"integer"}}}}}},"429":{"description":"Rate limit exceeded."}}}},"/v1/templates/{slug}":{"get":{"summary":"Get template","description":"Fetch one template by slug, with caption zones, dimensions, and example captions.","tags":["Templates"],"parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Template details.","content":{"application/json":{"schema":{"type":"object","properties":{"slug":{"type":"string"},"name":{"type":"string"},"structure":{"type":"string","nullable":true},"description":{"type":"string"},"previewUrl":{"type":"string"},"width":{"type":"integer"},"height":{"type":"integer"},"zoneCount":{"type":"integer"},"captionZones":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string"},"label":{"type":"string"},"maxChars":{"type":"integer"},"fixed":{"type":"boolean"},"fixedValue":{"type":"string"}}}},"exampleCaptions":{"type":"array","nullable":true,"items":{"type":"object"}},"origin":{"type":"string","nullable":true},"exampleUrl":{"type":"string","nullable":true}}}}}},"404":{"description":"Template not found."},"429":{"description":"Rate limit exceeded."}}}},"/v1/generate/auto":{"post":{"summary":"Generate a meme","description":"Pick a template (or use one you specify), generate captions, queue a render. Returns a job ID — poll `/v1/jobs/{jobId}` for the result.","tags":["Generation"],"security":[{"apiKey":[]},{"sessionCookie":[]}],"x-codeSamples":[{"lang":"curl","label":"curl","source":"curl -X POST https://api.memethropic.com/v1/generate/auto \\\n  -H \"X-API-Key: $MEMETHROPIC_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"topic\":\"deploying on Friday\",\"tone\":\"casual\"}'"},{"lang":"javascript","label":"JavaScript","source":"const res = await fetch('https://api.memethropic.com/v1/generate/auto', {\n  method: 'POST',\n  headers: {\n    'X-API-Key': process.env.MEMETHROPIC_API_KEY,\n    'Content-Type': 'application/json',\n  },\n  body: JSON.stringify({ topic: 'deploying on Friday', tone: 'casual' }),\n})\nconst { jobId } = await res.json()\n// then poll /v1/jobs/{jobId}"},{"lang":"python","label":"Python","source":"import os, requests\n\nr = requests.post(\n    \"https://api.memethropic.com/v1/generate/auto\",\n    headers={\"X-API-Key\": os.environ[\"MEMETHROPIC_API_KEY\"]},\n    json={\"topic\": \"deploying on Friday\", \"tone\": \"casual\"},\n)\njob_id = r.json()[\"jobId\"]\n# then poll /v1/jobs/{job_id}"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["topic"],"properties":{"topic":{"type":"string","description":"What the meme is about.","example":"developers deploying on friday"},"template":{"type":"string","description":"Optional slug. AI picks one if omitted.","example":"drake-hotline-bling"},"tone":{"type":"string","enum":["safe","casual","spicy","degen"],"description":"Humor level. Default: casual."}}}}}},"responses":{"202":{"description":"Job queued.","content":{"application/json":{"schema":{"type":"object","properties":{"jobId":{"type":"string","example":"8e24728d-cac0-40ad-a016-11e643201ba2"},"status":{"type":"string","example":"pending"}}}}}},"400":{"description":"AI refused the topic."},"401":{"description":"Missing or invalid auth."},"404":{"description":"Requested template slug not found."},"429":{"description":"Rate or credit limit exceeded."},"500":{"description":"Generation failed."}}}},"/v1/generate/inject":{"post":{"summary":"Inject memes into content","description":"Place memes in an article, blog post, or newsletter. Paid plans only. Send `content` directly or a `url` to fetch. Add `?stream=true` for SSE streaming.","tags":["Generation"],"security":[{"apiKey":[]},{"sessionCookie":[]}],"parameters":[{"name":"stream","in":"query","schema":{"type":"boolean"},"description":"Stream memes via SSE as they render."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"content":{"type":"string","description":"Raw text, markdown, or HTML. Required unless `url` is set."},"url":{"type":"string","description":"URL to fetch content from. Required unless `content` is set."},"memeCount":{"type":"integer","minimum":1,"maximum":8,"description":"Number of memes to inject. Auto-sized if omitted."},"tone":{"type":"string","enum":["safe","casual","spicy","degen"],"description":"Humor level. Default: casual."},"format":{"type":"string","enum":["markdown","html"],"description":"Output format. Default: markdown."}}}}}},"responses":{"200":{"description":"SSE stream (when `?stream=true`)."},"202":{"description":"Job queued (async mode)."},"400":{"description":"Invalid input, free plan, or content over 10,000 words."},"401":{"description":"Missing or invalid auth."},"429":{"description":"Rate limit or insufficient credits."}}}},"/v1/jobs/{jobId}":{"get":{"summary":"Poll job status","description":"Check a generation or inject job. Most finish in 3–8 seconds. Scoped to the authenticated user.","tags":["Generation"],"security":[{"apiKey":[]},{"sessionCookie":[]}],"x-codeSamples":[{"lang":"curl","label":"curl","source":"curl -H \"X-API-Key: $MEMETHROPIC_API_KEY\" \\\n  https://api.memethropic.com/v1/jobs/<jobId>"},{"lang":"javascript","label":"JavaScript","source":"async function waitForMeme(jobId) {\n  for (let i = 0; i < 30; i++) {\n    const res = await fetch(`https://api.memethropic.com/v1/jobs/${jobId}`, {\n      headers: { 'X-API-Key': process.env.MEMETHROPIC_API_KEY },\n    })\n    const job = await res.json()\n    if (job.status === 'completed') return job.result\n    if (job.status === 'failed') throw new Error(job.error)\n    await new Promise((r) => setTimeout(r, i < 10 ? 1000 : 2000))\n  }\n  throw new Error('Timed out')\n}"},{"lang":"python","label":"Python","source":"import os, time, requests\n\ndef wait_for_meme(job_id: str):\n    for i in range(30):\n        r = requests.get(\n            f\"https://api.memethropic.com/v1/jobs/{job_id}\",\n            headers={\"X-API-Key\": os.environ[\"MEMETHROPIC_API_KEY\"]},\n        )\n        job = r.json()\n        if job[\"status\"] == \"completed\":\n            return job[\"result\"]\n        if job[\"status\"] == \"failed\":\n            raise RuntimeError(job.get(\"error\", \"job failed\"))\n        time.sleep(1 if i < 10 else 2)\n    raise TimeoutError(\"Job did not complete in time\")"}],"parameters":[{"name":"jobId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Job state. Shape of `result` depends on job type (single meme vs. inject).","content":{"application/json":{"schema":{"type":"object","properties":{"jobId":{"type":"string"},"status":{"type":"string","enum":["pending","processing","completed","failed"]},"result":{"type":"object","description":"Set when status=completed.","properties":{"url":{"type":"string","description":"Single-meme jobs: rendered meme URL."},"template":{"type":"string"},"captions":{"type":"object"},"width":{"type":"integer"},"height":{"type":"integer"},"content":{"type":"string","description":"Inject jobs: full output content."},"totalMemes":{"type":"integer","description":"Inject jobs only."},"creditsCharged":{"type":"integer","description":"Inject jobs only."},"memes":{"type":"array","description":"Inject jobs: placed memes.","items":{"type":"object","properties":{"id":{"type":"string"},"url":{"type":"string"},"template":{"type":"string"},"captions":{"type":"object"}}}}}},"partialResult":{"type":"object","description":"Inject jobs in progress: memes rendered so far.","properties":{"memesCompleted":{"type":"integer"},"memesTotal":{"type":"integer"},"memes":{"type":"array","items":{"type":"object"}}}},"error":{"type":"string","description":"Set when status=failed."}}}}}},"401":{"description":"Missing or invalid auth."},"404":{"description":"Job not found or not owned by you."}}}},"/account/keys":{"get":{"summary":"List API keys","description":"Return all API keys for the signed-in user.","tags":["Account"],"security":[{"sessionCookie":[]}],"responses":{"200":{"description":"Key list.","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"prefix":{"type":"string"},"name":{"type":"string"},"isActive":{"type":"boolean"},"lastUsedAt":{"type":"string","format":"date-time","nullable":true},"createdAt":{"type":"string","format":"date-time"}}}}}}},"401":{"description":"Not signed in."}}},"post":{"summary":"Create API key","description":"Create a new key. The raw key is returned **once** — store it immediately.","tags":["Account"],"security":[{"sessionCookie":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name"],"properties":{"name":{"type":"string","description":"Label, e.g. \"staging-bot\"."}}}}}},"responses":{"201":{"description":"Key created.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"key":{"type":"string","description":"Raw API key. Shown once."},"prefix":{"type":"string"},"name":{"type":"string"}}}}}},"400":{"description":"Plan key limit reached or validation failed."},"401":{"description":"Not signed in."}}}},"/account/keys/{id}":{"delete":{"summary":"Revoke API key","description":"Revoke a key by ID. Stops working immediately.","tags":["Account"],"security":[{"sessionCookie":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Revoked."},"401":{"description":"Not signed in."},"404":{"description":"Key not found or not owned by you."}}}},"/account/usage":{"get":{"summary":"Current usage","description":"Credits used, remaining, and rollover/topup balances for the current period.","tags":["Account"],"security":[{"sessionCookie":[]}],"responses":{"200":{"description":"Usage snapshot.","content":{"application/json":{"schema":{"type":"object","properties":{"plan":{"type":"string"},"creditLimit":{"type":"integer"},"creditsUsed":{"type":"integer"},"creditsRemaining":{"type":"integer"},"isLifetime":{"type":"boolean"},"rolloverCredits":{"type":"integer"},"topupCredits":{"type":"integer"},"subscriptionCancelAt":{"type":"string","format":"date-time","nullable":true}}}}}},"401":{"description":"Not signed in."}}}},"/account/usage/history":{"get":{"summary":"Usage history","description":"Daily counts for the last 30 days plus per-key stats.","tags":["Account"],"security":[{"sessionCookie":[]}],"responses":{"200":{"description":"History.","content":{"application/json":{"schema":{"type":"object","properties":{"totalGenerations":{"type":"integer"},"dailyUsage":{"type":"array","items":{"type":"object","properties":{"date":{"type":"string"},"count":{"type":"integer"}}}},"keys":{"type":"array","items":{"type":"object","properties":{"prefix":{"type":"string"},"name":{"type":"string"},"isActive":{"type":"boolean"},"lastUsedAt":{"type":"string","format":"date-time","nullable":true}}}}}}}}},"401":{"description":"Not signed in."}}}},"/account/history/memes":{"get":{"summary":"Meme history","description":"Paginated list of completed meme generations. Default 20 per page, max 50.","tags":["Account"],"security":[{"sessionCookie":[]}],"parameters":[{"name":"page","in":"query","schema":{"type":"integer","default":1}},{"name":"limit","in":"query","schema":{"type":"integer","default":20,"maximum":50}}],"responses":{"200":{"description":"Paginated memes."},"401":{"description":"Not signed in."}}}},"/account/history/inject":{"get":{"summary":"Inject job history","description":"Paginated list of past inject jobs. Default 10 per page, max 20.","tags":["Account"],"security":[{"sessionCookie":[]}],"parameters":[{"name":"page","in":"query","schema":{"type":"integer","default":1}},{"name":"limit","in":"query","schema":{"type":"integer","default":10,"maximum":20}}],"responses":{"200":{"description":"Paginated jobs."},"401":{"description":"Not signed in."}}}},"/account/logo":{"get":{"summary":"Get watermark logo","description":"Return the current logo URL and watermark settings.","tags":["Watermark"],"security":[{"sessionCookie":[]}],"responses":{"200":{"description":"Logo + settings.","content":{"application/json":{"schema":{"type":"object","properties":{"logoUrl":{"type":"string","nullable":true},"watermarkSettings":{"type":"object","properties":{"enabled":{"type":"boolean"},"position":{"type":"string","enum":["top-left","top-right","bottom-left","bottom-right"]},"size":{"type":"integer"},"opacity":{"type":"integer"}}}}}}}},"401":{"description":"Not signed in."}}},"post":{"summary":"Upload watermark logo","description":"Upload a logo file. Paid plans only. PNG, JPEG, SVG, or WebP. Max 2MB.","tags":["Watermark"],"security":[{"sessionCookie":[]}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"type":"object","required":["logo"],"properties":{"logo":{"type":"string","format":"binary"}}}}}},"responses":{"200":{"description":"Uploaded."},"400":{"description":"Invalid file or format."},"401":{"description":"Not signed in."},"403":{"description":"Free plan — upgrade to upload."}}},"delete":{"summary":"Remove watermark logo","description":"Remove the stored logo.","tags":["Watermark"],"security":[{"sessionCookie":[]}],"responses":{"204":{"description":"Removed."},"401":{"description":"Not signed in."}}}},"/account/logo/settings":{"put":{"summary":"Update watermark settings","description":"Change watermark position, size, opacity, or enable/disable. Paid plans only.","tags":["Watermark"],"security":[{"sessionCookie":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["enabled","position","size","opacity"],"properties":{"enabled":{"type":"boolean"},"position":{"type":"string","enum":["top-left","top-right","bottom-left","bottom-right"]},"size":{"type":"integer","minimum":10,"maximum":50},"opacity":{"type":"integer","minimum":10,"maximum":100}}}}}},"responses":{"200":{"description":"Updated."},"401":{"description":"Not signed in."},"403":{"description":"Free plan — upgrade to configure."}}}},"/account/billing/checkout":{"post":{"summary":"Start subscription checkout","description":"Create a Stripe Checkout session for a subscription plan. Returns the redirect URL.","tags":["Billing"],"security":[{"sessionCookie":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["plan"],"properties":{"plan":{"type":"string","enum":["starter","pro","scale","starter_annual","pro_annual","scale_annual"]}}}}}},"responses":{"200":{"description":"Checkout URL."},"400":{"description":"Plan not configured."},"401":{"description":"Not signed in."},"404":{"description":"User not found."},"503":{"description":"Billing not configured on this server."}}}},"/account/billing/topup":{"post":{"summary":"Start credit top-up","description":"Pay-as-you-go — buy credits that never expire. Returns a Stripe Checkout URL.","tags":["Billing"],"security":[{"sessionCookie":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["dollars"],"properties":{"dollars":{"type":"integer","minimum":5,"description":"USD amount. Minimum 5."}}}}}},"responses":{"200":{"description":"Checkout URL."},"401":{"description":"Not signed in."},"404":{"description":"User not found."},"503":{"description":"Billing not configured."}}}},"/account/billing/switch-plan":{"post":{"summary":"Switch plan","description":"Move an active subscription to a different plan. Stripe handles proration.","tags":["Billing"],"security":[{"sessionCookie":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["plan"],"properties":{"plan":{"type":"string","enum":["starter","pro","scale","starter_annual","pro_annual","scale_annual"]}}}}}},"responses":{"200":{"description":"Switched."},"400":{"description":"No active subscription or plan not configured."},"401":{"description":"Not signed in."},"503":{"description":"Billing not configured."}}}},"/account/billing/portal":{"post":{"summary":"Open billing portal","description":"Create a Stripe Customer Portal session for managing payment methods and subscription. Returns the URL.","tags":["Billing"],"security":[{"sessionCookie":[]}],"responses":{"200":{"description":"Portal URL."},"400":{"description":"No billing account yet."},"401":{"description":"Not signed in."},"503":{"description":"Billing not configured."}}}},"/account/billing/auto-topup":{"get":{"summary":"Get auto top-up settings","description":"Returns whether auto top-up is on, the threshold, and the top-up amount.","tags":["Billing"],"security":[{"sessionCookie":[]}],"responses":{"200":{"description":"Auto top-up config.","content":{"application/json":{"schema":{"type":"object","properties":{"enabled":{"type":"boolean"},"threshold":{"type":"integer"},"dollars":{"type":"integer"},"hasPaymentMethod":{"type":"boolean"}}}}}},"401":{"description":"Not signed in."},"404":{"description":"User not found."}}},"patch":{"summary":"Update auto top-up","description":"Enable/disable auto top-up and set threshold + amount. Enabling requires a saved payment method — complete a manual top-up first.","tags":["Billing"],"security":[{"sessionCookie":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["enabled","threshold","dollars"],"properties":{"enabled":{"type":"boolean"},"threshold":{"type":"integer","minimum":10,"description":"Trigger when credits fall below this."},"dollars":{"type":"integer","minimum":5,"description":"USD to charge each time."}}}}}},"responses":{"200":{"description":"Updated."},"400":{"description":"No saved payment method."},"401":{"description":"Not signed in."},"404":{"description":"User not found."}}}},"/webhooks/stripe":{"post":{"summary":"Stripe webhook","description":"Stripe-facing webhook. Not for manual calls. Verifies `stripe-signature` and is idempotent per event ID.","tags":["Webhooks"],"responses":{"200":{"description":"Event received."},"400":{"description":"Missing or invalid signature."},"503":{"description":"Stripe not configured on this server."}}}},"/health":{"get":{"summary":"Liveness check","description":"200 when Postgres and Redis are reachable. 503 otherwise.","tags":["Health"],"responses":{"200":{"description":"Healthy."},"503":{"description":"One or more dependencies down."}}}},"/ready":{"get":{"summary":"Readiness check","description":"200 when Postgres, Redis, and R2 are all reachable.","tags":["Health"],"responses":{"200":{"description":"Ready."},"503":{"description":"Not ready."}}}}}}