# Introduction What Pixamake does and how the API is organized. Pixamake removes captions, hooks, titles and other text added in editing from video ads, **frame by frame**. Every frame that carries text is rebuilt with AI, then checked: leftover text, halos and blur are refused before a frame is kept. Text that is part of the scene, like packaging, labels or a phone screen, stays in the video. You can use Pixamake from the [web app](/app), from the REST API, or from an AI agent through the MCP server. ## How a video is processed 1. **Input.** You upload an MP4 or MOV file, or give a public URL. 2. **Reading.** AI reads the captions shown on screen and the spoken words. This list tells the engine exactly which text to erase. You can also provide it yourself with `texts`. 3. **Rebuilding.** Each frame carrying text is regenerated on GPUs, then verified. 4. **Delivery.** You get an MP4 at the original resolution (up to 1080p), with the original audio. ## Core concepts | Concept | Description | |---|---| | **Workspace** | Holds your plan, credits, videos, API keys and webhook. Teams share one workspace on the Scale plan. | | **Upload** | A slot to send a video file directly to storage, then attach it to a job. | | **Job** | One video being cleaned. It moves from `queued` to `succeeded`, `failed` or `canceled`. | | **Credits** | 150 credits per minute of video, billed per started second. Reserved when a job starts, returned if it fails. | ## API at a glance | Method | Endpoint | Purpose | |---|---|---| | `GET` | `/v1/account` | Plan, credits and limits | | `GET` | `/v1/estimate` | Credits for a given duration | | `POST` | `/v1/uploads` | Get a presigned URL to upload a file | | `POST` | `/v1/jobs` | Start cleaning a video | | `GET` | `/v1/jobs` | List jobs | | `GET` | `/v1/jobs/{id}` | Status, progress and result | | `POST` | `/v1/jobs/{id}/cancel` | Stop a job and get the credits back | The base URL is `https://pixamake.ai`. All requests and responses use JSON. The full reference is available as an [interactive page](/api-reference) and as an [OpenAPI 3.1 file](/openapi.json). ## Limits - MP4 or MOV, up to **15 minutes**, **2 GB** and **1080p** (1920 by 1080 in either orientation). - API access, webhooks and MCP are included in the **Growth** and **Scale** plans. --- # Quickstart Clean your first video with the API in three calls. This guide cleans a video in three calls: upload, create a job, download the result. ## 1. Create an API key Open [API and webhooks](/app/developers) in the dashboard and click **Create key**. The key starts with `pxm_live_` and is shown once. Store it as an environment variable: ```bash export PIXAMAKE_API_KEY="pxm_live_..." ``` API access requires the Growth or Scale plan. ## 2. Send the video If your video is already online, skip to step 3 and use `video_url`. Otherwise, ask for an upload slot: ```bash curl https://pixamake.ai/v1/uploads \ -H "Authorization: Bearer $PIXAMAKE_API_KEY" \ -H "Content-Type: application/json" \ -d '{"filename": "ad.mp4", "content_type": "video/mp4", "size_bytes": 4821331}' ``` ```json { "id": "up_7Qm2rT9vXk3LpZ8aWn4c", "object": "upload", "upload_url": "https://...", "method": "PUT", "headers": { "Content-Type": "video/mp4" }, "expires_at": "2026-10-04T13:00:00.000Z" } ``` Then send the file bytes to `upload_url` with the same `Content-Type`. The size must match `size_bytes` exactly. ```bash curl -X PUT "$UPLOAD_URL" -H "Content-Type: video/mp4" --data-binary @ad.mp4 ``` ## 3. Create the job ```bash curl https://pixamake.ai/v1/jobs \ -H "Authorization: Bearer $PIXAMAKE_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{"upload_id": "up_7Qm2rT9vXk3LpZ8aWn4c"}' ``` Or directly from a public URL: ```bash curl https://pixamake.ai/v1/jobs \ -H "Authorization: Bearer $PIXAMAKE_API_KEY" \ -H "Content-Type: application/json" \ -d '{"video_url": "https://cdn.example.com/ad.mp4"}' ``` The response is a [job](/docs/jobs) with `status: "queued"` and the credits it reserved. ## 4. Get the result Poll the job every 5 to 10 seconds, or configure a [webhook](/docs/webhooks): ```bash curl https://pixamake.ai/v1/jobs/job_3kT9xQ2vLm8RfZp1Wq7a \ -H "Authorization: Bearer $PIXAMAKE_API_KEY" ``` When `status` is `succeeded`, download `result.url`. The link is valid for one hour; fetch the job again for a fresh link. ## Complete example (Node.js) ```ts const BASE = "https://pixamake.ai"; const headers = { Authorization: `Bearer ${process.env.PIXAMAKE_API_KEY}`, "Content-Type": "application/json" }; const job = await fetch(`${BASE}/v1/jobs`, { method: "POST", headers: { ...headers, "Idempotency-Key": crypto.randomUUID() }, body: JSON.stringify({ video_url: "https://cdn.example.com/ad.mp4" }), }).then((r) => r.json()); let current = job; while (["queued", "analyzing", "processing"].includes(current.status)) { await new Promise((r) => setTimeout(r, 8000)); current = await fetch(`${BASE}/v1/jobs/${job.id}`, { headers }).then((r) => r.json()); } console.log(current.status, current.result?.url ?? current.error); ``` ## Complete example (Python) ```python import os, time, uuid, requests BASE = "https://pixamake.ai" H = {"Authorization": f"Bearer {os.environ['PIXAMAKE_API_KEY']}"} job = requests.post(f"{BASE}/v1/jobs", headers={**H, "Idempotency-Key": str(uuid.uuid4())}, json={"video_url": "https://cdn.example.com/ad.mp4"}).json() while job["status"] in ("queued", "analyzing", "processing"): time.sleep(8) job = requests.get(f"{BASE}/v1/jobs/{job['id']}", headers=H).json() print(job["status"], (job.get("result") or {}).get("url"), job.get("error")) ``` --- # Authentication API keys, headers and good practices. Every request to `/v1` and to the MCP server is authenticated with an API key of your workspace. ```bash curl https://pixamake.ai/v1/account -H "Authorization: Bearer pxm_live_..." ``` The `X-API-Key: pxm_live_...` header is also accepted. ## Keys - Create and revoke keys in [API and webhooks](/app/developers). A key gives full access to the workspace: its credits, videos and results. - Only a hash of the key is stored. If you lose a key, revoke it and create a new one. - Revoking a key takes effect immediately. - API access requires the Growth or Scale plan. With another plan, requests return `403 plan_required` (except `GET /v1/account` and `GET /v1/estimate`). ## Good practices - Keep keys on the server side, never in a browser or mobile app. - Use one key per integration (production server, staging, agent) so you can revoke one without touching the others. - Never print keys in logs, tickets or prompts shared with other people. ## Errors | Status | Code | Meaning | |---|---|---| | 401 | `invalid_api_key` | Missing, unknown or revoked key | | 403 | `plan_required` | The workspace plan does not include the API | --- # Uploading videos Direct uploads with presigned URLs, or public video URLs. There are two ways to give Pixamake a video. ## Option A: direct upload 1. `POST /v1/uploads` with the file name, type and exact size. 2. `PUT` the bytes to the returned `upload_url`, with the same `Content-Type`. 3. Create a job with `upload_id`. ```bash curl https://pixamake.ai/v1/uploads \ -H "Authorization: Bearer $PIXAMAKE_API_KEY" \ -H "Content-Type: application/json" \ -d '{"filename": "ad.mov", "content_type": "video/quicktime", "size_bytes": 18734102}' ``` | Field | Type | Description | |---|---|---| | `filename` | string | Original file name (shown in the dashboard) | | `content_type` | string | `video/mp4`, `video/quicktime` or `video/x-m4v` | | `size_bytes` | integer | Exact size of the file, up to 2 GB | The upload URL is valid for **one hour** and only accepts a body of the declared size and type. An upload that is never attached to a job is deleted after 24 hours. An upload can be used by one job only. ## Option B: public URL Pass `video_url` when creating the job. The URL must: - use `http` or `https` and be reachable from the internet (private and local addresses are refused); - answer HTTP range requests (status `206`), like most CDNs and object storages do; - point to an MP4 or MOV file of 2 GB or less. Pixamake reads the duration and resolution right away, so credits are reserved with the exact price, then copies the file when the job starts. ## Accepted videos | Limit | Value | |---|---| | Formats | MP4, MOV, M4V | | Duration | 15 minutes | | Size | 2 GB | | Resolution | 1080p (1920 by 1080, landscape or portrait) | Errors at this stage use status `422` with the codes `unsupported_format`, `unreadable_video`, `video_too_long`, `resolution_too_high`, and `413 video_too_large` for files over 2 GB. --- # Jobs Create, follow, list and cancel cleaning jobs. A job is one video being cleaned. Credits are reserved when the job is created and returned if it fails or is canceled. ## Lifecycle | Status | Meaning | |---|---| | `queued` | Credits reserved, waiting for a processing slot | | `analyzing` | Reading the captions and the speech of the video | | `processing` | Frames are being rebuilt and checked | | `succeeded` | The clean video is ready in `result.url` | | `failed` | Processing did not complete; see `error` (credits returned) | | `canceled` | Stopped by you (credits returned) | While a job runs, `stage` tells what happens and `progress` goes from 0 to 1. Stages: `queued`, `preparing`, `reading_text`, `downloading`, `masking`, `cleaning`, `verifying`, `assembling`, `finalizing`, `retrying`. Jobs of a workspace run in parallel up to the plan limit (2, 5 or 15). Extra jobs wait in the queue; higher plans are served first when the platform is busy. ## Create a job `POST /v1/jobs` | Field | Type | Description | |---|---|---| | `upload_id` | string | An upload created with `POST /v1/uploads` | | `video_url` | string | Public URL of the video (instead of `upload_id`) | | `filename` | string | Optional display name | | `texts` | string[] | Optional: the exact texts shown on screen to erase. When given, the automatic reading step is skipped | | `metadata` | object | Optional: up to 20 string keys and values, returned with the job and in webhooks | Send an `Idempotency-Key` header to retry safely: the same key returns the same job instead of creating a second one. ```bash curl https://pixamake.ai/v1/jobs \ -H "Authorization: Bearer $PIXAMAKE_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: 0d6c1c5e-62f4-4f57-9c1c-4e6a7c1a2f10" \ -d '{"upload_id": "up_7Qm2rT9vXk3LpZ8aWn4c", "metadata": {"campaign": "spring-sale"}}' ``` ## The job object ```json { "id": "job_3kT9xQ2vLm8RfZp1Wq7a", "object": "job", "status": "succeeded", "stage": null, "progress": 1, "source": "api", "input": { "filename": "ad.mp4", "duration_seconds": 30, "width": 1080, "height": 1920, "size_bytes": 4821331 }, "credits": 75, "credits_refunded": false, "result": { "url": "https://...", "expires_at": "2026-10-04T14:12:00.000Z", "size_bytes": 5310877 }, "error": null, "metadata": { "campaign": "spring-sale" }, "created_at": "2026-10-04T13:05:41.120Z", "started_at": "2026-10-04T13:05:42.008Z", "finished_at": "2026-10-04T13:09:57.774Z" } ``` `result.url` is a download link valid for one hour. Each `GET /v1/jobs/{id}` returns a fresh one. Files are kept for the retention period of your plan (7, 30 or 90 days). ## Retrieve a job `GET /v1/jobs/{id}`. Poll every 5 to 10 seconds, or use [webhooks](/docs/webhooks). ## List jobs `GET /v1/jobs?limit=20&status=succeeded&starting_after=job_...` | Parameter | Description | |---|---| | `limit` | 1 to 100, default 20 | | `status` | Filter by status | | `starting_after` | Cursor: the `id` of the last job of the previous page | ```json { "object": "list", "data": [ { "id": "job_..." } ], "has_more": true, "next_cursor": "job_..." } ``` ## Cancel a job `POST /v1/jobs/{id}/cancel` stops a queued or running job and returns its credits. Finished jobs are returned unchanged. ## Failure codes | `error.code` | Meaning | |---|---| | `no_text_found` | No caption or overlay was found in the video. No credits used. | | `nothing_erased` | The engine could not find the text to erase. No credits used. | | `processing_failed` | The engine stopped with an error. Credits returned. | | `timeout` | Processing took too long and was stopped. Credits returned. | | `start_failed` | The video could not be prepared after several attempts. Credits returned. | --- # Webhooks Signed notifications when a video is ready, failed or was canceled. Set an endpoint URL in [API and webhooks](/app/developers) (Growth and Scale plans). Pixamake sends a `POST` request with a JSON body for these events: | Event | When | |---|---| | `job.succeeded` | The clean video is ready | | `job.failed` | Processing failed (credits returned) | | `job.canceled` | The job was canceled | | `webhook.test` | Sent with the **Send test event** button | ## Payload ```json { "id": "evt_9sKq2mT4nVb7Lx3Rp8Wc", "object": "event", "type": "job.succeeded", "created_at": "2026-10-04T13:09:58.012Z", "data": { "object": { "id": "job_3kT9xQ2vLm8RfZp1Wq7a", "object": "job", "status": "succeeded", "result": { "url": "https://..." } } } } ``` `data.object` is the full [job object](/docs/jobs). Its `result.url` expires after one hour: download right away, or fetch the job later for a fresh link. ## Verify the signature Each request has a `Pixamake-Signature` header: ``` Pixamake-Signature: t=1791119398,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd ``` `v1` is the hex HMAC SHA-256 of `"{t}.{raw body}"` with your signing secret (`whsec_...`). Compare in constant time and refuse timestamps older than 5 minutes. ```ts import { createHmac, timingSafeEqual } from "node:crypto"; export function verify(rawBody: string, header: string, secret: string) { const parts = Object.fromEntries(header.split(",").map((p) => p.split("="))); const expected = createHmac("sha256", secret).update(`${parts.t}.${rawBody}`).digest("hex"); const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300; return fresh && expected.length === parts.v1?.length && timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1)); } ``` ```python import hashlib, hmac, time def verify(raw_body: bytes, header: str, secret: str) -> bool: parts = dict(p.split("=", 1) for p in header.split(",")) expected = hmac.new(secret.encode(), f"{parts['t']}.".encode() + raw_body, hashlib.sha256).hexdigest() return abs(time.time() - int(parts["t"])) < 300 and hmac.compare_digest(expected, parts.get("v1", "")) ``` Always verify against the **raw** request body, before parsing the JSON. ## Delivery and retries - Answer with any `2xx` status within 15 seconds. Do the heavy work after answering. - Failed deliveries are retried after 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours and 12 hours. - Deliveries can arrive more than once or out of order: use the event `id` and the job `status` to process each change once. - Redirects are not followed, and the URL must be public (`https` in production). - You can rotate the signing secret at any time from the dashboard. --- # Credits and billing How credits are counted, reserved, returned and renewed. ## Price of a video **150 credits per minute of video**, billed per started second, with a minimum of 25 credits (10 seconds). | Video | Credits | |---|---| | 10 seconds | 25 | | 30 seconds | 75 | | 1 minute | 150 | | 2 minutes 30 | 375 | `GET /v1/estimate?duration_seconds=95` returns the price of any duration. ## Reservation and refunds - Credits are **reserved** when the job is created (`402 insufficient_credits` if the balance is too low). - A job that **fails** or is **canceled** gives its credits back automatically (`credits_refunded: true`). - You only pay for delivered videos. ## Plans | Plan | Price | Credits per month | Parallel videos | API and webhooks | Seats | File retention | |---|---|---|---|---|---|---| | Starter | $29.90 | 5,000 (about 33 min) | 2 | No | 1 | 7 days | | Growth | $89.90 | 20,000 (about 2 h 13) | 5 | Yes | 1 | 30 days | | Scale | $199.90 | 50,000 (about 5 h 33) | 15 | Yes | 10 | 90 days | Plan credits are renewed at each billing date; unused plan credits expire. Upgrading takes effect immediately and adds the difference in credits; downgrading applies at the next renewal. ## Credit packs Packs are one-time purchases, available with an active plan. Pack credits **never expire** and are used after your plan credits. | Pack | Price | Per minute | |---|---|---| | 5,000 credits | $24.90 | $0.75 | | 20,000 credits | $89.90 | $0.67 | | 50,000 credits | $199.90 | $0.60 | ## Invoices Payments are processed by Stripe. Invoices, payment methods, plan changes and cancellation are available from **Billing**, then **Manage billing**, in the dashboard. --- # Errors, limits and retries Error format, rate limits and idempotency keys. ## Error format Errors use a standard HTTP status and a JSON body: ```json { "error": { "type": "invalid_request_error", "code": "video_too_long", "message": "Videos are limited to 15 minutes.", "request_id": "req_Fq8Kx2LmT9vN3pQa" } } ``` Give the `request_id` (also in the `X-Request-Id` header) when you contact support. | Status | Type | Typical codes | |---|---|---| | 400 | `invalid_request_error` | `invalid_json`, `invalid_parameter` | | 401 | `authentication_error` | `invalid_api_key` | | 402 | `billing_error` | `insufficient_credits` | | 403 | `permission_error` | `plan_required` | | 404 | `not_found_error` | `job_not_found`, `upload_not_found` | | 409 | `conflict_error` | `upload_already_used`, `idempotency_conflict` | | 413 | `invalid_request_error` | `video_too_large` | | 422 | `invalid_request_error` | `unsupported_format`, `unreadable_video`, `video_too_long`, `resolution_too_high`, `url_not_allowed` | | 429 | `rate_limit_error` | `rate_limited` | | 500 | `api_error` | `internal_error` | ## Rate limits Limits apply per API key, per method and per endpoint, over one minute: | Endpoint | Requests per minute | |---|---| | `POST /v1/jobs` | 30 | | `POST /v1/uploads` | 60 | | `GET /v1/jobs/{id}` | 240 | | Other endpoints | 120 | Every response includes `X-RateLimit-Limit`, `X-RateLimit-Remaining` and `X-RateLimit-Reset` (Unix seconds). A `429` response includes `Retry-After` in seconds. ## Idempotency `POST /v1/jobs` and `POST /v1/uploads` accept an `Idempotency-Key` header (up to 255 characters, a UUID is a good choice). - The same key with the same body returns the original response, with the header `Idempotent-Replayed: true`. No second job is created and no extra credits are reserved. - The same key with a different body returns `409 idempotency_conflict`. - Keys are kept for 24 hours. Retry on network errors, `429` and `5xx`, with exponential backoff and the same idempotency key. --- # AI agents and MCP Connect Claude, Cursor or any agent to Pixamake. Pixamake is designed to be used by AI agents as well as people: - an **MCP server** at `https://pixamake.ai/mcp` (Streamable HTTP); - [`/llms.txt`](/llms.txt) and [`/llms-full.txt`](/llms-full.txt): this documentation in plain Markdown; - an [OpenAPI 3.1 spec](/openapi.json) for tool generation. ## Connect the MCP server Create an API key in [API and webhooks](/app/developers), then add the server to your client. **Claude Code** ```bash claude mcp add --transport http pixamake https://pixamake.ai/mcp \ --header "Authorization: Bearer YOUR_API_KEY" ``` **Cursor** (`~/.cursor/mcp.json`) ```json { "mcpServers": { "pixamake": { "url": "https://pixamake.ai/mcp", "headers": { "Authorization": "Bearer YOUR_API_KEY" } } } } ``` **Claude Desktop and other stdio clients** ```json { "mcpServers": { "pixamake": { "command": "npx", "args": ["-y", "mcp-remote", "https://pixamake.ai/mcp", "--header", "Authorization: Bearer YOUR_API_KEY"] } } } ``` ## Tools | Tool | What it does | |---|---| | `get_account` | Plan, credit balance and limits | | `estimate_credits` | Credits for a video duration | | `create_upload` | Presigned URL to upload a local file (the agent then sends the bytes with HTTP PUT) | | `create_job` | Start cleaning a video from `video_url` or `upload_id` | | `get_job` | Status, progress and download link | | `wait_for_job` | Waits up to 50 seconds for a job to finish, then returns it | | `list_jobs` | Recent jobs of the workspace | | `cancel_job` | Stop a job and get the credits back | Example request to an agent: *"Clean the captions from https://cdn.example.com/ad.mp4 and give me the download link."* ## Guidelines for agents - Check the balance with `get_account` before starting large batches, and tell the user when credits run out (`402 insufficient_credits`). - Reuse the same `Idempotency-Key` when retrying a creation over HTTP. - Poll a running job every 5 to 10 seconds, or call `wait_for_job` repeatedly. Most 30 second videos take a few minutes. - `result.url` expires after one hour: download it, or fetch the job again for a new link. - Never print the API key in answers or logs.