Skip to content

Guides

Errors

Every refusal on this surface, over HTTP and over MCP, is the same four field envelope. The code is the machine's handle, the message is ours to change, and details is the only place a value you sent can appear.

The envelope

json
{
  "error": {
    "code": "insufficient_seconds",
    "message": "You do not have enough video time left. Buy more time to keep going.",
    "details": {
      "needed": 60,
      "available": 0
    },
    "requestId": "3f2b0c1a-9d4e-4a61-b8c2-000000000023"
  }
}
402 from POST /renders on an account with no time left.
FieldTypeAlways presentWhat it is
codestringYesThe stable machine readable handle. This is the only field to branch on.
messagestringYesOne English sentence from a catalogue in the server. It is written for a person, it is not stable, and it never contains anything you sent.
detailsobjectNoStructured, per code. Present only when there is something machine readable to add: the failed fields, the two wallet numbers, the seconds to wait. This is where anything derived from your request appears.
requestIdstring | nullYesThe id of this request. Log it. It is the single thing support can trace a failure by, and it is the only way the original error message, which is never sent to you, can be found.

Nothing else is in the body. There is no second error vocabulary for MCP: a tool call that fails answers isError: true with this same object as its text block, including arguments that do not match the tool schema.

Branch on the code, never on the message

The message is a sentence a person reads. It is rewritten when a better sentence is written, it is translated where translation exists, and it is deliberately the same sentence for a whole family of causes. Nothing in this API promises it will not change.

The code does promise that. A code that exists keeps meaning what it means, forever. What it does not promise is that the list stops growing: the catalogue gains a code whenever a service learns a new refusal, and that is an additive change that does not need a new API version. This surface even downgrades the OpenAPI diff rule for it on purpose, because a client with an exhaustive switch over the error enum is a client that was written against the wrong contract.

class GogoscreenError extends Error {
  constructor(status, body, retryAfter) {
    const e = (body && body.error) || {};
    super(e.message || "The request could not be completed.");
    this.name = "GogoscreenError";
    this.status = status;
    this.code = e.code || "unexpected_response";
    this.details = e.details ?? null;
    this.requestId = e.requestId ?? null;
    this.retryAfter = retryAfter; // seconds, or null
  }
}

async function call(path, init = {}) {
  const res = await fetch(`https://api.gogoscreen.com/api/v1${path}`, {
    ...init,
    headers: {
      Authorization: `Bearer ${process.env.GOGOSCREEN_API_KEY}`,
      ...(init.body ? { "Content-Type": "application/json" } : {}),
      ...init.headers,
    },
  });
  if (res.ok) return res.json();

  // A refusal always parses. A proxy in the way may not, so never assume.
  let body = null;
  try {
    body = await res.json();
  } catch {
    /* leave it null */
  }
  const raw = res.headers.get("retry-after");
  throw new GogoscreenError(res.status, body, raw === null ? null : Number(raw));
}

const sleep = (ms) => new Promise((r) => setTimeout(r, ms));

// Branch on the code, and keep a default: the catalogue grows.
try {
  await call("/renders", { method: "POST", body: JSON.stringify(req) });
} catch (err) {
  switch (err.code) {
    case "insufficient_seconds":
      await topUp(err.details.needed - err.details.available);
      break;
    case "validation_failed":
      for (const issue of err.details.issues) report(issue.path, issue.message);
      break;
    case "rate_limited":
    case "service_unavailable":
      await sleep((err.retryAfter ?? 1) * 1000);
      break;
    default:
      // Unknown code. Log it with the request id and stop; do not retry blind.
      log.error({ code: err.code, requestId: err.requestId }, err.message);
      throw err;
  }
}
One client, one error class, one switch with a default.

What the status tells you

The status is a category. It is useful for deciding whether to retry at all, and nothing finer.

StatusMeansRetry?
400The request is wrong. The body, the query, the path or a header.Not until you change it.
401The credential could not be resolved.Not until you fix the credential.
402The account has no video time left. insufficient_seconds is the only code that is ever a 402.After buying time.
403Resolved, and not allowed: a missing scope, an unverified email, a blocked account, no paid time on the account.Not until the account or the key changes. paid_account_required clears as soon as the account buys time.
404No such resource on this account. A resource somebody else owns answers this too.No.
409The state conflicts with the request.Only after reading the current state.
410It existed and is gone: a swept file, a deleted account.No.
413The body is over the transport limit, 256kb on HTTP and 1mb over MCP.Not until it is smaller.
415The body's content type or charset is not one this API reads.Not until the headers are right.
422Marketing content that cannot be laid out at the length or speed it is saved at.Not until the content changes.
429Out of budget. Retry-After says how long.Yes, after waiting.
500Our fault. The original message is logged, never sent.Once, then report the requestId.
503A dependency is unreachable. Retry-After says how long.Yes, after waiting.

