# Memes.ai API Reference > Generate on-brand ad creative — meme-style ad images, edits, short-form video, multi-slide carousels, storyboards, and brand kits — from a prompt, a website URL, and reference images. One key. One async pattern. A handful of calls to a finished campaign. **Base URL:** `https://api.memes.media/v1` **Docs:** `https://studio.memes.media/developers` **Recipes:** Use-case cookbook → [recipes.md](recipes.md) — product-feed ads, Meta creative tests, Reels video, feed-scale automation. **Status:** Beta — v1 endpoints are open for early integrations; additive fields may ship before GA. --- ## Table of contents - [Introduction](#introduction) - [Quickstart](#quickstart) - [A complete campaign](#a-complete-campaign) - [Authentication](#authentication) - [Making requests](#making-requests) - [Generations (the async model)](#generations) - [Models](#models) - [Errors](#errors) - [Webhooks](#webhooks) - **Resources** - [Images](#images) - [Memes](#memes) - [Videos](#videos) - [Carousels](#carousels) - [Storyboards](#storyboards) - [Brand Kits](#brand-kits) - [Prompt Enhancement](#prompt-enhancement) - [Images as inputs](#images-as-inputs) - [Account & Usage](#account--usage) - [SDKs & MCP](#sdks--mcp) --- ## Introduction The Memes.ai API turns a brand and an idea into finished ad creative. It is the same engine behind [studio.memes.media](https://studio.memes.media), exposed as a clean, key-authed REST API. You need a Studio account and workspace before API requests can run. Create an account at [studio.memes.media/login/register](https://studio.memes.media/login/register?next=%2Fapp%2Fdevelopers), then open the **Developers** tab in Studio to create your API key. You can: - **Generate ad images** from a prompt, a brand profile, and up to four reference images (product shots, logos, style templates). - **Generate memes** — the flagship branded-ad format, 1–4 variants per call, with full edit/reframe lineage. - **Generate video** — text-to-video and image-to-video (animate an existing asset). - **Build carousels** — a zero-credit strategy *plan* step, then a billable per-slide *render*. - **Compose storyboards** — a single 3–4 panel narrative ad image. - **Persist brand kits** — reusable brand profiles (logo, colors, voice, audience, products) that ground every generation. - **Enhance prompts** — turn a rough idea into a production-ready creative brief. Three principles run through the whole API: 1. **One async pattern.** Every creative operation returns a [Generation](#generations) — a job you poll (or receive via [webhook](#webhooks)). You never hold an HTTP connection open waiting for a render. 2. **One asset shape.** Every output — image, video, carousel slide, analysis — is a normalized [Asset](#the-asset-object). Write your handling code once. 3. **One brand context.** Pass a `brand_kit_id` (or inline `brand_context`) to any endpoint and the output is on-brand by construction. --- ## Quickstart ### 1. Create a Studio account and API key You need an AI Studio workspace before you can use the API: 1. Create a Studio account at [studio.memes.media/login/register](https://studio.memes.media/login/register?next=%2Fapp%2Fdevelopers), or sign in and open [Studio → Developers](https://studio.memes.media/app/developers). 2. In Studio, use the left sidebar: **Manage → Developers**. 3. Click **Create key**, give it a name like `Production server` or `Zapier`, and choose **Live** or **Test**. 4. Copy the secret immediately. It starts with `sk_live_…` or `sk_test_…` and is shown only once. ### 2. Generate your first meme ```bash curl https://api.memes.media/v1/memes \ -H "Authorization: Bearer sk_live_YOUR_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: 3f1c9a52-0e9b-4b8d-9a1a-1f2e3d4c5b6a" \ -d '{ "prompt": "When the coffee finally kicks in on a Monday", "brand_context": { "company_name": "Dawn Roasters", "industry": "coffee" }, "aspect_ratio": "1:1", "count": 2 }' ``` The API responds immediately with a queued **Generation**: ```json { "generation": { "id": "8f3c1c8e-7f5d-4cb8-a871-9e6d3a1b2c40", "object": "generation", "type": "meme", "status": "queued", "model": "forge-image", "requested_count": 2, "completed_count": 0, "output": [], "created_at": "2026-06-18T17:04:22Z" }, "usage": { "credits_used": 0, "credits_remaining": 480, "interval": "rolling_30_days" }, "meta": { "request_id": "req_b2c4", "livemode": true } } ``` ### 3. Poll for the result ```bash curl https://api.memes.media/v1/generations/8f3c1c8e-7f5d-4cb8-a871-9e6d3a1b2c40 \ -H "Authorization: Bearer sk_live_YOUR_KEY" ``` When `status` is `succeeded`, `output[]` holds your finished assets: ```json { "generation": { "id": "8f3c1c8e-7f5d-4cb8-a871-9e6d3a1b2c40", "type": "meme", "status": "succeeded", "completed_count": 2, "output": [ { "id": "5a7e2d91-8b44-4f30-a6c2-1d9e7b3f5a80", "type": "image", "url": "https://cdn.memes.media/a1.png", "aspect_ratio": "1:1" }, { "id": "6b8f3e02-9c55-4a41-b7d3-2e0f8c4a6b91", "type": "image", "url": "https://cdn.memes.media/a2.png", "aspect_ratio": "1:1" } ], "completed_at": "2026-06-18T17:04:41Z" }, "usage": { "credits_used": 2, "credits_remaining": 478, "interval": "rolling_30_days" }, "meta": { "request_id": "req_d4e1", "livemode": true } } ``` That's the whole loop: **submit → poll → use `output[]`**. Prefer push over polling? [Register a webhook](#webhooks) and we'll call you on `generation.completed`. --- ## A complete campaign A realistic flow: save a brand once, generate variants, then resize the winner for Stories. Notice how each id threads into the next call — a `brand_kit_id` grounds the generation, and an `output[].id` UUID becomes the input to the reframe. **1 · Save the brand once.** The returned `id` is reusable forever. ```bash curl https://api.memes.media/v1/brand-kits \ -H "Authorization: Bearer sk_live_YOUR_KEY" -H "Content-Type: application/json" \ -H "Idempotency-Key: 05c2b738-8667-4ad0-aabe-a7c61fa5b5e2" \ -d '{ "name": "Dawn Roasters", "company_name": "Dawn Roasters", "website_url": "https://dawnroasters.example", "brand_colors": ["#3B2A1A", "#E8C39E"], "tone_of_voice": "warm, witty" }' # -> { "brand_kit": { "id": "3a9f6d20-4c7b-4e18-9d52-6f1a8b3c5e70", "object": "brand_kit", ... } } ``` **2 · Generate four on-brand variants.** ```bash curl https://api.memes.media/v1/memes \ -H "Authorization: Bearer sk_live_YOUR_KEY" -H "Content-Type: application/json" \ -H "Idempotency-Key: 9b1d3e52-0e9b-4b8d-9a1a-1f2e3d4c5b6a" \ -d '{ "prompt": "POV: cold brew the night before a deadline", "brand_kit_id": "3a9f6d20-4c7b-4e18-9d52-6f1a8b3c5e70", "count": 4 }' # -> { "generation": { "id": "8f3c1c8e-7f5d-4cb8-a871-9e6d3a1b2c40", "status": "queued" } } ``` **3 · Poll until terminal.** ```bash curl https://api.memes.media/v1/generations/8f3c1c8e-7f5d-4cb8-a871-9e6d3a1b2c40 \ -H "Authorization: Bearer sk_live_YOUR_KEY" # -> status "succeeded", output: [ { "id": "5a7e2d91-8b44-4f30-a6c2-1d9e7b3f5a80", ... }, ... ] ``` **4 · Reframe the winner for Stories.** Any `output[].id` can be reused as an input. ```bash curl https://api.memes.media/v1/images/reframe \ -H "Authorization: Bearer sk_live_YOUR_KEY" -H "Content-Type: application/json" \ -H "Idempotency-Key: 62be9a8d-a5bb-4e85-b020-205018b78d0f" \ -d '{ "image_id": "5a7e2d91-8b44-4f30-a6c2-1d9e7b3f5a80", "aspect_ratio": "9:16" }' # Reframe is synchronous (fast) -> 200 with the finished asset, no polling: # -> { "generation": { "id": "4d7a2f80-3b95-4e61-a8c2-5f0d9b7e3a14", "type": "reframe", "status": "succeeded", "output": [ { "id": "7c2e4a91-6d80-4f35-b1a7-9e3d5b8f2c60", ... } ] } } ``` From here, [animate](#videos) the winner into video (`POST /videos/animate`), or fan out a [carousel](#carousels) from the same brand kit. Every output asset is reusable as the next call's input. --- ## Authentication The API authenticates with secret keys sent as a bearer token. If you do not have a key yet, create a Studio account or open [Studio → Developers](https://studio.memes.media/app/developers). ```bash curl https://api.memes.media/v1/account \ -H "Authorization: Bearer sk_live_YOUR_KEY" ``` | | | |---|---| | **Header** | `Authorization: Bearer ` | | **Live keys** | `sk_live_` + 48 lowercase hexadecimal characters — bill and affect your live workspace | | **Test keys** | `sk_test_` + 48 lowercase hexadecimal characters — use the deterministic, provider-free integration simulator | | **Scopes** | Read methods require `read`; mutations require `write`. | | **Visibility** | The full secret is shown once at creation. Store only the key; we store a one-way hash. | | **Rotation** | Create a new key, deploy it, then revoke the old one. Revocation is immediate. | Every key belongs to a **workspace**. The `livemode` boolean on every response tells you whether a live or test key was used. Test keys are intercepted before every live generator and the live request ledger. They use short-lived state isolated to the exact API key, never call generation or webhook-delivery providers, never spend credits or quota, and never read or mutate canonical workspace resources. Simulated generations progress from queued to completed fixture output in about one second; responses include `livemode: false` and `meta.simulated: true`, and fixture runs and webhook endpoints expire after one hour. Expired rows are physically cleaned in bounded batches, and each test key is capped at 2,000 active runs and 25 active webhook endpoints. Image, meme, edit, reframe, storyboard, carousel, video, prompt-enhancement, URL-analysis, polling, cancellation, asset reads/deletes, and webhook-management flows are simulated. Brand Kit mutation operations that cannot be represented without touching canonical workspace data return `501 test_mode_not_supported`; a test request never falls through to the live handler. Test webhook delivery returns the event that would be sent but makes no outbound request. Test assets are dedicated local fixtures, never discounted live generations. > **Keep keys server-side.** Secret keys grant full access to a workspace's generation and billing. Never embed them in browser, mobile, or other client-side code. If a key leaks, revoke it immediately. `401 authentication_error` is returned for a missing, malformed, expired, or revoked key. --- ## Making requests ### Base URL All requests go to `https://api.memes.media/v1`. ### Request & response format Requests are JSON (`Content-Type: application/json`) with a 24 MiB maximum body. Remote image inputs are fetched through a public-network-only, redirect-bounded, byte-bounded, dimension-bounded deadline. Responses are JSON and always include a `meta` object: ```json "meta": { "request_id": "req_b2c4f9", "livemode": true } ``` Include the `request_id` in any support conversation — it pins the exact call. ### Idempotency Every `POST` requires an `Idempotency-Key` header. Generate one UUID per logical operation and retain it before sending the request: ```bash -H "Idempotency-Key: 3f1c9a52-0e9b-4b8d-9a1a-1f2e3d4c5b6a" ``` For live keys, POST endpoints use durable, body-fingerprinted admission. The general 24-hour request ledger scopes a key to the API key, method, path, and request body: the same request replays the stored outcome; a changed body returns `409 idempotency_conflict`; an attempt still running returns `409 idempotency_in_progress`. Video creation and animation use their own transactional fingerprinted admission and return `409 idempotency_key_conflict` for changed-payload reuse. Webhook creation is retry-safe too: the ledger derives a stable external endpoint identity and persists only redacted response data. A same-key retry recovers the same endpoint and signing secret; a later `GET /webhooks/{id}` never exposes that secret. Test-mode identities are deterministic from the API key, operation, body, and `Idempotency-Key`. Same-key/same-body retries return the same fixture; changed-body reuse returns `409 idempotency_key_conflict`. Live generation and asset ids are UUIDs. Simulator ids remain visibly isolated as `gen_test_<32 hex>`, `asset_test_<32 hex>_`, and `ep_test_<32 hex>`. ### Credits, entitlements & rate limits Billable generation endpoints return `429 quota_exceeded` when the workspace lacks credits. Live provider-backed operations—including zero-credit planning, prompt enhancement, and URL analysis—require an eligible paid workspace by default and return `403 plan_feature_unavailable` otherwise. Test keys never consult live subscription state because their simulator makes no provider calls. Every request also consumes weighted points in fixed windows across API-key, workspace, and client-IP buckets. Expensive operations hold a separate weighted concurrency lease. Successful and rejected requests expose the most constrained bucket through `RateLimit-Limit`, `RateLimit-Remaining`, and `RateLimit-Reset`; rejected requests also send `Retry-After`. Handle `429 rate_limited`, `authentication_rate_limited`, `concurrency_limited`, and `test_mode_capacity_limited` with bounded exponential backoff and jitter. These controls are independent of credit quota. Inline `brand_context` objects use the same limits as Brand Kit fields: company names up to 200 characters, industry up to 160, website URLs up to 2,048, website context up to 8,000, tone and audience up to 2,000 each, and at most 16 valid hexadecimal colors. Any supplied `brand_kit_id` must be a workspace-owned UUID; an invalid, missing, or non-owned kit returns `404` before reservation or provider work. If both are supplied, inline text wins, but the saved kit is still validated because its logo and product references may be attached automatically. ### Pagination List endpoints are cursor-paginated: ```bash curl "https://api.memes.media/v1/memes?limit=40&starting_after=6b8f3e02-9c55-4a41-b7d3-2e0f8c4a6b91" \ -H "Authorization: Bearer sk_live_YOUR_KEY" ``` ```json { "data": [ ... ], "has_more": true, "next_cursor": "7c2e4a91-6d80-4f35-b1a7-9e3d5b8f2c60" } ``` | Param | Type | Description | |---|---|---| | `limit` | integer | Page size, 1–100. Default 40. | | `starting_after` | string | Object id to start after (from `next_cursor`). | | `q` | string | Free-text search, ≤200 chars (where supported). | | `order` | string | `newest` (default) or `oldest`. | The cursor is always the `id` of the last object in the page you received — pass it as `starting_after` to fetch the next page, and stop when `has_more` is `false`. --- ## Endpoint index This index is generated from [`openapi.yaml`](openapi.yaml), the machine-readable source of truth. ### Generations | Method | Path | Purpose | |---|---|---| | `GET` | `/generations` | List generations | | `GET` | `/generations/{id}` | Retrieve a generation | | `POST` | `/generations/{id}/cancel` | Cancel a generation | ### Images | Method | Path | Purpose | |---|---|---| | `GET` | `/images` | List images | | `POST` | `/images` | Generate an image | | `GET` | `/images/{id}` | Retrieve an image | | `POST` | `/images/edits` | Edit an image | | `POST` | `/images/reframe` | Reframe an image | ### Memes | Method | Path | Purpose | |---|---|---| | `GET` | `/memes` | List memes | | `POST` | `/memes` | Generate a meme | | `GET` | `/memes/{id}` | Retrieve a meme | | `DELETE` | `/memes/{id}` | Delete a meme | | `GET` | `/memes/{id}/lineage` | Retrieve meme lineage | ### Videos | Method | Path | Purpose | |---|---|---| | `GET` | `/videos` | List videos | | `POST` | `/videos` | Generate a video | | `POST` | `/videos/animate` | Animate an image into a video | | `GET` | `/videos/{id}` | Retrieve a video | | `DELETE` | `/videos/{id}` | Delete a video | ### Carousels | Method | Path | Purpose | |---|---|---| | `POST` | `/carousels/plan` | Plan a carousel | | `GET` | `/carousels` | List carousels | | `POST` | `/carousels` | Render a carousel | | `GET` | `/carousels/{id}` | Retrieve a carousel | | `DELETE` | `/carousels/{id}` | Delete a carousel | ### Storyboards | Method | Path | Purpose | |---|---|---| | `POST` | `/storyboards` | Generate a storyboard | | `GET` | `/storyboards/{id}` | Retrieve a storyboard | ### Brand Kits | Method | Path | Purpose | |---|---|---| | `GET` | `/brand-kits` | List brand kits | | `POST` | `/brand-kits` | Create a brand kit | | `GET` | `/brand-kits/{id}` | Retrieve a brand kit | | `PATCH` | `/brand-kits/{id}` | Update a brand kit | | `DELETE` | `/brand-kits/{id}` | Delete a brand kit | | `POST` | `/brand-kits/{id}/set-default` | Set a brand kit as default | | `POST` | `/brand-kits/{id}/logo` | Upload a brand kit logo | | `DELETE` | `/brand-kits/{id}/logo` | Remove a brand kit logo | | `POST` | `/brand-kits/{id}/product-references` | Add a product reference | | `DELETE` | `/brand-kits/{id}/product-references/{ref_id}` | Remove a product reference | | `POST` | `/brand-kits/analyze-url` | Extract a brand profile from a URL | ### Prompt Enhancement | Method | Path | Purpose | |---|---|---| | `POST` | `/prompts/enhance` | Enhance a prompt | ### Webhooks | Method | Path | Purpose | |---|---|---| | `GET` | `/webhooks` | List webhook endpoints | | `POST` | `/webhooks` | Register a webhook endpoint | | `GET` | `/webhooks/{id}` | Retrieve a webhook endpoint | | `PATCH` | `/webhooks/{id}` | Update a webhook endpoint | | `DELETE` | `/webhooks/{id}` | Delete a webhook endpoint | | `POST` | `/webhooks/{id}/test` | Send a test event | ### Account | Method | Path | Purpose | |---|---|---| | `GET` | `/account` | Retrieve account | | `GET` | `/account/usage` | Retrieve usage | ## Generations A **Generation** is the single resource for every long-running operation. Whether you're rendering one image or a ten-slide carousel, you get back a Generation and read results from it. ### Lifecycle ``` POST /v1/ -> 202 { generation: { status: "queued" } } GET /v1/generations/{id} -> 200 { generation: { status, output } } webhook generation.completed -> push (optional) ``` ### Status values | Status | Meaning | |---|---| | `queued` | Accepted and credits reserved; not started yet. | | `processing` | Actively rendering. | | `succeeded` | All requested outputs produced. | | `partial` | Some of N outputs succeeded (e.g. 3 of 4 variants); unfilled credits are refunded. | | `failed` | Terminal failure; reserved credits are refunded. | | `canceled` | Canceled before completion. | `succeeded`, `partial`, `failed`, and `canceled` are terminal. Stop polling once you reach one. ### Credits Credits are **reserved when a job is queued** and **settle when it completes** — you're charged only for delivered outputs, and reserved credits are refunded on `failed`, `canceled`, or the unfilled portion of a `partial`. That's why a job shows `credits_used: 0` at submit and the final figure once terminal. Every response carries a `usage` block: ```json "usage": { "credits_used": 2, "credits_remaining": 478, "interval": "rolling_30_days", "reset_at": "2026-07-01T00:00:00Z" } ``` ### Paid API plans Your API key inherits the paid plan on the Studio workspace that created it. The standard ad-generation bucket is reported as `images` by `GET /account/usage`; video has its own monthly bucket. | Plan | Ad generations / rolling 30 days | Video generations / rolling 30 days | |---|---:|---:| | Starter | 100 | 5 | | Pro | 250 | 15 | | Business | 500 | 30 | | Elite | 1,000 | 60 | Need a higher monthly allowance, custom throughput, or a managed plan? Email [members@memes.com](mailto:members@memes.com). ### The Generation object ```json { "id": "8f3c1c8e-7f5d-4cb8-a871-9e6d3a1b2c40", "object": "generation", "type": "meme", "status": "succeeded", "model": "forge-image", "requested_count": 4, "completed_count": 4, "failed_count": 0, "output": [ /* Asset objects */ ], "error": null, "created_at": "2026-06-18T17:04:22Z", "completed_at": "2026-06-18T17:04:53Z" } ``` `type` is one of `meme`, `image`, `edit`, `reframe`, `video`, `carousel`, `storyboard`. ### The Asset object Every output, for every endpoint, has the same shape: ```json { "id": "5f2b1c34-7a90-4c0e-9b21-2e6f3a8d1c44", "type": "image", "url": "https://cdn.memes.media/a1.png", "aspect_ratio": "1:1", "slide_index": null, "parent_id": null, "model": "forge-image", "created_at": "2026-06-18T17:04:41Z" } ``` `type` is `image`, `video`, `carousel_slide`, or `analysis`. `slide_index` is set for carousel slides; `parent_id` links edits/reframes back to their source. Live generation, asset, Brand Kit, product-reference, carousel-plan, and carousel-set ids are UUID strings. Test-only generations and assets retain their explicit `gen_test_…` / `asset_test_…` forms. Pixel `width`/`height` and `thumbnail_url` are not currently returned; `aspect_ratio` is the size hint. ### Endpoints | Method · Path | Purpose | |---|---| | `GET /generations` | List jobs. Filter by `type` and `status`. | | `GET /generations/{id}` | Poll any job; returns the Generation with normalized `output[]`. | | `POST /generations/{id}/cancel` | Cancel a `queued` or `processing` job. | ```bash curl https://api.memes.media/v1/generations/8f3c1c8e-7f5d-4cb8-a871-9e6d3a1b2c40 \ -H "Authorization: Bearer sk_live_YOUR_KEY" ``` **Polling guidance:** poll every 2–3 seconds and stop at the first terminal status. Images and memes typically finish in 10–30s, video in 1–4 minutes. For production, prefer [webhooks](#webhooks). --- ## Models Choose a model with the `model` parameter, or omit it to use the smart default for each endpoint. Models belong to the **Forge** family — each named by capability, with a faster `-fast` variant where one is offered. | Model | Best for | |---|---| | `forge-image` | Primary text-to-image ad creative; reference, product, and logo conditioning. *(default for Images & Memes)* | | `forge-image-fast` | Faster, cheaper image generation — quick variants and iteration. | | `forge-edit` | Instruction edits, mask inpainting, and aspect-ratio reframing. | | `forge-video` | Text-to-video and multi-reference (2–4 image) video. | | `forge-motion` | Image-to-video — animate a single existing asset. | | `forge-prompt` | Prompt enhancement — rough idea to production-ready brief. | | `forge-plan` | Carousel strategy planning. | | `forge-copy-fast` | Fast brand/creative analysis (e.g. URL analysis). | Most endpoints pick the right model for you — you only set `model` to choose between `forge-image` and `forge-image-fast`. Models route to the best available engine internally and fail over automatically; your code references only the stable Forge id. We may improve the engine behind a Forge id at any time — the id and its contract stay constant. --- ## Errors The API uses conventional HTTP status codes and returns a single error envelope: ```json { "error": { "type": "invalid_request_error", "code": "invalid_request", "message": "Either prompt, brand_context, or reference_images is required.", "param": "prompt", "request_id": "req_8f2a1c" }, "meta": { "request_id": "req_8f2a1c", "livemode": true } } ``` | HTTP | `type` | Common `code`s | When | |---|---|---|---| | `400` | `invalid_request_error` | `invalid_request`, `idempotency_key_invalid`, `idempotency_key_required` | Malformed/missing input or retry key. | | `401` | `authentication_error` | `invalid_api_key` | Missing, invalid, expired, or revoked key. | | `403` | `permission_error` | `insufficient_scope`, `plan_feature_unavailable` | Key scope or live-workspace entitlement is insufficient. | | `404` | `not_found_error` | `resource_not_found`, `test_resource_not_found` | Unknown/unowned live id or an expired/unknown simulator fixture. | | `409` | `conflict_error` | `idempotency_conflict`, `idempotency_key_conflict`, `idempotency_in_progress`, `idempotency_outcome_unknown`, `brand_kit_limit_reached`, `webhook_endpoint_limit_reached`, `conflict` | Retry-key conflict/in-flight state, resource cap, or rejected cancellation. | | `429` | `quota_error` / `rate_limit_error` | `quota_exceeded`, `rate_limited`, `authentication_rate_limited`, `concurrency_limited`, `test_mode_capacity_limited` | Credit quota, fixed-window request points, concurrency, or simulator capacity exhausted. | | `500` | `api_error` | `internal_error`, `test_mode_internal_error` | Unexpected error on our side. | | `501` | `test_mode_error` | `test_mode_not_supported` | This mutation has no isolated simulator representation. | | `502` | `generation_error` / `api_error` | `generation_failed`, `webhook_provider_error` | A synchronous dependency request failed. | | `503` | `service_unavailable` / `api_error` / `generation_error` | `temporarily_unavailable`, `webhook_provider_unavailable`, `idempotency_outcome_unknown`, `video_admission_unavailable`, `queue_full` | Admission, persistence, webhook configuration, or capacity is unavailable. | `error.request_id`, `meta.request_id`, and the `X-Request-Id` response header always match. Async rendering failures appear on the generation resource as `generation.error.code=generation_failed`, not as a `422` response to the create call. For `409 idempotency_in_progress`, honor `Retry-After` and retry the same key/body. For transient `503`, use exponential backoff and the same key. For `429`, branch on `error.code`: rate/capacity errors are retryable after `Retry-After`; `quota_exceeded` requires credits or a quota reset. --- ## Webhooks Rather than polling, register an HTTPS endpoint and we'll POST a signed event when a job reaches a terminal state. ### Events | Event | Fires when | |---|---| | `generation.completed` | A job `succeeded`. | | `generation.partial` | A job finished with some outputs (`partial`). | | `generation.failed` | A job `failed` or timed out. | | `generation.canceled` | A job was canceled. | ### Payload ```json { "id": "evt_2x9k", "type": "generation.completed", "created_at": "2026-06-18T17:04:53Z", "livemode": true, "data": { "generation": { /* full Generation object */ } } } ``` ### Verifying signatures Every delivery is signed with your endpoint's secret (`whsec_…`, shown once when the endpoint is created) and carries three headers: `svix-id`, `svix-timestamp`, and `svix-signature`. The simplest, recommended way to verify is the official libraries — available for JavaScript/TypeScript, Python, Go, Rust, Ruby, PHP, Java, C#, Kotlin, and Elixir: ```js import { Webhook } from "svix"; const wh = new Webhook(process.env.MEMES_WEBHOOK_SECRET); // whsec_… // Pass the RAW request body and the svix-* headers. const event = wh.verify(rawBody, { "svix-id": req.headers["svix-id"], "svix-timestamp": req.headers["svix-timestamp"], "svix-signature": req.headers["svix-signature"], }); // throws on an invalid signature or a stale timestamp; returns the parsed event ``` ```bash bun add svix # or: pip install svix ``` To verify manually: `svix-signature` is a space-separated list of `v1,` signatures (a list so secrets can rotate), where each is `HMAC-SHA256(secret, "{svix-id}.{svix-timestamp}.{raw_body}")` and `secret` is the base64 bytes following the `whsec_` prefix. Reject the request if `|now − svix-timestamp| > 5 minutes` (replay protection). Deliveries are **at-least-once** — dedupe on `svix-id` (or the event `id`). Failed deliveries retry with exponential backoff for ~24 hours, and every attempt is logged and replayable from your webhook portal. Webhook destinations must use a public HTTPS URL. Each workspace may register at most 25 endpoints, including disabled endpoints. `GET /webhooks` and `GET /webhooks/{id}` are side-effect free and never provision webhook state. Only idempotent endpoint creation provisions it. ### Managing endpoints | Method · Path | Purpose | |---|---| | `GET /webhooks` | List endpoints. | | `POST /webhooks` | Register an endpoint; returns the signing secret on its idempotent creation lineage. | | `GET /webhooks/{id}` | Fetch an endpoint. | | `PATCH /webhooks/{id}` | Update url, events, or enabled state. | | `DELETE /webhooks/{id}` | Remove an endpoint. | | `POST /webhooks/{id}/test` | Send a test event. | ```bash curl https://api.memes.media/v1/webhooks \ -H "Authorization: Bearer sk_live_YOUR_KEY" \ -H "Idempotency-Key: webhook-primary-v1" \ -H "Content-Type: application/json" \ -d '{ "url": "https://example.com/hooks/memes", "events": ["generation.completed", "generation.failed"] }' ``` The original successful create and a same-key/same-body recovery retry return the same endpoint and signing secret. Store the secret immediately; ordinary endpoint reads never expose it. Creating endpoint 26 returns `409 webhook_endpoint_limit_reached`. --- ## Images Generate and edit ad images. Pass a prompt, a brand context, and up to four reference images. | Method · Path | Purpose | |---|---| | `POST /images` | Text-to-image / reference-conditioned ad image (1–4). | | `POST /images/edits` | Instruction edit or mask inpaint → a child asset. | | `POST /images/reframe` | Outpaint to a new aspect ratio. | | `GET /images` | List generated images. | | `GET /images/{id}` | Fetch one image asset. | ### Create an image `POST /images` | Param | Type | Required | Description | |---|---|---|---| | `prompt` | string | one of* | Creative direction, max 4,000 characters. | | `business_name` | string | no | Brand/business name, max 200 characters. | | `brand_kit_id` | string | one of* | A saved [brand kit](#brand-kits) to ground the image. | | `brand_context` | object | one of* | Inline brand profile if you don't have a kit. | | `template_prompt` | string | no | Optional format/template directive, max 4,000 characters. | | `reference_images` | array | no | Up to 4 `{ type: "ref" \| "template" \| "logo", source }`, where `source` is an [https URL or a workspace asset id](#images-as-inputs). | | `aspect_ratio` | enum | no | `auto`, `1:1`, `2:1`, `3:1`, `2:3`, `3:2`, `3:4`, `4:5`, `4:3`, `16:9`, `9:16`, `21:9`. Default `1:1`. | | `count` | integer | no | Variants, 1–4. Default 1. | | `quality` | enum | no | `fast`, `standard` (default), `premium`. | | `model` | enum | no | `forge-image` (default) or `forge-image-fast`. | | `controls` | object | no | Advanced fine-tuning knobs (`platform`, `visual_taste`, `product_role`, `brand_strictness`, `copy_density`, `reference_strength`, `risk_level`). Optional; accepted values are evolving during Preview. | \* At least one of `prompt`, `brand_kit_id` / `brand_context`, or `reference_images`. > **Reuse outputs as inputs.** Any live UUID in `output[].id` (or a test-key `asset_test_…` id) can be passed as a `reference_images[].source`, an `image_id` for edits/reframes, or an `image_id` to [animate](#videos) — so you can iterate on generated creative without re-uploading. ```bash curl https://api.memes.media/v1/images \ -H "Authorization: Bearer sk_live_YOUR_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: b2693cf4-1383-4dc9-8b96-8bf2ddd58ea8" \ -d '{ "prompt": "Minimalist product hero on a sunlit kitchen counter", "brand_kit_id": "3a9f6d20-4c7b-4e18-9d52-6f1a8b3c5e70", "reference_images": [{ "type": "logo", "source": "https://yourcdn.example/logo.png" }], "aspect_ratio": "4:5", "count": 3 }' ``` Returns `202` with a Generation (`type: "image"`). Credits: 1 per delivered image. ### Edit an image `POST /images/edits` — apply a natural-language instruction, optionally within a mask. | Param | Type | Required | Description | |---|---|---|---| | `image_id` | string | yes | The asset to edit. | | `instruction` | string | yes | What to change, ≤2000 chars. | | `mask` | string | no | A PNG mask as a `data:` URL; enables inpainting within the masked region. | | `reference_images` | array | no | Up to 4 references. | | `aspect_ratio` | enum | no | Output ratio. | Returns `202` (`type: "edit"`). The new asset's `parent_id` points to `image_id`. Credits: 1. ### Reframe an image `POST /images/reframe` — outpaint into a new aspect ratio. | Param | Type | Required | Description | |---|---|---|---| | `image_id` | string | yes | The asset to reframe. | | `aspect_ratio` | enum | yes | One of `9:16`, `16:9`, `1:1`, `4:5`, `2:3`, `3:2`. Must differ from the source. | Live keys return `200` with a completed `type: "reframe"` generation. Test keys return `202` and progress the deterministic fixture asynchronously. Credits: 1 for live work; test fixtures use none. --- ## Memes Memes are the flagship branded-ad format: a brand-grounded image with full edit/reframe lineage. Functionally, Memes are Images with meme defaults plus lineage fields. | Method · Path | Purpose | |---|---| | `POST /memes` | Generate 1–4 brand-grounded meme ads. | | `GET /memes` | List memes (search, exclude carousel-derived). | | `GET /memes/{id}` | Fetch one meme. | | `GET /memes/{id}/lineage` | Full edit/reframe family tree. | | `DELETE /memes/{id}` | Soft-delete a meme. | ### Create memes `POST /memes` | Param | Type | Required | Description | |---|---|---|---| | `prompt` | string | one of* | The joke, angle, or message, max 4,000 characters. | | `business_name` | string | no | Brand/business name, max 200 characters. | | `brand_kit_id` | string | one of* | Brand kit to ground with. | | `brand_context` | object | one of* | Inline brand profile. | | `template_prompt` | string | no | A meme-template directive to anchor the format, max 4,000 characters. | | `reference_images` | array | no | Up to 4 typed references (`ref`/`template`/`logo`). | | `aspect_ratio` | enum | no | Default `1:1`. | | `count` | integer | no | 1–4. Default 1. | | `enhance` | boolean | no | Run [prompt enhancement](#prompt-enhancement) first. | \* At least one of `prompt`, `brand_kit_id` / `brand_context`, `template_prompt`, or `reference_images`. ```bash curl https://api.memes.media/v1/memes \ -H "Authorization: Bearer sk_live_YOUR_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: 5884509e-3312-4547-a89c-b2773d20da3e" \ -d '{ "prompt": "POV: you discover cold brew the night before a deadline", "brand_kit_id": "3a9f6d20-4c7b-4e18-9d52-6f1a8b3c5e70", "aspect_ratio": "1:1", "count": 4, "enhance": true }' ``` Returns `202` (`type: "meme"`, `model: "forge-image"`). Credits: 1 per delivered variant. ### Meme lineage `GET /memes/{id}/lineage` returns the root meme plus every edit and reframe derived from it, in chronological order — the data behind an edit-history view. --- ## Videos Generate short-form video from text, references, or an existing image. | Method · Path | Purpose | |---|---| | `POST /videos` | Text-to-video or reference-to-video. | | `POST /videos/animate` | Image-to-video from an existing asset. | | `GET /videos` | List video jobs. | | `GET /videos/{id}` | Fetch one video. | | `DELETE /videos/{id}` | Delete a video (refunds its credits). | ### Create a video `POST /videos` | Param | Type | Required | Description | |---|---|---|---| | `prompt` | string | one of* | What the video shows, max 4,000 characters. | | `business_name` | string | no | Brand/business name, max 200 characters. | | `business_url` | string | one of* | A website to ground the concept in, max 2,048 characters. | | `brand_kit_id` / `brand_context` | string / object | one of* | Brand grounding. | | `reference_images` | array | no | Up to 4 `ref` or `logo` references. Each source must be a public HTTPS URL or owned workspace asset id; video rejects data URIs and `template` references. Their presence routes to reference-to-video. | | `aspect_ratio` | enum | no | `1:1`, `16:9` (default), `9:16`, `4:3`, `3:4`, `3:2`, `2:3`. | | `duration` | integer | no | Seconds, 1–15. Default 5. | | `resolution` | enum | no | `480p` or `720p` (default). | \* At least one of `prompt`, `business_url`, or `brand_kit_id` / `brand_context`. The model (`forge-video` or `forge-motion`) is selected automatically from your inputs — you don't set it. ```bash curl https://api.memes.media/v1/videos \ -H "Authorization: Bearer sk_live_YOUR_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: c733a729-4050-4b3b-97d4-431056e865ab" \ -d '{ "prompt": "A latte being poured in slow motion, steam rising, warm morning light", "brand_kit_id": "3a9f6d20-4c7b-4e18-9d52-6f1a8b3c5e70", "aspect_ratio": "9:16", "duration": 6 }' ``` Returns `202` (`type: "video"`). Credits: 10 per completed video. Video typically completes in 1–4 minutes — use a [webhook](#webhooks). ### Animate an image `POST /videos/animate` | Param | Type | Required | Description | |---|---|---|---| | `image_id` | string | yes | A workspace-owned image asset to animate. | Runs at 5s / 720p with `forge-motion`. Returns `202` (`type: "video"`). --- ## Carousels Carousels are two steps by design: a **zero-credit** strategy plan, then a billable render that fans out one image per slide. Both live POSTs require the paid public-API entitlement. | Method · Path | Purpose | |---|---| | `POST /carousels/plan` | Generate slide strategy (no images, zero credits). | | `POST /carousels` | Render: persist the set and fan out N slide jobs. | | `GET /carousels` | List carousel sets with hydrated slides. | | `GET /carousels/{id}` | One set with per-slide status and URLs. | | `DELETE /carousels/{id}` | Soft-delete a set. | ### Plan a carousel `POST /carousels/plan` | Param | Type | Required | Description | |---|---|---|---| | `brief` | string | yes | What the carousel should accomplish, ≤4000 chars. | | `business_website` | string | no | URL to ground the plan, max 2,048 characters. | | `goal` | enum | no | `awareness`, `leads` (default), `sales`, `followers`, `engagement`. | | `platform` | enum | no | `ig-portrait` (default), `ig-square`, `linkedin`, `tiktok`. | | `aspect_ratio` | enum | no | Slide ratio. | | `slide_count` | integer | no | 3–10. Default 5. | | `brand_kit_id` / `brand_context` | string / object | no | Brand grounding. | ```bash curl https://api.memes.media/v1/carousels/plan \ -H "Authorization: Bearer sk_live_YOUR_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: 4859f5ac-04fe-4790-8437-cce026a4bff8" \ -d '{ "brief": "5 myths about cold brew, debunked, for first-time buyers", "goal": "leads", "platform": "ig-portrait", "slide_count": 5, "brand_kit_id": "3a9f6d20-4c7b-4e18-9d52-6f1a8b3c5e70" }' ``` Returns `200` synchronously (planning uses zero credits) with a plan: ```json { "plan_id": "9e2b7c41-5d8a-4f30-b6c9-1a7e3d5f8b20", "plan": { "id": "9e2b7c41-5d8a-4f30-b6c9-1a7e3d5f8b20", "concept_title": "Cold Brew Myths, Busted", "viral_mechanic": "myth/fact reveal", "audience": "first-time cold brew buyers", "cta": "Try the starter kit", "slides": [ { "slide_index": 0, "role": "hook", "on_image_text": "5 cold brew myths", "visual_direction": "..." } ] }, "model": "forge-plan", "meta": { "request_id": "req_b2c4f9", "livemode": true } } ``` ### Render a carousel `POST /carousels` accepts a `plan_id` from `POST /carousels/plan`, an inline `plan` with 3–10 slides and canonical JSON no larger than 128 KiB, or enough bounded planning inputs to create a plan. It also accepts optional `quality`, `controls`, and up to four `reference_images`. Returns `202` with a `carousel_set` whose UUID is the pollable set id. Poll `GET /carousels/{id}`; each slide's `asset` fills in as it completes (and each slide id is independently pollable via `GET /generations/{id}`). Credits: 1 × `slide_count`. --- ## Storyboards A storyboard is a single composite 3–4 panel narrative ad image (not a fan-out). | Method · Path | Purpose | |---|---| | `POST /storyboards` | Generate one multi-panel storyboard image. | | `GET /storyboards/{id}` | Fetch the result. | `POST /storyboards` | Param | Type | Required | Description | |---|---|---|---| | `goal` | string | one of* | What the story should land, max 4,000 characters. | | `business_name` | string | no | Brand/business name, max 200 characters. | | `brand_kit_id` / `brand_context` | string / object | one of* | Brand grounding. | | `reference_images` | array | no | Typed references. | | `aspect_ratio` | enum | no | `16:9`, `9:16`, `1:1`. | | `preset` | enum | no | `auto`, `comic-strip`, `problem-solution`, `before-after`, `how-it-works`, `customer-journey`, `testimonial-sequence`, `founder-story`. | | `panel_count` | integer | no | 3 or 4. Default 3. | | `tone` | enum | no | Narrative tone, e.g. `playful`, `bold`, `warm`, `professional`. | | `text_density` | enum | no | `minimal`, `balanced`, `rich`. | | `product_role` | enum | no | How prominently the product features. | | `visual_style` | enum | no | `comic-strip` or `clean-storyboard`. | Returns `202` (`type: "storyboard"`). Credits: 4. --- ## Brand Kits Brand kits are reusable brand profiles — logo, colors, voice, audience, products — that ground any generation. Kit persistence uses zero credits; public API creation is rejected once the workspace has 50 total Brand Kits. Live URL analysis and remote logo/product ingestion require the paid public-API entitlement because they perform external work. | Method · Path | Purpose | |---|---| | `GET /brand-kits` | List kits. | | `POST /brand-kits` | Create a kit. | | `GET /brand-kits/{id}` | Fetch a kit. | | `PATCH /brand-kits/{id}` | Partial update. | | `DELETE /brand-kits/{id}` | Delete a kit. | | `POST /brand-kits/{id}/logo` | Upload or replace the logo. | | `DELETE /brand-kits/{id}/logo` | Remove the logo. | | `POST /brand-kits/{id}/product-references` | Add a product reference (max 5). | | `DELETE /brand-kits/{id}/product-references/{ref_id}` | Remove one. | | `POST /brand-kits/{id}/set-default` | Mark the default kit. | | `POST /brand-kits/analyze-url` | Extract a brand profile from a URL (does not persist). | > **No public brand-audit endpoint in v1.** `analyze-url` only extracts brand-kit profile fields; it does not create or publish brand audit reports. ### Create a brand kit `POST /brand-kits` | Param | Type | Required | Description | |---|---|---|---| | `name` | string | yes | A non-empty label, max 160 characters. | | `company_name` | string | no | Brand name, max 200 characters. | | `website_url` | string | no | Brand site, max 2,048 characters. | | `website_context` | string | no | Notes/positioning, max 8,000 characters. | | `brand_colors` | string[] | no | Up to 16 three-, four-, six-, or eight-digit hexadecimal colors including `#`. | | `tone_of_voice` | string | no | Voice description, max 2,000 characters. | | `target_audience` | string | no | Audience description, max 2,000 characters. | | `industry` | string | no | Industry, max 160 characters. | | `auto_attach_product_references` | boolean | no | Auto-pull product images from the site. Default `true`. | ```bash curl https://api.memes.media/v1/brand-kits \ -H "Authorization: Bearer sk_live_YOUR_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: 652c1f3c-4ee0-47dc-971e-858615a4be4f" \ -d '{ "name": "Dawn Roasters", "company_name": "Dawn Roasters", "website_url": "https://dawnroasters.example", "brand_colors": ["#3B2A1A", "#E8C39E"], "tone_of_voice": "warm, witty, unpretentious", "target_audience": "remote workers who care about good coffee" }' ``` Returns the created kit — the `id` is what you pass as `brand_kit_id` everywhere else: ```json { "brand_kit": { "id": "3a9f6d20-4c7b-4e18-9d52-6f1a8b3c5e70", "object": "brand_kit", "name": "Dawn Roasters", "is_default": false, "created_at": "2026-06-18T17:00:00Z" } } ``` ### Analyze a URL `POST /brand-kits/analyze-url` with `{ "url": "https://..." }` returns an extracted profile (`company_name`, `industry`, `tone_of_voice`, `brand_colors`, `target_audience`, `website_context`) without saving it — useful for pre-filling a kit, not for running a public audit. Synchronous `200`, `model: forge-copy-fast`. --- ## Prompt Enhancement Turn a rough idea into a polished, production-ready creative prompt. | Method · Path | Purpose | |---|---| | `POST /prompts/enhance` | Rough idea → polished image prompt. | `POST /prompts/enhance` | Param | Type | Required | Description | |---|---|---|---| | `prompt` | string | yes | The rough idea, max 4,000 characters. | | `website_url` | string | no | Site to ground in. | | `has_logo` | boolean | no | Whether you'll attach a logo. | | `has_reference_image` | boolean | no | Whether you'll attach references. | | `aspect_ratio` | enum | no | Intended output ratio. | | `reference_images` | array | no | https URLs or workspace asset ids to consider. | ```bash curl https://api.memes.media/v1/prompts/enhance \ -H "Authorization: Bearer sk_live_YOUR_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: 30b652c3-4af8-445f-8266-e24d07f392e9" \ -d '{ "prompt": "funny ad about mondays and coffee", "website_url": "https://dawnroasters.example" }' ``` Returns `200` synchronously: `{ "enhanced_prompt": "...", "original_prompt": "...", "model": "forge-prompt" }`. --- ## Images as inputs Pass an `https` URL, data URI, or workspace asset id as a reference or edit source; the API validates and normalizes it server-side. Edit masks also accept a data URI. Video references are stricter: public HTTPS or an owned workspace asset id only, with `ref` or `logo` type. A `source` (in `reference_images[]`) or an `image_id` accepts: - **An `https` URL** — your own hosted asset URL, or any public image URL. - **A workspace asset id** — any live UUID returned in a previous generation's `output[]` (or a test-key `asset_test_…` id). This lets you iterate: feed generated creative straight back in as a reference, edit source, or animate source. ```jsonc "reference_images": [ { "type": "logo", "source": "https://yourcdn.example/logo.png" }, { "type": "ref", "source": "5a7e2d91-8b44-4f30-a6c2-1d9e7b3f5a80" } // prior live output ] ``` --- ## Account & Usage | Method · Path | Purpose | |---|---| | `GET /account` | Workspace info, plan, key prefix, livemode. | | `GET /account/usage` | Credit consumption and quota by interval. | ```bash curl https://api.memes.media/v1/account/usage \ -H "Authorization: Bearer sk_live_YOUR_KEY" ``` ```json { "plan": "pro", "credits_remaining": 480, "intervals": { "images": { "limit": 1000, "used": 520, "remaining": 480, "interval": "rolling_30_days", "reset_at": "2026-07-01T00:00:00Z" }, "video": { "limit": 50, "used": 4, "remaining": 46, "interval": "rolling_30_days", "reset_at": "2026-07-01T00:00:00Z" } } } ``` --- ## SDKs & MCP - **TypeScript & Python SDKs** — typed clients with automatic retries, idempotency, and a webhook-signature helper. *Coming soon.* - **MCP server** — the same operations exposed as Model Context Protocol tools (`generate_meme`, `generate_image`, `generate_video`, `plan_carousel`, `get_generation`, …) so agents can create campaigns directly. *Coming soon.* Until the SDKs ship, the API is plain REST — any HTTP client works. Want early access to a key, an SDK, or the MCP server? Email **members@memes.com**.