Skip to content

Guides

Jobs

Planning a walkthrough, recording one, scanning a website, generating a marketing project and cutting an export all run as background jobs. Every one of them answers the same envelope and is polled the same way.

The 5 states

The public states. succeeded, failed and cancelled are terminal and nothing leaves them.States: queued (the starting state), running, succeeded (a final state), failed (a final state), cancelled (a final state). Transitions: queued moves to running work starts. queued moves to cancelled cancel. queued moves to failed refused before it started. running moves to succeeded a usable result exists. running moves to failed it could not be done. running moves to cancelled cancel, at the next checkpoint.work startscancelrefused before it starteda usable result existsit could not be donecancel, at the next checkpointqueuedSTARTrunningsucceededFINALfailedFINALcancelledFINAL
The public states. succeeded, failed and cancelled are terminal and nothing leaves them.
stateMeanspollAfterMs
queuedAccepted and waiting to start.5000
runningIn progress.2500
succeededA usable result exists. Read result, then fetch the file with the matching download operation.null
failedIt could not be done. error carries a code. Nothing is charged.null
cancelledYou stopped it. Not a failure: nothing went wrong and the reserved seconds went back whole. An integration reconciling its own records has to be able to tell we could not from you said not to.null

The 6 kinds

kindStarted byPolled at
walkthrough.storyboardPOST /storyboardsGET /storyboards/{id}
walkthrough.renderPOST /renders or POST /storyboards/{id}/rendersGET /renders/{id}
marketing.source_scanPOST /marketing/sources or its /refreshGET /marketing/sources/{id}
marketing.generationPOST /marketing/generationsGET /marketing/generations/{id}
marketing.exportPOST /marketing/projects/{id}/exportsGET /marketing/exports/{id}
marketing.source_operationAn operation the scan runs on a sourceReported through the source

202, Location, and the 200 replay

An operation that starts slow work answers 202 Accepted with a Location header pointing at the resource to poll, and a body of { resource, created } where resource is the envelope. Use the header rather than building the path: a replayed 202 carries the same one.

http
HTTP/1.1 202 Accepted
Location: /api/v1/renders/3f2b0c1a-9d4e-4a61-b8c2-000000000025
RateLimit-Limit: 20
RateLimit-Remaining: 19
RateLimit-Reset: 60
Cache-Control: private, no-store

{
  "resource": {
    "id": "3f2b0c1a-9d4e-4a61-b8c2-000000000025",
    "kind": "walkthrough.render",
    "state": "queued",
    "internalStatus": "queued",
    "stage": null,
    "progress": null,
    "error": null,
    "result": null,
    "cost": {
      "secondsReserved": 30,
      "secondsCharged": null,
      "watermark": false
    },
    "artifactsExpired": false,
    "cancellable": true,
    "idempotencyKey": "render-1790012026461",
    "requestId": null,
    "createdAt": "2026-09-21T11:21:00.000Z",
    "startedAt": null,
    "finishedAt": null,
    "expiresAt": null,
    "pollAfterMs": 5000
  },
  "created": true
}
POST /renders. Executed once against a live server and cancelled immediately, so the seconds went back whole.

The same request with the same Idempotency-Key answers the same 202 again, with Idempotent-Replayed: true and the same Location. Nothing new is started.

200 instead of 202 means the work was already known and nothing new was started: created is false. It happens on the operations that also remember the key on the job itself, when the 24 hour idempotency claim has expired but the job has not. Treat it exactly like a replay.

The envelope, field by field