validation_failed and details.issues

Every input on this surface is validated against a strict schema before the handler runs, and the same mapping produces the same details.issues on both transports. Each issue names a path, a human message and a machine readable code. An agent can fix the named fields and call again without reading prose.

http
HTTP/1.1 400 Bad Request

{
  "error": {
    "code": "validation_failed",
    "message": "That request is not valid.",
    "details": {
      "issues": [
        {
          "path": "targetSeconds",
          "message": "Too big: expected number to be <=600",
          "code": "too_big"
        }
      ]
    },
    "requestId": "3f2b0c1a-9d4e-4a61-b8c2-000000000018"
  }
}
Executed against a live server.

The schemas are strict, so an unknown field is a failure, not something ignored. That is deliberate: a request accepted with a typo in an option name would be a request that silently did something else. The issue for it has an empty path and the code unrecognized_keys, with the offending key named in the message.

Three whole body rules fail the same way, before any per field schema runs.

  • A string carrying an unpaired surrogate, half of a character, is refused with details.reason: "lone_surrogate" and an issue naming the path. It cannot be stored in utf-8, and storing a replacement character instead would mean telling you one thing was kept and keeping another.
  • A body carrying a __proto__, constructor or prototype key anywhere is refused with the issue code unsafe_key.
  • A body whose Content-Type is not application/json, or which declares a charset other than utf-8, is 415 unsupported_media_type with details.contentType or details.charset naming what was objected to. Nothing could be read at all, so there is no field to point at.
  • A body that is declared JSON and is not valid JSON is 400 validation_failed with a single issue whose code is invalid_json and whose path is empty. The same is true over MCP.

insufficient_seconds carries two numbers

402 is the one status the product acts on, and insufficient_seconds is the only code that is ever answered with it. Any other code arriving at 402 is turned into a 500, so a customer with a full balance can never be sent to buy time they already have.

details.needed is how many seconds the request would hold, and details.available is what the wallet has. Both are integers. For a fixed length video needed is its length; for an Auto video it is the shortest a walkthrough can be, so the number you are told is the number that would let you through.

http
HTTP/1.1 402 Payment Required

{
  "error": {
    "code": "insufficient_seconds",
    "message": "You do not have enough video time left. Buy more time to keep going.",
    "details": {
      "needed": 60,
      "available": 0
    },
    "requestId": "3f2b0c1a-9d4e-4a61-b8c2-000000000023"
  }
}
Executed against an account with no time left.

details.missing: what to put right

A marketing export refusal that a caller can act on carries a list of what to fix, one entry per thing, in details.missing. It is the same list the editor draws beside its own Export button, so a caller with no editor is told exactly what a person would be shown.

FieldTypeWhat it is
fieldstringThe request or revision field the problem is about, for example format, durationSeconds, audioMode, coverText or expectedRevisionId.
sectionstringWhich panel of the editor owns that field: story, style, sound or export. Useful for putting the message next to the control a person would change.
messagestringOne plain sentence naming the fix, including what the design does offer where that is the problem.

Retry-After

Any refusal that knows how long to wait sends the header. Today that is 429 rate_limited, 409 idempotency_in_progress, which always says 2, 503 service_unavailable and 503 tts_unavailable. A refusal that carries details.retryAfterSeconds always sends the header too. The value is whole seconds.

Read the header rather than the body where both are present. They are derived from the same number, and the header is the one an HTTP client library already knows about.

A resource you do not own is a 404

Every read and every write resolves the resource against the calling account. A resource that belongs to somebody else answers exactly what a resource that never existed answers, because the alternative confirms that an id is real.

http
HTTP/1.1 404 Not Found

{
  "error": {
    "code": "not_found",
    "message": "No such resource.",
    "requestId": "3f2b0c1a-9d4e-4a61-b8c2-000000000021"
  }
}
GET /renders/{id} for an id this account does not own.

