Skip to content

Errors and retries

Stable machine codes, what each one means, and which requests are safe to send again.

Every failure has the same shape:

JSON
{
  "error": {
    "code": "insufficient_scope",
    "message": "This key is missing the `render.write` scope. Re-mint it with that scope at developer.vidmoat.com.",
    "docs_url": "https://developer.vidmoat.com/developer/docs/errors#insufficient_scope",
    "scope": "render.write",
    "granted": ["account.read", "projects.read"]
  }
}

Branch on code, never on message. The code is part of the contract; the message is written for a person reading a log and gets reworded whenever a clearer sentence turns up. Some codes add detail fields next to it (scope, granted, remaining, restriction, field, conflictCode, rev). New codes can appear in v1: treat an unknown one as a generic failure of its HTTP status rather than crashing.

Codes#

HTTPCodeRetry?
400 (409)invalid_requestNo; fix the request. For 409, re-read the project and send again.
401unauthorizedNo, until the credential is fixed.
402plan_requiredNo; upgrade the plan.
402insufficient_creditsAfter topping up.
402 (413)quota_exceededWhen the allowance frees up (a render finishes, the month resets).
403insufficient_scopeNo; mint a key with that scope.
403access_suspendedNo.
403app_suspendedNo; check the contact address on the app.
404not_foundNo.
413payload_too_largeNo; import from a URL or use a smaller file.
422prompt_rejectedNo; reword it.
429rate_limitedYes, after Retry-After seconds.
500internal_errorOnce, with backoff, after re-reading state.
501not_configuredLater.
502provider_errorYes, with backoff.
503unavailableYes, after Retry-After (30 seconds).

invalid_request#

400 (409). The request is malformed: a missing or wrong-typed field, a body that is not a JSON object, a bad command list. Also sent with status 409 when a project changed while your commands ran (conflictCode: "stale_rev", with the current rev). Retry: No; fix the request. For 409, re-read the project and send again.

unauthorized#

401. No bearer token, or one that is unknown, revoked or expired. Retry: No, until the credential is fixed.

plan_required#

402. The owner's plan does not include this feature. The scope was necessary but not sufficient. Retry: No; upgrade the plan.

insufficient_credits#

402. Not enough credits for this call. Nothing ran and nothing was charged. remaining is the balance. Also used when video generation is not available on a trial or a full-waiver promotion. Retry: After topping up.

quota_exceeded#

402 (413). An allowance is used up: the plan's project limit, monthly exports, renders running at once, one video job at a time. Sent with 413 when cloud storage is full. Retry: When the allowance frees up (a render finishes, the month resets).

insufficient_scope#

403. The credential is valid but lacks a scope. scope names the one needed and granted lists what the credential has. Retry: No; mint a key with that scope.

access_suspended#

403. The owning account is restricted. restriction says what is limited, until when, and links to the appeal. Retry: No.

app_suspended#

403. This app's kill switch is on, independent of the account's standing. Retry: No; check the contact address on the app.

not_found#

404. No such resource, or it belongs to somebody else. The two are deliberately indistinguishable. Retry: No.

payload_too_large#

413. An upload is over 32 MB or over the plan's upload cap, or an import is over the import limit. Retry: No; import from a URL or use a smaller file.

prompt_rejected#

422. A prompt failed the safety check. field names the field. Nothing was charged. Retry: No; reword it.

rate_limited#

429. Too many requests in the window. Refused requests do not count against the limit. Retry: Yes, after Retry-After seconds.

internal_error#

500. Our fault. Nothing is implied about whether the work happened. Retry: Once, with backoff, after re-reading state.

not_configured#

501. A provider this call needs (speech-to-text, a stock library) is not available. Nothing was charged. Retry: Later.

provider_error#

502. An upstream failed: a generation provider, a URL you asked us to import, a plugin, a frame render. A charged call is refunded. Retry: Yes, with backoff.

unavailable#

503. The rate limiter could not be reached, so the request was refused without running or charging anything. Retry: Yes, after Retry-After (30 seconds).

Retrying safely#

There is no Idempotency-Key header. Decide what to retry by what the call does:

RequestAfter a timeout or a 5xx
Any GETSafe to retry.
PATCH and PUTSafe to retry: they set state, so repeating one gives the same result.
DELETESafe; a second call answers 404.
POST /v1/projects/{id}/commandsNot safe. A repeated batch adds its clips again. Re-read the project first.
POST /v1/projectsNot safe. It creates a second project. List projects first.
POST /v1/rendersNot safe. It queues a second render and uses another export. List GET /v1/renders?projectId=… first.
Generation (/v1/ai/*)Not safe. A repeat is charged again. A failure that was our fault (502) is refunded, so retrying one of those is fine.

Back off exponentially (1, 2, 4, 8 seconds, with jitter) and give up after a few attempts. Honour Retry-After when it is present.

Rate limits#

Limits are per key, per minute, by plan. See rate limits.