NameTypeDescription
idstringThe resource id. Also the last segment of Location.
kindstringWhat sort of work this is. It never changes.one of the six kinds
statestringThe only field to branch on. Nothing outside this list is ever answered.queued | running | succeeded | failed | cancelled
internalStatusstringThe kind's own status word. Informational; useful in a log, never in a condition.
stagestring | nullA coarse sub state where the kind has one: an export's queued, preparing, rendering, mastering or checking; a generation's reading, planning or assembling. Null where the kind has none.
progressobject | nullHow far the work has got, as last reported. Null until something is recorded, and null for every terminal state, because a finished job is 100 by definition and a failed one has no bar to draw.
stagestringThe current stage. For a render: starting, checking, recon, plan, dry_run, narration, recording, editing, encoding, uploading. Show it, do not branch on it.
percentintegerThe service's own reading. It never goes backwards. Draw exactly this number: nothing is interpolated, and a client that animates between readings is inventing progress.0 to 100
atstring | nullWhen it was last updated, ISO 8601. A reading that has stopped moving means the work has, not that the bar is broken.
errorobject | nullPresent only while state is failed.
codestringThe job's own failure code, or "unknown" when none was recorded. url_private is the render specific one worth knowing: the target was private and nothing ran.
reasonstring | nullA longer word where there is one.
retryablebooleanWhether sending the same request again could plausibly work.
terminalbooleanAlways true. A failed job does not resume.
resultobject | nullPresent only while state is succeeded.
artifactsarrayWhat was produced, each with a type, a short lived signed url, an expiresAt, bytes and a sha256. Any of the last four may be null when the kind does not record it. Fetch the URL directly; the bytes are never proxied through this API.
durationSecondsnumber | nullHow long the finished video is. Null for a kind that does not produce one.
costobjectWhat this job held and what it was charged, in whole seconds of video time.
secondsReservednumberHeld when the job was queued. 0 for work that spends nothing, such as planning a storyboard or scanning a site.
secondsChargednumber | nullNull until it settles. On success it is the finished file's measured length, which can be less than was reserved and the difference goes back. On a failure or a cancel it is 0.
watermarkbooleanTrue when any of the seconds came out of the free bucket, which means the file carries the free tier mark. Free seconds are spent last, so a video that paid time covers whole is never watermarked.
artifactsExpiredbooleanThe work succeeded and its files have since been deleted at the end of the retention period. The outcome still reads succeeded, because it did; the download is gone.
cancellablebooleanTrue while the state is not terminal, for every one of the six kinds. It is a truthful answer, not a constant: it means there is an operation that stops this and a settlement that hands the money back.
idempotencyKeystring | nullThe key this job was created under, when it was created with one.
requestIdstring | nullThe request that created it, where one was kept.
createdAtstring | nullWhen it was queued, ISO 8601.
startedAtstring | nullWhen work started. Null while queued.
finishedAtstring | nullWhen it settled. Null until then.
expiresAtstring | nullWhen its files will be deleted at the end of the retention period. Download before this.
pollAfterMsnumber | nullHow long to wait before asking again: 5000 while queued, 2500 while running, null on every terminal state. Null is the signal to stop polling.

The envelope is nested under a key named for the resource: storyboard, render, source, generation, export. Beside it the read may carry the resource's own detail, for example a storyboard's beats or a project's revision. The create answer nests it under resource.

Polling

Wait pollAfterMs, ask again, stop on a terminal state. That is the whole loop, and both timings come from the server so they can be tuned without a client change.

BASE=https://api.gogoscreen.com/api/v1
AUTH="Authorization: Bearer $GOGOSCREEN_API_KEY"

# 1. Start the work. Keep the Location header.
location=$(curl -sS -D - -o /tmp/created.json -X POST "$BASE/storyboards" \
  -H "$AUTH" -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{"url":"https://www.rfc-editor.org","hint":"Show how to search for an RFC by its number"}' \
  | tr -d '\r' | awk 'tolower($1) == "location:" { print $2 }')

# 2. Poll it, waiting what the answer told you to wait.
while : ; do
  body=$(curl -sS "https://api.gogoscreen.com$location" -H "$AUTH")
  state=$(echo "$body" | jq -r '.storyboard.state')
  wait_ms=$(echo "$body" | jq -r '.storyboard.pollAfterMs // 0')
  echo "state: $state"
  case "$state" in
    succeeded|failed|cancelled) break ;;
  esac
  sleep "$(echo "$wait_ms / 1000" | bc -l)"
done

A failed job

error.code is the job's own failure code. error.retryable says whether the same request could plausibly work a second time. cost.secondsCharged is 0: nothing is charged for a video you cannot download.

http
HTTP/1.1 200 OK

