Skip to content
RPT AI Video Studio

Reference

REST API and webhooks

The REST API is a small HTTP interface under /v1 for scripts and backends: price a clip, generate it, read it and download the file, and run clip batches. It uses the same API token as MCP, and the same limits apply to it.

The basics

  • The base path is /v1 on the studio host, the same host as the MCP server. In the examples STUDIO_URL holds the address and MCP_API_TOKEN holds the token.
  • Authentication is a bearer token only: Authorization: Bearer <token>. A web sign-in cookie is never read. A call without a token fails with UNAUTHENTICATED (HTTP 401).
  • Create the token on the Developers page, as in the quickstart. Its mode, allowed tools, project scope, calls per minute, spend cap, paid jobs at once and suspension all apply exactly as they do over MCP.
  • An endpoint needs the MCP tool of the same purpose in the token’s allowed tools (see the table below). GET /v1/me needs none.
First call: who is this token?
export STUDIO_URL="https://<your-studio-host>"
export MCP_API_TOKEN="<your token>"
curl -sS "$STUDIO_URL/v1/me" -H "Authorization: Bearer $MCP_API_TOKEN"
Example reply of GET /v1/me
{
  "object": "token",
  "workspace_id": "<workspace id>",
  "project_id": null,
  "mode": "execute",
  "environment": "<environment>",
  "tools": ["quote_media_task", "generate_media_task", "get_media_task"],
  "rate_limit_per_minute": 60
}

Endpoints

REST endpoints
EndpointNeeds the toolWhat it does
GET /v1/menoneThe token’s workspace, project, mode, allowed tools and calls per minute.
GET /v1/modelslist_video_modelsThe video models you can select, and default_video_model.
POST /v1/media-tasks/quotesquote_media_taskQuotes a media task and answers 201. A quote is free.
POST /v1/media-tasks/{task_id}/generategenerate_media_taskStarts a quoted task. It spends at the quoted price at once.
GET /v1/media-tasksget_media_taskLists tasks. kind defaults to video_clip.
GET /v1/media-tasks/{task_id}get_media_taskReads one task.
GET /v1/media-tasks/{task_id}/fileget_media_taskDownloads the file. It answers 409 until the task is READY.
POST /v1/media-batchescreate_media_batchCreates a clip batch and, by default, starts it. Answers 201.
GET /v1/media-batches/{batch_id}get_media_batchReads a batch: status, counts and progress.
GET /v1/media-batches/{batch_id}/itemslist_media_batch_itemsLists the items. An item that is ready has a file_url.
GET /v1/openapi.json, GET /v1/docsnoneThe OpenAPI document and a page that lists the endpoints. Both are open: no token.

The quote body takes the same fields as the MCP tool quote_media_task; unknown fields are refused. The fields for a video clip are kind, prompt, duration_seconds, ratio, resolution, route and reference_asset_id. The other kinds are voice, music, sfx and image. The workspace is never part of a body: a token belongs to one workspace.

Price, then generate a clip

  1. Quote the clip. The reply has the task id and state QUOTED.
  2. Show the price to the person. Only after they agree, generate. The reply has state QUEUED.
  3. Read the task until state is READY, or wait for a webhook (below).
  4. Download the file.
1. Quote (the reply carries the task id)
curl -sS -X POST "$STUDIO_URL/v1/media-tasks/quotes" \
  -H "Authorization: Bearer $MCP_API_TOKEN" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"kind": "video_clip", "prompt": "A ceramic mug on a bright table, slow turn", "duration_seconds": 4, "ratio": "9:16"}'
2. Generate, after the person agreed to the price
export TASK_ID="<id from the quote>"
curl -sS -X POST "$STUDIO_URL/v1/media-tasks/$TASK_ID/generate" \
  -H "Authorization: Bearer $MCP_API_TOKEN" \
  -H "Idempotency-Key: $(uuidgen)"
3. Read the state, then download the file
curl -sS "$STUDIO_URL/v1/media-tasks/$TASK_ID" -H "Authorization: Bearer $MCP_API_TOKEN"
curl -sS -o clip.mp4 "$STUDIO_URL/v1/media-tasks/$TASK_ID/file" -H "Authorization: Bearer $MCP_API_TOKEN"

