Skip to content

Guides

Authentication

Every call to the public API carries one API key. The key acts for one account, spends that account's video time, and holds only the scopes it was issued with.

The two headers

A key goes in either header. They are the same credential and the same actor; two headers exist because some MCP hosts and proxies send one and not the other.

http
Authorization: Bearer gsk_live_4Qh2vNc8KsPbR6wTzA1eLmXy9DgJfUoV3nIkH7Zt

X-API-Key: gsk_live_4Qh2vNc8KsPbR6wTzA1eLmXy9DgJfUoV3nIkH7Zt

A third credential exists and is not for you: the dashboard's own developer console sends a dashboard session token in Authorization: Bearer, so it can call this API with the session a person is already signed in to. A user credential holds every scope the account has, including keys:manage and assistant:use.

X-API-Key carries an API key and nothing else. A value in it that does not start with gsk_ is refused, even when it is a valid dashboard session token, because a session token is accepted only in Authorization.

Check a key works

GET /account is the cheapest call on the surface that proves a credential. It answers who the key acts for, the account's seconds balance, and the scopes the key actually holds.

curl https://api.gogoscreen.com/api/v1/account \
  -H "Authorization: Bearer $GOGOSCREEN_API_KEY"
json
{
  "account": {
    "id": "3f2b0c1a-9d4e-4a61-b8c2-000000000001",
    "email": "docs-funded-d81d104d@docs.local",
    "name": "Docs examples",
    "emailVerified": true,
    "role": "user",
    "createdAt": "2026-09-21T11:00:00.000Z",
    "deletionStatus": "active"
  },
  "wallet": {
    "planSeconds": 0,
    "planId": null,
    "planExpiresAt": null,
    "planActive": false,
    "paygSeconds": 542,
    "freeSeconds": 0,
    "reservedSeconds": 0,
    "availableSeconds": 542
  },
  "credential": {
    "kind": "api_key",
    "apiKeyId": "3f2b0c1a-9d4e-4a61-b8c2-000000000002",
    "scopes": [
      "account:read",
      "catalog:read",
      "marketing:export",
      "marketing:read",
      "marketing:write",
      "renders:read",
      "renders:write",
      "storyboards:read",
      "storyboards:write",
      "usage:read",
      "wallet:read",
      "webhooks:manage"
    ]
  }
}
200. Ids are replaced with stable stand ins and the address belongs to a test account; the shape is the real one.

What a key looks like

A key is gsk_live_ followed by 40 base62 characters, about 238 bits of randomness. There is no test prefix and no sandbox: a key is live or it does not exist.

The server does not keep the key in a readable form. What it keeps in the clear is the prefix, which is the scheme plus the first eight characters, and the last4, so a key can be recognised in a list and in an audit trail. Nothing can give the plaintext back.

text
gsk_live_4Qh2vNc8KsPbR6wTzA1eLmXy9DgJfUoV3nIkH7Zt
└───────┘└──────────────────────────────────────┘
 scheme    40 base62 characters

prefix stored in the clear:  gsk_live_4Qh2vNc8   (17 characters)
last4  stored in the clear:  H7Zt

Where a key is created

In the developer console at app.gogoscreen.com/dashboard/developers, signed in. Key management is a user action and is not reachable with a key, so there is no bootstrap problem to solve: the first key is made by a person.

API and MCP access comes with any paid plan or a top up. Creating or rotating a key needs one, and every call a key makes checks it again: an account with no live plan and no top up time left is answered 403 paid_account_required. Nothing is revoked; the same key works again as soon as the account has paid time. The free seconds given at signup do not count.

The Create a key form takes three things.

NameTypeDescription
NamestringWhat will use the key. It appears in the key list and in the account's audit trail, and it is the only way to tell two keys apart later.max 120 chars
ExpiresdateOptional. The key stops working at the end of that day. Leave it empty for a key that does not expire.must be in the future
What this key may doscope[]One checkbox per grantable scope (12 of them), each with its own line. The two that spend video time carry a Spends time badge on the picker rather than a surprise on the bill.at least one of the 12

An account may hold 25 active keys. The console shows the count beside the list. A 26th is refused with 409 conflict and details.reason: "too_many_keys".

The 14 scopes

A scope is a promise to the account about what a credential handed to somebody else can do with it. The list is deliberately short and it is split by what a thing costs: reading is one scope, writing is another, and the two that spend the account's seconds are named apart from the writes that do not. An operation that declares two scopes needs both, never either.

The sentences below are the API's own descriptions of each scope, verbatim.