Every code

146 codes, grouped by the part of the API that raises them, and within a group by status. Every row is generated from the server's own catalogue: a code the server learns appears here when the docs are rebuilt, and one with no recovery written for it fails the build rather than rendering an empty cell.

One of them takes the OpenAPI document's fallback of 409 because no operation on this surface declares it: verification_required. It is in the catalogue for the dashboard's sake, and you will not meet it here.

Generic

Any operation can answer these. No operation declares them, because they are true of all of them.

CodeStatusWhen it happensWhat to do
idempotency_key_required400A mutating operation arrived over HTTP with no Idempotency-Key header. It is refused before the body is parsed, so a caller missing both learns about both at once.Add an Idempotency-Key header of up to 128 characters, unique to this request. Over MCP the same value goes in the idempotencyKey tool argument.
invalid_cursor400The cursor query parameter was not one this API minted, or it was edited, or it is older than a hundred years either way.Drop the cursor and start the walk from the first page. Pass back exactly the nextCursor string you were given and never build one by hand.
validation_failed400The merged path, query and body did not match the operation's schema. The schemas are strict, so an unknown field is a failure rather than something ignored, and a string carrying half of a character is refused before it can be stored.Read details.issues. Each entry carries a path, a message and a machine readable code. Fix every named field and send the request again; the same body always fails the same way.
unauthorized401No credential was presented, or the key is unknown, revoked or expired, or an X-API-Key header carried something that is not an API key. details.reason says which.Send Authorization: Bearer gsk_live_…. A revoked or expired key never starts working again, so issue a new one in the developer console. Put a dashboard session token in Authorization, never in X-API-Key.
insufficient_seconds402The account's seconds wallet does not hold enough time for the video that was asked for. It is the only refusal on this surface that is a 402.details.needed and details.available are the two numbers. Buy more time, or ask for a shorter video. Retrying without changing either fails the same way.
account_blocked403The account behind this credential is suspended. Every operation on this surface is refused, whatever scopes the key holds.Contact support. Nothing a caller can change will lift it.
account_pending_deletion403The account is scheduled for deletion under a GDPR request, so every operation on this surface is refused until the request is withdrawn.Restore the account from the dashboard. details.deletionScheduledFor says when the data goes.
email_unverified403The operation declares a verified email and the owning account's address is not verified. A key reads the account's current fact, not the fact that was true when the key was issued.Verify the address on the account, then retry. The key does not need to be reissued.
feature_disabled403The operation sits behind a feature flag this deployment has switched off.Nothing a caller can change. Stop calling the operation, or ask the operator to enable it.
forbidden_scope403The credential does not carry every scope the operation declares, or it is the wrong kind of credential for it. keys.* and assistant.* are user only and can never be called by a key.details.requiredScopes and details.missingScopes name what is missing. A key's scopes cannot be changed after it is issued, so create a new key with them. When details.allowedActors is present, no key can call this operation at all.
paid_account_required403The account behind this key has no live paid plan and no top up time left. API and MCP access comes with any paid plan or a top up; the free seconds given at signup do not count. The check runs on every call, so nothing is revoked.Buy a plan or top up time in the dashboard, then retry with the same key. It works again as soon as the account has paid time.
not_found404No resource of that id belongs to this account. A resource that belongs to somebody else answers 404 as well, never 403.Check the id. Do not treat this as a permissions problem.
conflict409The request conflicts with the state the resource is in, and no more specific code applies.Re-read the resource and decide from its current state. Retrying the same request unchanged will not help.
idempotency_conflict409That idempotency key has already been used on this operation with a different request body.Use a fresh key for a different request. A key stands for one intent, not for one attempt.
idempotency_in_progress409The first request under that key is still being worked. Two concurrent identical requests both reach the store and one of them is told this.Wait the Retry-After seconds, which is 2, and send exactly the same request again with the same key. Do not change the body and do not mint a new key.
too_many_endpoints409The account already holds ten active webhook endpoints, which is the cap.Delete or disable one, then register the new one. details.max and details.active carry the numbers.
too_many_in_flight409The account already has as many renders or exports running as it may. The default ceiling is three of each.details.limit and details.inFlight carry the numbers. Wait for one to settle, or cancel one, then retry.
account_deleted410The account behind this credential has been deleted.Nothing to retry. The credential is dead and no operation will answer again.
gone410The resource existed and is no longer available.Nothing to retry on this id. Make the work again if it is still wanted.
payload_too_large413The request body is over the transport's limit: 256 kb over HTTP, 1 mb over MCP.Send less. details.limit carries the limit. Version 1 has no upload operations, so a body this size is usually a mistake.
unsupported_media_type415The request carried a body whose Content-Type is not application/json, whose charset is not utf-8, or whose Content-Encoding this API does not decode. A body nobody can read would otherwise be silently discarded.Send Content-Type: application/json and utf-8 bytes. details names the charset, encoding or content type that was objected to.
rate_limited429The credential's per minute budget, the account's per minute ceiling, or a daily cap was exceeded. details.scope is minute, day or account, or global when the product's own daily cap on voice previews is spent.Wait the Retry-After seconds and retry. details.limit and details.resetAt say what was hit and when it reopens. The RateLimit-* headers on every answer let a client pace itself before it gets here.
internal_error500An unhandled failure inside this API. The original message is logged and never answered.Retry once, then quote requestId to support. A 5xx deletes the idempotency claim rather than storing it, so retrying under the same key is safe.
service_unavailable503A service this API depends on could not be reached. details.reason is a short category, never an internal error message.Back off and retry. Retry-After and details.retryAfterSeconds say how long to wait.

