API v1

API Documentation

Version 1 (v1) — additive only: new endpoints and optional fields are added; nothing existing is removed or changed.

Quickstart

The shortest path: find a product, create an order, follow it to delivered and download the video.

  1. 1.GET /v1/products — pick a product_id and a length_seconds from the prices.
  2. 2.GET /v1/products/'{id'}/intake-form — see which answer fields the product asks for.
  3. 3.POST /v1/uploads (if the order needs files) — PUT the bytes to the presigned URL and keep the upload_id.
  4. 4.POST /v1/orders — send product_id, length_seconds, answers and uploads, with an Idempotency-Key.
  5. 5.GET /v1/orders/'{id'} — the status progresses: received → in_production → (awaiting_approval / changes_requested) → delivered.
  6. 6.GET /v1/orders/'{id'}/video — once delivered, get a secure download link.

Authentication

Every call sends your API key in the Authorization header, Bearer style. No cookies, no CORS — the API is called from your backend, not the end user's browser. The key can ONLY reach your own account: every order, file and money movement is bound to your account, whatever you send. The key is shown once at creation — if it leaks, revoke it from the portal and make a new one.

Authorization: Bearer vapi_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Base URL

All endpoints live under https://mystage.film/api/v1. Responses are JSON. Every response carries an x-request-id header — quote it when contacting support.

https://mystage.film/api/v1

Pagination

List endpoints return { data, has_more, next_cursor }. Pass next_cursor as the cursor query parameter on the next call. limit: 1–100 (default 20). Order is always newest first.

Idempotency (safe retries)

Every POST that creates something (orders, uploads, change requests, approvals, cancel) REQUIRES the Idempotency-Key header with a unique value per operation (e.g. one UUID per workflow). Same key + same body = the stored answer, never a second run; same key + different body = 422 idempotency_mismatch; a key still running = 409 idempotency_in_flight. Responses are kept for 48 hours. A network retry can therefore never create a duplicate order or a double charge.

Idempotency-Key: 8f3c1d2a-77b9-4c1e-9a01-5f6e8d9c0b1a

Errors

All errors follow RFC 9457 (application/problem+json) with type, title, status, code, request_id and, for field problems, an errors list. The codes you will meet:

  • 400 invalid_input — invalid body or field (see the errors list)
  • 401 unauthenticated — wrong or revoked API key
  • 402 insufficient_credit — the balance is not enough; nothing was charged
  • 403 forbidden / pinned_customer — the key lacks the scope, or you sent a foreign customer_id
  • 404 not_found — unknown id, or another account''s id (same answer, no information leak)
  • 409 idempotency_in_flight / conflict — the request is already running, or not allowed in the current state
  • 422 idempotency_mismatch / *_not_offered — same key with a different body, or a choice the product does not sell
  • 429 rate_limited — too many calls; retry with backoff
  • 5xx — a problem on our side; retrying with the same Idempotency-Key is safe

Rate limits

Up to 120 calls per minute per key (reads and writes together) and up to 30 POST/PATCH/DELETE per minute per key. A 429 answer carries Retry-After. Refused calls are never charged.

Endpoints

All calls are relative to the base URL. Bodies are strict: unknown fields are refused.

GET/ping

Check that the key is valid. No fields.

response

  • ok: true
GET/me

Your key, your account, your wallet balance and a 30-day usage summary (calls, orders, charges in cents).

response

  • key { id, name, last4, scopes, created_at, last_used_at }
  • customer { id, email, name }
  • wallet { currency, balance_cents }
  • usage { window_days, requests, orders, credits_cents }
curl -H "Authorization: Bearer $KEY" https://mystage.film/api/v1/me
GET/usage

The key's request history: route, method, status, order_id when the call created an order, and credits_cents for what it debited. Cursor pagination.

body / query

  • cursor string? — next_cursor of the previous page
  • limit int? — 1..100 (default 20)

response

  • data: [{ id, created_at, route, method, status_code, order_id, credits_cents, product_name, order_status, delivered_at, currency }]
  • has_more: boolean
  • next_cursor: string | null
curl -H "Authorization: Bearer $KEY" "https://mystage.film/api/v1/usage?limit=50"
GET/products

The catalog with prices per duration, aspect ratios, add-ons and the intake schema. Cursor pagination.

body / query

  • cursor string?
  • limit int? — 1..100

response

  • data: [{ id, slug, name, lengths: [{ seconds, price_cents }], aspect_ratios, add_ons, intake_schema }]
  • has_more
  • next_cursor