The API's own scope descriptions, verbatim.
ScopeSpends secondsA key may hold itWhat it allows
catalog:readNoYesRead the catalogue: voices, plans, render options and marketing templates. Costs nothing and reveals nothing about the account.
account:readNoYesRead the account's own profile — id, email, plan, and whether the email is verified.
wallet:readNoYesRead the seconds balance and the wallet ledger.
usage:readNoYesRead this credential's API usage counts.
storyboards:readNoYesRead walkthrough storyboards and their plans.
storyboards:writeNoYesPlan a walkthrough storyboard from a URL, and cancel one. Spends no seconds.
renders:readNoYesRead renders and fetch a finished video's download URL.
renders:writeYesYesSPENDS SECONDS. Start a walkthrough render and cancel one.
marketing:readNoYesRead marketing sources, projects, revisions, exports and the brand kit.
marketing:writeNoYesScan a URL, generate and edit marketing projects, and write the brand kit. Spends no seconds.
marketing:exportYesYesSPENDS SECONDS. Export a marketing project to video and cancel an export.
webhooks:manageNoYesRegister, change and delete webhook endpoints, send a test delivery, and read the delivery log.
keys:manageNoNoCreate, read, revoke and rotate API keys. NEVER grantable to a key — a key that can mint keys outlives its own revocation.
assistant:useNoNoAsk the in-app support assistant about this product and read your own conversations with it. NEVER grantable to a key — it is a person's surface. Questions to the assistant count toward the account's daily assistant allowance, and messages to a person toward their own hourly limit; neither spends seconds.

Two scopes spend seconds: renders:write and marketing:export. Everything else either reads, or writes without touching the account's seconds. A key that carries neither can plan, edit and read all day and can never take a minute of video out of the balance.

keys:manage and assistant:use can never be held by a key. A key that could mint keys would outlive its own revocation: revoke it, and the key it made yesterday is still working. The scope is not in the grantable list, so a create that asks for it fails validation, and every keys.* operation refuses an API key whatever scopes it claims. assistant:use is the support assistant in the dashboard, a person's surface, and the assistant.* operations refuse an API key the same way.

Per key limits

A key may carry its own ceilings. Both are lower bounds only: they can never ask for more than the rate class allows, or a key would be a self service quota increase. The lower of the two always wins.

NameTypeDescription
rateLimitPerMinuteinteger | nullA per minute ceiling for this key across every rate class. Null means the class's own limit applies.Default null1 to 600
rateLimitPerDayinteger | nullA per key daily cap, counted in UTC days. Null means the key is not capped by the day. 864000 is the per minute ceiling run flat out for a whole day, so nothing above it could ever bind.Default null1 to 864000

A 429 from the daily cap carries details.scope: "day". See Rate limits for the three ceilings and how to back off.

Rotation, revocation and expiry

Rotate issues a new key with the same name, scopes and limits, and answers the new plaintext once. graceSeconds decides how long the old key keeps working, from 0 to 86400. With a grace window, a fleet picks the new value up without an outage; with 0 the old key dies immediately. The console offers three: immediately, in an hour, in a day.

Revoke kills a key now. It stops working everywhere within a second, for every request that follows.

Expiry is the date set at creation. An expired key answers 401 unauthorized with details.reason saying so, and within the hour its status becomes expired and it stops counting against the 25.

A revoked or expired key never starts working again. There is no un-revoke; issue a new key.

The two refusals

401 unauthorized means the credential could not be resolved. details.reason says which of the three ways, and it is the only field worth branching on.

http
HTTP/1.1 401 Unauthorized
RateLimit-Limit: 60
RateLimit-Remaining: 55
RateLimit-Reset: 44

{
  "error": {
    "code": "unauthorized",
    "message": "Authentication is required.",
    "details": { "reason": "no_credential" },
    "requestId": "4ef6ff50-5441-4d2f-bc44-65cf56160851"
  }
}
No header at all.
http
HTTP/1.1 401 Unauthorized

{
  "error": {
    "code": "unauthorized",
    "message": "Authentication is required.",
    "details": { "reason": "unknown_key" },
    "requestId": "51b3a5e6-c891-4e3a-8864-94b052fb7c2d"
  }
}
A key that is unknown, revoked or expired.
http
HTTP/1.1 401 Unauthorized

{
  "error": {
    "code": "unauthorized",
    "message": "Authentication is required.",
    "details": {
      "reason": "api_key_required",
      "hint": "X-API-Key carries an API key (gsk_live_\u2026). A dashboard session token goes in Authorization: Bearer."
    },
    "requestId": "14255480-adba-4751-ada7-e19a78e258d6"
  }
}
An X-API-Key header carrying something that is not a key.

Note the RateLimit-* headers on the refusals. An unauthenticated caller is metered at 60 failures a minute per IP address, and the trio says how many are left before the 401 becomes a 429.

403 forbidden_scope means the credential resolved and may not do this. details.requiredScopes and details.missingScopes name exactly what is short. A key's scopes cannot be changed after it is issued, so the fix is a new key.

http
HTTP/1.1 403 Forbidden
RateLimit-Limit: 120
RateLimit-Remaining: 120
RateLimit-Reset: 60

{
  "error": {
    "code": "forbidden_scope",
    "message": "This credential does not carry the scope this operation needs.",
    "details": {
      "requiredScopes": ["keys:manage"],
      "missingScopes": ["keys:manage"]
    },
    "requestId": "68cd298d-ad08-4b8a-a5a0-e0a87fd980b7"
  }
}
GET /keys with an API key. keys:manage is user only, so no key can ever hold it.