The address guard

Every operation that fetches a URL you chose runs the same guard, and every one of its refusals is a fact about the address you sent, so every one of them is a 400. There is no way to switch it off.

CodeStatusWhen it happensWhat to do
url_bare_host400The hostname has no dot, so it is a single label that would resolve through local search domains to something internal.Use a full public hostname, for example https://example.com.
url_blocked400The address is on this deployment's denylist, or resolved to an address the guard refuses.Use a different public address.
url_credentials400The address carried a username or a password in the URL, which would be logged and stored.Remove them from the URL and use the credentials object, which is encrypted at rest, used once and deleted. When a storyboard is planned first, it is kept encrypted for that session and deleted at most two hours after its last use.
url_dns400The hostname could not be resolved.Check the spelling and that the name is public. A name that only resolves inside your own network will never resolve here.
url_invalid400The address is not one this API will fetch, for a reason the more specific codes do not cover.Send an absolute public http or https URL.
url_malformed400The address could not be parsed as a URL.Send an absolute URL including the scheme.
url_missing400An address was required and none was sent.Send the url field.
url_no_host400The address has no host component.Send an absolute URL with a hostname.
url_private400The address resolves to a private, loopback, link local, carrier grade NAT or IPv4 mapped IPv6 address. This API will not fetch inside a network.Point at a publicly reachable address. There is no way to opt out of this guard.
url_scheme400The address used a scheme other than http or https.Use https.

Walkthroughs: storyboards and renders

