Promoat.Partner API

Start here

Errors & limits

Every error is JSON with "error", a machine-readable "error_code" and a "message", and an honest status code. Nothing is charged on a request that fails validation, auth, or the balance check.

Error shape

Every error
{
  "error": "string",
  "error_code": "UPPER_SNAKE",   // the code to branch on
  "message": "human-readable",   // always present
  "field": "resolution",         // when one field is at fault
  ...details                     // e.g. credits_required, max_active_jobs
}
  • Since 1.1.1 every failure on every endpoint has error, error_code (UPPER_SNAKE) and message. Branch on error_code and the HTTP status.
  • error is kept for older integrations: failures that existed before 1.1.0 keep their old error string (for example Unauthorized with error_code UNAUTHORIZED, or Not found with NOT_FOUND) and HTTP status; newer failures use the code as error too. Some bodies also carry a lowercase code (for example quote_mismatch, no_speech); don't branch on it.
Examples
{ "error": "resolution must be \"480p\" or \"720p\"", "error_code": "INVALID_RESOLUTION",
  "message": "…", "field": "resolution" }

{ "error": "INSUFFICIENT_CREDITS", "error_code": "INSUFFICIENT_CREDITS",
  "credits_required": 125, "credits_balance": 40, "message": "…" }

{ "error": "QUOTE_MISMATCH", "error_code": "QUOTE_MISMATCH", "code": "quote_mismatch",
  "credits_quoted": 125, "message": "…" }

{ "error": "Unauthorized", "error_code": "UNAUTHORIZED", "message": "The API key is not valid" }

Status codes

CodeMeaningWhat to do
400Validation failed — a field is missing, malformed, or over a cap.Fix the request; error_code and field name the problem.
401UNAUTHORIZED — missing or invalid API key.Check the header format in Authentication.
402INSUFFICIENT_CREDITS — balance below the quote.Top up, or shorten the script / lower the resolution.
404NOT_FOUND — resource not found, or not yours.Verify the id came from your own account.
409Conflict: quote mismatch, idempotency key reused, voice cap, voice unavailable, free clone attempts used up, edit rules.See the code tables below.
422An input URL could not be read (audio probe, selfies, samples without speech).Make sure the URLs are public https and serve real media.
429TOO_MANY_ACTIVE_JOBS — concurrent job cap hit.Wait for a running job to finish, then re-queue.
502CLONE_UNAVAILABLE — voice cloning is temporarily unavailable.Retry later; the charge is refunded.
503TRY_AGAIN — briefly unavailable.Nothing was charged. Retry with the same idempotency key.
5xxSomething failed on our side.Safe to retry reads; retry POSTs with an idempotency key.
404 never leaks existence

Asking for another client's character, voice, asset, or job returns the same 404 as an id that was never created.

Codes on /videos and /videos/estimate

Statuserror (kept from before 1.1.0)error_code
400Invalid JSON bodyINVALID_BODY
400mode must be "talking_head" or "scenes"INVALID_BODY
400character_id is requiredINVALID_BODY
400resolution must be "480p" or "720p"INVALID_RESOLUTION
400Provide exactly one audio source: { voice_id, script } or { audio_url }INVALID_BODY
400video_idea is required unless first_frame_url is providedINVALID_BODY
400script too longAUDIO_TOO_LONG
422Could not probe audio_url duration: …INVALID_URL
404Not foundNOT_FOUND
402INSUFFICIENT_CREDITSwith credits_required, credits_balance
429TOO_MANY_ACTIVE_JOBSwith max_active_jobs
500Failed to start workflowSTART_FAILED (refunded)
500Failed to queue jobINTERNAL

New in 1.1.0 (here error = error_code):

error_codeWhen
INVALID_VIDEO_MODEvideo_mode is not reference | text.
INVALID_VOICE_MODEvoice_mode is not voice | none.
INVALID_DURATIONtarget_seconds missing or outside 5–60.
INVALID_SCRIPTScript empty or over 4,000 characters.
INVALID_SCENE_LINESscene_lines not 1–15 strings of ≤600.
INVALID_SCENE_PHOTOSscene_photos malformed or over 12 items.
INVALID_VOICE_SETTINGSvoice_settings malformed.
AUDIO_TOO_SHORTAudio under 1 s.
INVALID_URLA private, link-local or otherwise non-public URL.
FIRST_FRAME_KIND_MISMATCHfirst_frame_asset_id was made for the other video type.
QUOTE_MISMATCH409 — quote above expected_credits; body has credits_quoted.
TRY_AGAIN503 — retry; nothing charged.