Batches

Send the table as csv (text), xlsx_base64 (a base64 .xlsx file) or rows (a list of objects). Besides the table the body takes these fields; unknown fields are refused.

Batch body
FieldMeaning
csv, xlsx_base64, rowsThe table. csv up to 2,000,000 characters, xlsx_base64 up to 4,000,000. A batch has 100 rows by default.
templateUp to 3600 characters, with {{column}} placeholders. Without it each row’s prompt column is the prompt.
route, duration_seconds, ratio, resolution, reference_asset_idThe clip options for every row; a column in the table replaces them for one row. See Video clips and clip batches.
project_idOptional project to attach the batch to.
startThe default is true: the batch starts at once. false makes a free draft that a person starts in the web app.
A two-row draft batch
curl -sS -X POST "$STUDIO_URL/v1/media-batches" \
  -H "Authorization: Bearer $MCP_API_TOKEN" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"rows": [{"product": "Ceramic mug"}, {"product": "Linen shirt"}], "template": "Slow turntable shot of a {{product}} on a bright table", "ratio": "9:16", "start": false}'

The reply has the batch id, its status (draft here) and item_count. Read the batch with GET /v1/media-batches/{batch_id} and its items with GET /v1/media-batches/{batch_id}/items, optionally with state=ready.

Idempotency-Key

  • Every POST needs an Idempotency-Key header of 8 to 200 printable ASCII characters. A UUID is a good key. Without a valid one the call fails with V1_IDEMPOTENCY_KEY_REQUIRED (HTTP 400).
  • The key is scoped to your workspace and to the route. Repeating a key on the same route returns the first result and makes no second quote, task or batch. The same key on another route is a different request.
  • Make a new key for each new request; reuse a key only to retry exactly the same call after a timeout.

Rate limit, errors and pagination

  • Every answer carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset. The reset value is a unix time in seconds, when the current minute window ends. A 429 answer also carries Retry-After.
  • A failed call returns {code, message, retryable, request_id}. The message follows the Accept-Language header of the request. Quote the request_id when you ask for help.
  • The codes are the ones in the error reference; the REST-only ones are V1_IDEMPOTENCY_KEY_REQUIRED and V1_CURSOR_INVALID.
  • Lists return {object: "list", data, has_more, next_cursor, total}. Pass next_cursor back unchanged as cursor to get the next page. limit defaults to 20 and is at most 100. A cursor that is not one of ours fails with V1_CURSOR_INVALID (HTTP 400).
Error body
{
  "code": "V1_IDEMPOTENCY_KEY_REQUIRED",
  "message": "<text in the language of Accept-Language>",
  "retryable": false,
  "request_id": "<request id>"
}

OpenAPI document

GET /v1/openapi.json returns the OpenAPI document of the public API and GET /v1/docs shows the endpoints as a page. Neither needs a token, and both may be cached for a few minutes. The public contract is this document; the routes the web app itself uses are not part of it.

Fetch the document
curl -sS "$STUDIO_URL/v1/openapi.json"

Webhooks

A webhook sends a signed HTTPS POST to your server when something finishes, so a script does not have to poll.

  1. A workspace admin signs in to the web app, opens Developers and then Webhooks. An API token or an MCP session cannot manage webhooks: it gets FORBIDDEN. There is no REST endpoint for it.
  2. Add the endpoint URL, an optional description of up to 120 characters, and the events to send, or all of them. A workspace has at most 10 endpoints (WEBHOOK_LIMIT_REACHED, HTTP 409).
  3. Copy the signing secret, which starts with whsec_. It is shown once, when you create the endpoint or rotate the secret. Afterwards only its last 4 characters are shown.
  4. Choose Send test to receive a webhook.test event, usually within about 30 seconds. Test and replay sends are rate limited: too many waiting at once fails with WEBHOOK_RATE_LIMITED (HTTP 429, retryable); wait a few seconds and try again.
  • Rotate secret: the previous secret keeps signing for 24 hours, so you can switch without losing events. During that time the signature header carries one v1= value per live secret.
  • An endpoint can be disabled and deleted. A disabled endpoint receives nothing (WEBHOOK_ENDPOINT_DISABLED, HTTP 409, when you try to send it a test).
  • The delivery log shows every attempt. Replay sends a delivery again with the same event id, so your server can recognise a duplicate.