CodeStatusWhen it happensWhat to do
body_invalid400The per beat edits sent to renders.createFromStoryboard are not a valid shape: narration that is not text, a beat entry that is not an object.Send beats as a list of objects, each naming a beat id from the storyboard, with an optional rewritten narration or a hide flag.
credentials_required400The storyboard was planned signed in, so the render needs the same demo credentials again. The originals were deleted at settlement and are never kept.Send the credentials block, including loginUrl, on the render request.
hint_required400The hint is missing, or it is empty once trimmed. The schema requires 3 to 1000 characters and the service refuses a blank one before a browser is started.Say what to show in a sentence, for example "show the invoice creation flow". Nothing checks that a hint is specific, so a thin one is accepted and produces a poor video; storyboards.improveHint rewrites one before you spend anything.
hint_too_long400The hint is longer than 1000 characters.Shorten it. storyboards.improveHint rewrites a long brief into the form that plans most reliably, within the same bound.
idempotency_key_unscoped400An idempotency key arrived on a request with no account to scope it to.Call with an API key or a signed in session. An unscoped key would deduplicate nothing.
login_origins_invalid400The list of additional sign-in addresses sent with the demo credentials is not valid: more than five entries, an entry that is not https, or an address this API will not fetch.Send only the https origins the sign-in flow really passes through, each a public address, and retry.
login_url_invalid400The login page address is not one this API will fetch: a private address, a name that does not resolve, or an address that is not https. The sign-in page must be https because the demo account's password is typed there.Send a public https address for the page the demo account signs in on.
login_url_required400A credentials block was sent without loginUrl. Without it the run can only infer where the app is, and a wrong inference produces a video of the wrong thing.Add credentials.loginUrl, the address of the page those credentials sign in on. The credentials block itself stays optional.
no_beats400Every beat on the storyboard was hidden, so there would be nothing to record.Keep at least one beat visible.
storyboard_empty400The storyboard has no beats, so there is nothing to record.Plan again with a more specific hint. Planning is free.
unknown_beat400One of the beat ids named in the render request is not on that storyboard.Read the storyboard and name only beats it carries.
not_ready409A download was asked for on a render that has not finished.Poll renders.get until state is succeeded, then download.
storyboard_in_flight409A plan is already being made on this account. One at a time.Poll the running storyboard until state is terminal, or cancel it, then plan again.
storyboard_expired410The storyboard is past its retention window and its screenshots have been swept.Plan it again. Planning reserves no video time.
anon_lifetime_cap429An anonymous free tier session has planned as many walkthroughs as it may. It cannot be reached from the public API, which never mints an anonymous session.Call with an API key. The free tier is a dashboard funnel, not part of this surface.
origin_daily_cap429That website has been planned as many times today as it may be, across all accounts.Try again tomorrow, or plan a different site.
hint_improve_failed502The brief could not be rewritten this time.Use the words you already have. The operation changes nothing, so the original brief is still valid input to storyboards.create.
abuse_check_unavailable503The abuse check that runs before planning starts could not be reached, so the request was refused rather than run without it.Back off and try again in a minute. Nothing was created and nothing was charged.
budget_exhausted503Today's planning capacity is used up, so no new plan is started today.Try again tomorrow. Nothing was created and nothing was charged.
queue_unavailable503The work could not be started because a service it needs was briefly unavailable.Back off and retry. Nothing was created and nothing was charged.
vault_unavailable503The demo credentials could not be stored securely, so the run was refused rather than started without them.Back off and retry. Nothing was created and nothing was charged.
wallet_unavailable503The seconds wallet could not be reached to hold the time, so nothing was started.Back off and retry. No time was reserved, so nothing is stranded.

Four of these come from an abuse check that applies to the dashboard: anon_lifetime_cap, origin_daily_cap, budget_exhausted and abuse_check_unavailable. Their statuses are fixed in the server's catalogue, but nobody has managed to reach any of them with an API key. They may not be reachable from this surface at all. Handle them if you handle unknown codes; do not build a flow around them.

Render options, voices and the catalogue

CodeStatusWhen it happensWhat to do
avatar_unavailable400A presenter circle was asked for on a deployment where the presenter is switched off.Make the video without one. While the presenter is off, avatarId is not declared on the public schemas at all, so a public caller meets validation_failed first.
avatar_unknown400The avatarId named is not in the catalogue.Read GET /catalog/render-options and send an id from avatars.
background_unknown400The background named is not in the catalogue.Read GET /catalog/render-options and send an id from backgrounds, or omit the field for the default.
caption_style_unknown400The captionStyle named is not in the catalogue.Read GET /catalog/render-options and send an id from captionStyles.
capture_incompatible400A phone view capture was paired with a landscape orientation. Phone view only makes a portrait video.Pick a portrait orientation, or switch the capture to a computer window.
capture_unknown400The capture named is not in the catalogue.Read GET /catalog/render-options and send an id from captures.
language_unknown400The language named is not in the catalogue.Read GET /catalog/render-options and send a code from languages.
length_below_minimum400The length asked for is shorter than a walkthrough can be. The floor is 30 seconds.Ask for at least 30 seconds, or omit targetSeconds for Auto, which sizes itself.
options_invalid400The render options do not resolve together, for a reason the per field codes do not cover.Re-read GET /catalog/render-options and send a combination it offers. renders.quote prices options without creating anything.
orientation_unknown400The orientation named is not in the catalogue.Read GET /catalog/render-options and send an id from orientations.
template_unknown400The templateId named is neither a system template nor one of this account's saved templates.Read GET /catalog/render-options and send an id it lists, or a saved template id from GET /templates.
voice_language400The voice chosen does not speak the language chosen.Read GET /catalog/voices and pick a voice listed for that language, or change the language.
voice_unknown400The voiceId named is not in the catalogue.Read GET /catalog/voices and send an id it lists.
preview_failed502The voice preview could not be made.Try again shortly. The same sentence in the same voice is only ever synthesised once, so a successful retry is free.
tts_unavailable503Speech is not configured on this deployment, so no voice preview can be made.Nothing a caller can change. Ask the operator.