POST /images/first-frame keeps 500 First-frame generation failed with error_code FIRST_FRAME_NOT_VERTICAL, CHARACTER_IMAGE_UNAVAILABLE, PROVIDER_* or INTERNAL; its 400s keep their old sentence in error (error_code INVALID_BODY or INVALID_SCENE_PHOTOS), its 404 is NOT_FOUND and its 402 INSUFFICIENT_CREDITS. Edit errors are listed in Video edits; failed-job codes in Jobs & polling.

Warnings

A 200 can carry warnings: [{ field, code, message }] — the request worked, but something was adjusted:

codeMeaning
RESOLUTION_MAPPED720p was served as 768P.
FIELD_IGNOREDA legacy field, a field with no effect (topic, audience), a field that doesn't apply (e.g. voice_id on a silent video), a non-https first_frame_url / audio_url, or a selfie that failed the URL check.
SCENE_PHOTOS_TRUNCATEDMore than 3 scene photos; the first 3 were kept.
LOW_SAMPLE_AUDIOVoice clone made from under 25 s of usable speech.
SCENE_PHOTO_DROPPEDJob-time, scene videos: a scene photo couldn't be fetched safely, so it wasn't used or charged.

Warnings found while a job runs (like SCENE_PHOTO_DROPPED) are listed on that job's row in GET /usage/jobs as metadata.warnings, with the same { field, code, message } shape.

Idempotency

Send Idempotency-Key (1–255 visible ASCII characters) or client_request_id (uuid). Optional on /videos; required on fix-scene, edit-script and fix-sound unless dry_run.

  • Same key, same body → 200 with { workflow_id, status, duplicate: true, credits_reserved }; nothing is charged twice.
  • One key covers all /videos job types: a talking-head body and then a scenes body under the same key → 409 IDEMPOTENCY_KEY_REUSED (one key, one job across both modes).
  • On /videos the body is validated and priced again before the duplicate lookup, so a retry can get 409 QUOTE_MISMATCH (re-confirm and resend with the new expected_credits under the same key) or a 404 / 422 if something it referenced is gone. Edits answer the duplicate first.
  • Keys are scoped per client and per endpoint family — a /videos key never collides with a fix-scene key.
StatuserrorWhen
400IDEMPOTENCY_KEY_MISMATCHHeader and client_request_id are both sent and differ.
400IDEMPOTENCY_KEY_INVALIDThe key or client_request_id is malformed (key: 1–255 visible ASCII; client_request_id: a uuid).
400IDEMPOTENCY_KEY_REQUIREDAn edit without a key (and not a dry run).
409IDEMPOTENCY_KEY_REUSEDSame key, different body.

Account limits

LimitDefaultWhere it bites
Active video jobs at once5429 from POST /videos and edits — queued + running jobs count.
Cloned voices3409 VOICE_LIMIT_REACHED — delete one to free a slot.
Library voices10Separate cap, same 409 when full.
Voiced video audio1–59.5 sAUDIO_TOO_SHORT / AUDIO_TOO_LONG at queue time.
Silent scene video5–60 starget_seconds; else INVALID_DURATION.
Video script4,000 charactersINVALID_SCRIPT
TTS text5,000 characters400 from POST /tts.
Open edits per video1 (an edit idle for 30 min no longer blocks)409 EDIT_IN_PROGRESS
Quiet job75 minutesFailed with STALE_TIMEOUT and refunded.
List page size100 rows maxlimit values above 100 are capped at 100 on assets / usage lists.

Retry guidance

  • GETs are always safe to retry.
  • Send an idempotency key on every POST /videos. Then a timeout or a 503 TRY_AGAIN is safe to retry with the same key and body — you get the original job back, not a second one.
  • A failed job is refunded automatically. Check its error_code (list) and re-queue with a new key.