curl -H "Authorization: Bearer $KEY" https://mystage.film/api/v1/products
GET/products/{productId}

One product with all its details.

response

  • Product — one product, all details
GET/products/{productId}/intake-form

The answer fields the product asks for: type, label, required, options, whether it takes an upload. Build your form from here.

response

  • fields: [{ key, type, label, required, options?, upload? }]
POST/uploadsIdempotency-Key required

Requests a presigned URL for one intake file. Body: purpose="intake", content_type (image/jpeg|png|webp, audio/mpeg|mp4), size_bytes (up to 50 MB, exact). Always PUT to url with the right Content-Type before it expires (10 minutes). Keep the upload_id for the order answers.

body / query

  • customer_id uuid — your own customer id (GET /me)
  • purpose "intake"
  • content_type enum — image/jpeg | image/png | image/webp | audio/mpeg | audio/mp4
  • size_bytes int — 1..52428800, exact

response

  • upload_id — reference it in the order answers
  • url — PUT the bytes here
  • headers { content-type }
  • expires_at (10 minutes)
curl -X POST -H "Authorization: Bearer $KEY" -H "Idempotency-Key: $IDEM" \
  -H "Content-Type: application/json" -d '{"customer_id":"…","purpose":"intake","content_type":"image/jpeg","size_bytes":48123}' \
  https://mystage.film/api/v1/uploads
POST/ordersIdempotency-Key required

Creates the order and charges your wallet in one transaction. Body: customer_id, product_id, length_seconds (from the prices), aspect (optional, from aspect_ratios), add_ons { subtitles, extra_formats, resolution, duration_seconds } (optional, only what the product offers), answers (the intake form fields; files as upload_id), uploads (list of upload_id). complimentary is not available through the API. 402 when the balance is not enough — then nothing is created.

body / query

  • customer_id uuid — your own customer id
  • product_id uuid — from GET /products
  • length_seconds int — one of the product price lengths
  • aspect enum? — one of the product aspect_ratios (default: native)
  • add_ons object? — { subtitles?, extra_formats?, resolution?, duration_seconds? } only what the product offers
  • answers object — the intake form fields; files as upload_id
  • uploads string[] — the upload_ids used in answers

response

  • Order — 201 with status "received", total_cents, add_ons
  • 402 insufficient_credit — nothing created, nothing charged
curl -X POST -H "Authorization: Bearer $KEY" -H "Idempotency-Key: $IDEM" \
  -H "Content-Type: application/json" -d '{"customer_id":"…","product_id":"…","length_seconds":30,"answers":{}}' \
  https://mystage.film/api/v1/orders
GET/orders

Your orders, newest first. Filters: status, created_after (ISO), customer_id. Cursor pagination.

body / query

  • cursor string?
  • limit int? — 1..100
  • status enum? — received | awaiting_approval | changes_requested | in_production | delivered | failed | cancelled | awaiting_balance
  • created_after string? — ISO timestamp

response

  • data: [Order]
  • has_more
  • next_cursor
curl -H "Authorization: Bearer $KEY" "https://mystage.film/api/v1/orders?status=delivered"
GET/orders/{orderId}

One order: status (received, awaiting_approval, changes_requested, in_production, delivered, failed, cancelled, awaiting_balance), details, add-ons, total_cents.

response

  • Order
POST/orders/{orderId}/cancelIdempotency-Key required

A cancellation request for an order in paid state. Body: reason (optional). The cancellation is completed by our team; if production has started, the charge is refunded to your wallet.

body / query

  • reason string? — why you cancel

response

  • Cancellation — status "pending" until our team completes it
GET/orders/{orderId}/storyboard

An order's current storyboard: version, status, scenes with images (presigned URLs expiring in ~15 minutes).

response

  • version_id, version_number, status, revisions_allowed
  • scenes: [{ scene_index, label, caption, image_url }] (image URLs expire in ~15 minutes)
POST/orders/{orderId}/storyboard/approveIdempotency-Key required

Approves the storyboard and starts production. No body.

response

  • StoryboardDecision — production starts
POST/orders/{orderId}/change-requestsIdempotency-Key required

Requests changes on scenes. Body: requests array with scene_index and comment (the same shape as the site).

body / query

  • requests array — [{ scene_index int, comment string }]

response

  • StoryboardDecision — the order goes to changes_requested
GET/orders/{orderId}/video

Only when delivered: a secure download URL for the finished video (MP4), expiring.

response

  • url — secure MP4 download, expiring
  • 409 not_delivered — the video is not ready yet
API Documentation