Cloud Console Own Projects

An app talks to Cloud Console through /v1, with one of its project's API keys. With a key, an app can:

Queues and jobs are made and managed only on the console pages. That is one of the owner's decisions: they are not made in code.

Base path

/v1/projects/{p}

A key looks like

cck_<43 characters>

Per key

600 requests a minute

Per project, by default

5,000 tasks and runs a day

What a project API key can do

A key can

  • create a task in any queue of its project
  • read a task: its state, attempts and last error
  • delete a task that is waiting or failed
  • check itself, with GET /v1/projects/{p}
  • turn its project's maintenance switch on, and off again when a key turned it on

A key cannot

  • make, edit, pause or delete queues and jobs; those are made only on the pages
  • reach another project: that answers 403
  • switch the whole console's maintenance
  • use the pages' routes under /api, such as tasks from a file

Keys have no narrower scope. Within its own project, a key may do everything /v1 offers. Keys are made, revoked and rotated on the Keys page, as Security and access describes.

Calling the API

Every refusal has the same shape. code and fields come when the refusal has them, and fields maps each field's path in the request to its message:

{ "ok": false, "error": "<one sentence for a person>", "code": "invalid",
  "fields": { "<path in the request>": "<message>" } }

Each /v1 route

MethodPathWhat it doesAnswers
GET/v1/projects/{p}Checks the key, and answers the project's name.200 { ok, project: { name } }; 401; 403
POST/v1/projects/{p}/queues/{q}/tasksCreates a task in queue {q}, with the same checks as the pages.201 { ok, task }; 400 invalid; 401; 403; 404 No such queue; 409 a name the queue remembers; 413; 415; 429 quota or a rate limit
GET/v1/projects/{p}/queues/{q}/tasks/{t}Reads a task.200 { ok, task }; 404
DELETE/v1/projects/{p}/queues/{q}/tasks/{t}Deletes a task.200 { ok, task }, now deleted; 404; 409 while it runs, or once it is done
PUT/v1/projects/{p}/maintenanceTurns the project's maintenance switch on or off: { "on": true }.200 { ok, maintenance }; 400 invalid; 401; 403; 409 a hold a person switched on; 429 a rate limit, or a change within 10 seconds of the last

Every route also answers 401 for a missing, unknown, revoked or expired key, 403 for another project's key, and 429 past the key's limits.

Create a task

The same JSON as the Create a task page. The host, project, key and target below are placeholders.

POST /v1/projects/my-app/queues/email/tasks HTTP/1.1
Host: console.example.com
Authorization: Bearer cck_<43 characters>
Content-Type: application/json

{
  "name": "welcome-email-42",
  "scheduleTime": "2026-10-23T09:00:00.000Z",
  "method": "POST",
  "url": "https://example.com/api/worker",
  "headers": { "content-type": "application/json" },
  "body": "{\"job\":\"email.send\",\"user\":42}",
  "auth": {
    "kind": "oidc",
    "serviceAccount": "my-app-tasks@my-app.cloud-console",
    "audience": "https://example.com/api/worker"
  },
  "attemptDeadline": "3m"
}

The answer is 201 { "ok": true, "task": { … } }: the task as stored, in state queued until its time. Times come back as Unix milliseconds (UTC). For the OIDC option, https://example.com must be among the service account's allowed origins.

FieldNeededRule
nameOptional1 to 500 letters, digits, _ and -. Left out, the queue makes one (43 random base64url characters). Names are deduplicated per queue (below).
scheduleTimeOptionalISO 8601 text with Z or an offset, or Unix milliseconds. At most 30 days ahead. Left out or in the past, the task is due now.
methodOptionalPOST (the default), GET, HEAD, PUT, DELETE, PATCH or OPTIONS.
urlRequiredhttp:// or https://, at most 2,048 characters. It must pass the target rules: no private, loopback or metadata address, and not the console itself. It is stored and called as standard URL parsing writes it, so https://Example.COM becomes https://example.com/.
headersOptionalAn object of text values, at most 50, each passing the header rules. No one sets Authorization here. {} when left out.
bodyOptionalText (UTF-8), sent as it is, at most 100 KB. None with GET or HEAD. No base64: send text, such as JSON. With a body and no content-type header, fetch's text/plain;charset=UTF-8 goes.
authOptionalOne of the six options, { "kind": "none" } when left out. It names a secret of this project, or a service account by its email. For Basic, Bearer, OAuth and OIDC, the URL must be at one of that credential's allowed origins.
attemptDeadlineOptional15 seconds to 15 minutes, as a number of seconds or as text ("90s", "5m"). "3m" when left out.

Unknown fields are refused: 400, fields.<name>. A refusal names each field by its path, such as auth.secret or headers.X-Foo. The six auth options, and what each sends, are on Security and access.

Behaviour an app relies on