Events and request headers

Webhook events
EventSent whenFields in `data`
media_task.succeededA media task, such as a clip, is ready.task_id, kind, state, billed_micros, currency, error_code, batch_id, row_index, url
media_task.failedA media task failed.The same fields.
batch.completedA clip batch finished.batch_id, status, ready, needs_attention, cancelled, url
delivery.issuedA delivery was issued.delivery_id, project_id
webhook.testYou chose Send test. It cannot be subscribed to.
An event body (created is a unix time in seconds)
{
  "api_version": "v1",
  "created": 1700000000,
  "data": {
    "batch_id": "<batch id>",
    "cancelled": 0,
    "needs_attention": 1,
    "ready": 9,
    "status": "completed",
    "url": "/v1/media-batches/<batch id>"
  },
  "id": "<event id>",
  "object": "event",
  "type": "batch.completed",
  "workspace_id": "<workspace id>"
}
Request headers
HeaderValue
Content-Typeapplication/json
User-AgentRPT-Webhooks/1
RPT-Signaturet=<unix seconds>,v1=<hex>
RPT-Event-IdThe event id. A replay keeps it.
RPT-Event-TypeThe event type.
RPT-Delivery-IdThis delivery.
RPT-Delivery-AttemptThe attempt number, starting at 1.

Verify the signature

The signature is the hex HMAC-SHA256 of {t}.{raw body}, keyed with the secret. Always verify before you trust a request:

  • Use the raw bytes of the body, exactly as received. Do not parse and re-serialize it first.
  • Compare in constant time. Accept the request if any v1 value matches.
  • Reject a timestamp that is more than 300 seconds from your clock, so an old request cannot be replayed.
  • Answer with a 2xx status as soon as you have stored the event. Do the slow work afterwards.
A receiver in Python (standard library only; run it behind HTTPS)
import hashlib
import hmac
import os
import time
from http.server import BaseHTTPRequestHandler, HTTPServer

SECRET = os.environ["WEBHOOK_SECRET"]


def valid(header, body):
    timestamp, candidates = None, []
    for part in (header or "").split(","):
        key, _, value = part.strip().partition("=")
        if key == "t":
            timestamp = value
        elif key == "v1":
            candidates.append(value)
    if timestamp is None or not timestamp.isdigit() or abs(time.time() - int(timestamp)) > 300:
        return False
    expected = hmac.new(SECRET.encode(), timestamp.encode() + b"." + body, hashlib.sha256).hexdigest()
    return any(hmac.compare_digest(expected, candidate) for candidate in candidates)


class Handler(BaseHTTPRequestHandler):
    def do_POST(self):
        body = self.rfile.read(int(self.headers.get("Content-Length", 0)))
        ok = valid(self.headers.get("RPT-Signature"), body)
        self.send_response(204 if ok else 400)
        self.end_headers()


HTTPServer(("0.0.0.0", 8080), Handler).serve_forever()

Delivery, retries and safe targets

  • Each attempt waits at most 8 seconds. Any status other than 2xx counts as a failure. A redirect (3xx) is a failure too: redirects are never followed.
  • A failed delivery is tried again after 30 seconds, 2 minutes, 10 minutes, 30 minutes, 1 hour, 2 hours, 4 hours, 8 hours and 8 hours. That is nine retries over about 24 hours. After that the delivery is failed. Its state is queued, delivered or failed.
  • The endpoint URL must be https, have no user name or password and no fragment, and be at most 2000 characters long.
  • The host must be a public name. An IP address, a name without a dot, or a name ending in .local, .internal, .localhost, .lan or .home.arpa is refused. So is a name that points to a loopback, private, link-local or otherwise reserved address. The sender connects to the address it checked, so the answer cannot change between the check and the call.
  • A refused URL fails with WEBHOOK_URL_REFUSED (HTTP 422); details.reason is url_invalid, https_required, target_not_public or dns_failed. A refused target in a later delivery fails at once, without retries.