avatar_unknown and avatar_unavailable are in the catalogue and are not reachable on a deployment with the presenter circle switched off, which is the default. The public schemas do not declare avatarId while it is off, so a request naming one meets validation_failed with Unrecognized key: "avatarId" first, and catalog.renderOptions answers an empty avatars list.

Saved render templates

CodeStatusWhen it happensWhat to do
description_invalid400A saved template's description is longer than allowed.Keep it to 300 characters or fewer.
name_invalid400A saved template's name is empty or longer than 80 characters.Send a name between 1 and 80 characters.
settings_invalid400The settings object on a saved template is not valid.Send only keys the template settings schema declares, with values the catalogue offers.
source_render_not_found404The sourceRenderId or renderId named is not a render of this account.Send a render id this account owns. A render belonging to somebody else answers the same way.
template_not_found404No saved template of that id belongs to this account.Check the id, or list the account's templates.
template_limit409The account already holds the most saved render templates it may.Delete one, then save the new one.

Marketing: website sources

CodeStatusWhen it happensWhat to do
source_refresh_url_mismatch400A refresh named a different address from the one the source was created with. Allowing it would silently replace a customer's site with another.Refresh with the original address, or create a new source for the new one.
website_demo_invalid400The demo login sent with a scan is not a valid shape.Send a username, a password and the login page address together, or none of them.
source_not_found404No website source of that id belongs to this account.Check the id. A source belonging to another account answers the same way.
source_owner_not_found404The account a scan is being run for does not exist.Nothing a caller can change. Quote requestId to support.
source_version_not_found404No such version of that website source.Read the source to see which versions exist, then name one of those.
source_export_in_flight409A source delete was asked for while an export is still being cut from one of its versions.Cancel the export, then delete the source.
source_idempotency_conflict409That idempotency key was already used on this account with a different scan.Use a fresh key for a different scan.
source_inspection_busy409A scan is already running on this account. One at a time.Poll the running scan until its state is terminal, or cancel it, then start the new one.
source_inspection_stopping409A scan is still shutting down after a cancel.Wait a moment and try again.
source_kind_mismatch409The project was made from a different kind of source than the one being used, for example a walkthrough render where a scanned website is expected.Use the source the project was built from, or start a new project of the right kind.
source_not_ready409The source has not published a manifest yet, or a walkthrough project's render no longer has its clean source kept.Poll the source until state is succeeded. For a walkthrough project, make the render again.
source_owner_unavailable409The account cannot start new work, because it is blocked or scheduled for deletion.Resolve the account's state, then retry.
source_unreadable409The scanned website could not be read: it answered nothing usable, or its content was empty.Check the address loads in a browser without a sign in, then scan again.
source_version_mismatch409The version named belongs to a different website source.Send a version number from the source you are naming.
source_deleted410The website source has been deleted. It still reads state: "cancelled".Scan the site again with marketing.sources.create.
source_expired410The website source is past its retention window.Scan the site again. Projects already built from it keep the version they were built from.
source_version_expired410That manifest version has been cleaned up.Scan the site again and build from the new version.

Marketing: generations and the brand kit