BehaviourWhat the app seesDetail
Names are deduplicated409 A task named x is in this queue, or succeeded or was deleted within the last day. Nothing is added.The queue remembers a name while its task waits, runs or is failed (up to 7 days), and for 24 hours after it succeeded or was deleted. So a named task sent twice is made once. FlowDeck treats 409 as done.
The daily quota429 with code: "quota". Nothing is added.Each project's task creations, job runs and retries are counted per UTC day: 5,000 by default, set by an admin. The same answer comes while the console as a whole is held at a free-plan limit, until 00:00 UTC.
Rate limits per key429600 requests a minute per key. 600 task reads, deletes and repeated names an hour per key; a create that answers 409 counts there. An address that sends more than 120 unknown or revoked keys in a minute gets 429 before any key is looked up.
No redirectsNo 3xx, ever/v1 never answers with a redirect. When the console calls a target, it does not follow one: a 3xx is a failed attempt.
Held, not refused201, as usualA paused queue, or a project under maintenance, still takes new tasks. They wait, and are sent once the queue resumes or the switch is off.

The maintenance switch

Around a deploy, the switch would be used like this (placeholders throughout):

curl -fsS -X PUT "https://console.example.com/v1/projects/my-app/maintenance" \
  -H "Authorization: Bearer cck_<43 characters>" -H 'content-type: application/json' -d '{"on":true}'
# ... migrate and deploy ...
curl -fsS -X PUT "https://console.example.com/v1/projects/my-app/maintenance" \
  -H "Authorization: Bearer cck_<43 characters>" -H 'content-type: application/json' -d '{"on":false}'

For FlowDeck's own deploys this is described, not wired.

How FlowDeck uses it

FlowDeck sends its background work to the console as tasks, and its timers come from scheduler jobs. Its client is createTask in its api/src/shared/jobs/job.service.ts, and the API matches it exactly.

  1. FlowDeck creates a taskA POST to /v1/projects/flowdeck/queues/<queue>/tasks with its project key. Its queues are email, push, ai and timer.
    Anything but a 2xx or 409, or no answer in 10 seconds: FlowDeck runs the job itself. A task with a delay, such as a timer's follow-up, is logged and dropped instead.
  2. What the task holdsA POST to FlowDeck's /api/worker with a JSON body: the job's name and a payload FlowDeck seals itself. The task may have a name and a scheduleTime in ISO 8601. Its auth is OIDC as the project's service account flowdeck-tasks, with the worker's URL as the audience.
    A missing queue answers 404, and FlowDeck runs the job itself.
  3. The console checks and counts itThe key, the fields, the allowed origin and the project's daily quota.
    409: FlowDeck takes it as done. 429 quota: FlowDeck runs the job itself.
  4. The queue calls FlowDeckAt the task's time, with a fresh OIDC token, within the queue's rate and retry settings.
    A failed answer is retried as the queue's settings say.
  5. FlowDeck checks the tokenIts /api/worker verifies the token, sub and email included. Its verifier fetches the JWKS again for a kid it lacks.
    2xx: the task is done.

The OIDC token a target receives

With the OIDC option, the console signs a short-lived token for the task's service account and sends it as Authorization: Bearer <JWT>.

PartValue
Header alg, typES256, JWT
Header kidA key in the console's JWKS
issThe console's URL (its CONSOLE_URL setting)
audThe audience the task or job gave, or else the target URL without its query
sub, emailBoth the service account's email, <name>@<project>.cloud-console
iat, expWhen it was issued, and 300 seconds later
jtiThe token's own id

How the target checks it

  1. Find the key by kid in /.well-known/jwks.json. The discovery document is at /.well-known/openid-configuration.
  2. Check the signature, with alg ES256.
  3. Check iss: the console's URL.
  4. Check aud: its own URL, or the audience it was given.
  5. Check exp, and iat.
  6. Check sub or email against the service account it expects. Every project's tokens share the issuer and the signing key, so this check is what tells the projects apart.

Any JWT library does this, for example jose. No shared secret is needed.

Keeping the keys

  • The JWKS and the discovery document may be cached for an hour.
  • Keys rotate every 90 days. The next key appears in the JWKS a day before it first signs, and a retired key stays an hour.
  • A key replaced after a change of SEAL_KEY is not published a day ahead. So a verifier that keeps the JWKS must fetch it again for a kid it lacks, as FlowDeck's does.

What the target answers

Tasks from a file

This is a route for people on the console pages, not for apps. It is not under /v1, and an API key cannot call it. It needs a signed-in session, its CSRF token, and the create permission in the project. An app creates tasks one request at a time, through POST /v1/.../tasks.

MethodPathWhoAnswers
POST/api/projects/{p}/queues/{q}/tasks/batch-createA signed-in person: session, CSRF token, create200 { ok, done, refused, label }; 400 invalid at the task's path in the file; 403; 404; 413 over 256 KB; 429 quota or the person's change budget

The full write-up, with the page and its sample file, is Tasks from a file.

Cloud Console, as it is on main. 22 Oct 2026.