{
  "storyboard": {
    "id": "3714fd13-9e14-42ab-b1f6-537a2b5b13f4",
    "kind": "walkthrough.storyboard",
    "state": "failed",
    "internalStatus": "failed",
    "stage": null,
    "progress": null,
    "error": {
      "code": "unknown",
      "reason": null,
      "retryable": false,
      "terminal": true
    },
    "result": null,
    "cost": {
      "secondsReserved": 0,
      "secondsCharged": null,
      "watermark": false
    },
    "artifactsExpired": false,
    "cancellable": false,
    "idempotencyKey": "docs-inflight-1",
    "requestId": null,
    "createdAt": "2026-09-21T16:17:16.000Z",
    "startedAt": "2026-09-21T16:17:16.000Z",
    "finishedAt": "2026-09-21T16:17:16.000Z",
    "expiresAt": "2026-09-28T16:17:16.000Z",
    "pollAfterMs": null
  },
  "title": null,
  "summary": null,
  "hint": "Show the protocol registries",
  "targetOrigin": "https://www.iana.org",
  "targetPath": "/",
  "appEntryUrl": null,
  "hadCredentials": false,
  "credentialsHeld": false,
  "beats": [],
  "stuckSteps": [],
  "coverage": { "uncovered": [], "wider": [] },
  "lengthFit": null
}
GET /storyboards/{id} for a storyboard that failed. Executed. The envelope is under storyboard; the resource's own detail sits beside it.

Cancelling, and what happens to the seconds

Every kind has a cancel operation. What it does depends on how far the work has got, and each one says so plainly.

CancelQueuedRunningWhat happens to the money
renders.cancelCancelled before it starts.Stops at its next checkpoint.The reservation is released whole. secondsCharged is 0.
storyboards.cancelCancelled before it starts.Stops at the next step boundary.Nothing to refund. Planning holds no video time.
marketing.exports.cancelCancelled before it starts.Stopped.The reserved time is handed back whole. A cancel that races a finish never moves the money twice.
marketing.generations.cancelStopped.A step already under way finishes first. A late cancel is a 200 with the generation unchanged.Generating holds no video time.
marketing.sources.cancelStopped.Stopped. The source itself stays: with a published manifest it goes back to succeeded, without one it is failed.Scanning holds no video time.
http
HTTP/1.1 200 OK
RateLimit-Limit: 120
RateLimit-Remaining: 115
RateLimit-Reset: 60
Cache-Control: private, no-store

{
  "resource": {
    "id": "3f2b0c1a-9d4e-4a61-b8c2-000000000025",
    "kind": "walkthrough.render",
    "state": "cancelled",
    "internalStatus": "cancelled",
    "stage": null,
    "progress": null,
    "error": null,
    "result": null,
    "cost": {
      "secondsReserved": 30,
      "secondsCharged": 0,
      "watermark": false
    },
    "artifactsExpired": false,
    "cancellable": false,
    "idempotencyKey": "render-1790012026461",
    "requestId": null,
    "createdAt": "2026-09-21T11:21:00.000Z",
    "startedAt": "2026-09-21T11:21:00.000Z",
    "finishedAt": "2026-09-21T11:21:00.000Z",
    "expiresAt": null,
    "pollAfterMs": null
  },
  "released": true,
  "removedFromQueue": false
}
POST /renders/{id}/cancel on a queued render. secondsCharged is 0, released says the hold went back, and the envelope is the same shape as every other answer.

Cancelling something that has already settled is 409 already_settled and changes nothing. Read the resource to see what it actually did.

http
HTTP/1.1 409 Conflict

{
  "error": {
    "code": "already_settled",
    "message": "That operation has already finished.",
    "details": { "status": "failed" },
    "requestId": "bd17a468-a784-400c-98b3-46693bbdffbf"
  }
}
POST /storyboards/{id}/cancel on a storyboard that had already failed. Executed.

A cancel is a mutating operation, so it takes an Idempotency-Key like every other one. Sending the same cancel twice under one key replays the first answer rather than producing the 409.

Or stop polling entirely

Register a webhook endpoint and every terminal transition is posted to you with the same envelope as its data. There is no event for queued or running on purpose: an event per stage would be a stream, not a notification, and it would save you no polls at all. Webhooks has the details.