CodeStatusWhen it happensWhat to do
brand_logo_source_mismatch400The logo choice names a source other than the one the generation is being built from.Choose a logo from the source this generation reads, or use the brand kit's own.
invalid_brand_kit400The brand kit sent is not valid: a missing required field, a colour that is not a colour, or a dash in copy the designs set in type that has no dash.details.errors names each problem. Fix them all and send the whole kit again, since the kit is replaced rather than merged.
generation_not_found404No generation of that id belongs to this account.Check the id. A generation belonging to another account answers the same way.
brand_kit_required409The generation needs a brand kit and the account has never saved one.Save one with PUT /marketing/brand-kit first. name and accent are the two required fields.
brand_logo_not_retained409The generation asked for a logo the account no longer holds, because retention took it.Pick a different logo choice, or scan the source again so a logo is held.
generation_busy409Another generation is already running for this source. One at a time per account.Poll the running one until its state is terminal, or cancel it, then start the new one.
generation_idempotency_conflict409That idempotency key was already used on this account with a different generation request.Use a fresh key for a different request. One key stands for one intent.
generation_not_retryable409A retry was asked for on a failure that a retry cannot change, for example a design that cannot fit the words.Read retryable on the generation before asking. When it is false, fix the request and start a new generation.
campaign_copy_overflow422Words sent for a marketing scene do not fit the frame the design draws them in. The fit is measured, not guessed.Shorten the fields details.fields names, then send the revision again. marketing.generations.preflight answers the same refusal before anything is created.
campaign_logo_aspect_unsupported422The logo's aspect ratio is not one any design can place.Supply a logo closer to square or to a standard wordmark shape.
campaign_unsupported_claim422A statement sent for the film is not supported by anything the scanned website says.Reword the claim so it repeats something the source actually says, or drop it.
campaign_visuals_insufficient422The design needs more pictures than the scanned source has.details says how many are needed and which scenes are short. Scan a richer page, refresh the source, or choose a design with fewer picture slots.
invalid_campaign_narration422The narration request for a marketing film is not a valid shape.Send narration as the operation's schema declares it, or omit it and let the design's default stand.

Marketing: projects, revisions and exports

CodeStatusWhen it happensWhat to do
edit_not_understood400A sentence sent to the deterministic editor was not understood whole. Nothing is half applied, so the project is unchanged.details says why, and names the vocabulary the editor does take. Rewrite the sentence within it, or make the change with marketing.revisions.create instead.
expected_revision_required400A save arrived without expectedRevisionId, so it could have landed on words the caller never saw.Read the project, send its current revision id as expectedRevisionId, and handle revision_conflict when somebody else saved first.
invalid_cover_text400The cover words are not valid for this design at this shape, usually because they do not fit.Shorten the cover text, or choose a format with more room.
invalid_revision400The revision document sent does not satisfy the design it names.details.errors says why. Fix each one and save again.
unknown_template400The marketing design named does not exist.Read GET /catalog/marketing/templates and send an id it lists.
unsupported_audio_mode400The design does not offer that audio mode.details.audioModes lists what it does offer. Pick one of those.
unsupported_duration400The design does not offer that length.details.durations lists what it does offer. Pick one of those.
unsupported_export400The design does not offer that format at that length. Both are authored per design, not computed.details lists the pairs it does offer. Ask marketing.exports.quote before creating an export.
unsupported_format400The design does not offer that shape.details.formats lists what it does offer. Pick one of those.
project_not_found404No marketing project of that id belongs to this account.Check the id. A project belonging to another account answers the same way.
already_settled409A cancel arrived for an export that had already succeeded, failed or been cancelled. The settlement is one conditional update, so a cancel racing a finish never moves the money twice.Read the resource and take its current state as the answer. Nothing changed.
export_in_flight409A project delete or another export was asked for while an export of that project is still running.Wait for it, or cancel it with marketing.exports.cancel, then retry.
gallery_example409A project delete was asked for while one of its exports is published as a gallery example.Nothing a caller can change. Ask support to unpublish the example first.
missing_required_footage409The design needs footage the recording this project was made from does not have.details.reasons says which. Choose a design that fits the recording, or make a recording that covers the missing beats.
no_revision409An export was asked for on a project that has no revision yet.Create a revision first, or wait for the generation that is making one to finish.
not_finished409A download was asked for on an export that has not finished.Poll marketing.exports.get until state is succeeded, then download.
revision_configuration_mismatch409The export asks for a shape, length or audio mode that is not the revision's own.Export the revision as it stands, or save a new revision at the configuration you want and export that.
revision_conflict409expectedRevisionId is no longer the project's current revision, so somebody else saved first. Nothing was written.details.currentRevisionId is the revision that won. Re-read the project, rebase the change on it, and save again.
expired410The file is past its retention window and has been deleted. The export itself still reads succeeded, with failureCode: "expired".Nothing to retry on this id. Export the revision again if the file is still wanted.
campaign_speed_breaks_music_ending422At the revision's saved playback speed the music would no longer end with the film.details.missing says what to put right. Save a new revision at a speed the music bed can end on, then export that.
campaign_speed_breaks_narration_fit422At the revision's saved playback speed a spoken line would no longer fit the shot it belongs to.details.missing says what to put right. Shorten the line or save the revision at a slower speed, then export that.
campaign_speed_not_offered422The revision is saved at a playback speed this build does not deliver, and the export judges the saved speed again before it reserves anything.details.speed is the saved speed and details.missing names the one way out, which is saving the revision at a speed the build offers. Save a new revision, then export that.
reservation_failed503The seconds wallet could not hold the time for an export.Back off and retry. Nothing was reserved, so no time is stranded.
storage_unavailable503File storage could not be reached, so no signed URL could be created.Back off and retry. The file itself is unaffected.

Webhooks

CodeStatusWhen it happensWhat to do
webhook_host_denied400The webhook host is on this deployment's denylist. The list matches a bare hostname and its subdomains.Point the endpoint at a host that is not on the list, or ask the operator.
webhook_url_blocked400The webhook URL resolves to an address this API will not post to, or its hostname does not resolve at all. Every resolved address must be public, so one private answer among several is refused.Point the endpoint at a public address. The fence runs again on every delivery attempt, so an address that changes later stops being delivered to.
webhook_url_invalid400The webhook URL is not a shape this API will post to: not https in production, a username or password in the URL, a fragment, a non default port, a single label hostname, or over 2048 characters.Use an https URL with a full public hostname, no credentials, no fragment and the default port. details.reason names which rule it broke.
webhook_endpoint_disabled409A test delivery was asked for on an endpoint that is disabled, either by its owner or by twenty consecutive failures.PATCH /webhooks/{id} with status: "active" re-enables it and clears the failure streak, then send the test again.

Account

CodeStatusWhen it happensWhat to do
verification_required409The action needs a verified email address on the account.Verify the address, then retry.

verification_required is the dashboard's own code for an unverified email address and is listed here only because it is in the server's catalogue. It is not reachable on /api/v1: this surface answers 403 email_unverified instead, and the status above is the document's fallback rather than anything observed.

The dashboard support assistant

CodeStatusWhen it happensWhat to do
conversation_not_found404No conversation with that id belongs to this account. Another account's conversation answers the same 404, so this does not say whether the id exists.Check the id. Read the current conversation to find the one to resume.
assistant_conversation_full409This conversation with the support assistant has reached the most messages one conversation may hold.Start a new conversation. details.limit says how many messages a conversation holds.
conversation_closed409A question was sent to a conversation that is closed or has been handed off to a person.Start a new conversation. The closed one can still be read.
ai_rate_limited429The support assistant was asked too many questions by this account in the last hour. Only the dashboard assistant answers it; a key cannot reach the assistant.Wait and ask again later, or send the question to a person with the hand off instead. Nothing was charged.
assistant_daily_limit429This account has asked the support assistant as many questions as it may today.Wait for details.resetAt, or send the question to a person with the hand off, which this limit does not cover.
handoff_limit429This account has sent support as many messages as it may in the last hour.Wait for details.resetAt and send it again. Messages already sent were received.
handoff_failed502The message to support could not be recorded, the account has no address support could reply to, or a retry could not read the original request's current state. details.reason says which.Try again shortly with the same Idempotency-Key, so a retry cannot send it twice. When details.reason is no_contact_email, the account needs an email address first.
ai_rate_unavailable503An hourly AI cap on a free dashboard door could not be counted, so the request was refused rather than run uncounted. Only the dashboard answers it; the /api/v1 and MCP surfaces never do.Try again in a minute. Nothing was created and nothing was charged.
assistant_unavailable503The support assistant could not answer: every model provider failed, or a limit that has to be checked could not be.Try again shortly, or send the question to a person with the hand off, which needs no model.

These belong to the support assistant in the dashboard, which only a signed in person can use. No API key and no MCP tool can reach it, so an integration will not meet them. They are listed because they are in the server's catalogue.

Other

Codes the server carries that this page has not yet filed under a heading.

CodeStatusWhen it happensWhat to do
site_not_allowed400The website is a major public platform (a social network, search engine, marketplace or big portal) or a site about a sensitive topic (adult content, gambling, drugs, weapons, hate or violence). details.reason says which: public_platform or sensitive_topic. A sign-in address on a sensitive-topic site is refused too, and so is a sign-in that lands on a refused site.Use your own product's website. No time was used. If you think your site was refused by mistake, write to support@gogoscreen.com or open a ticket with "Talk to a person": support can allow a site by name.