For apps: the project API
An app talks to Cloud Console through /v1, with one of its project's API keys. With a key, an app can:
- create, read and delete tasks
- check its key
- turn its project's maintenance switch on or off
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
- Send the key as
Authorization: Bearer cck_<43 characters>. - A request carries
content-type: application/jsonand a JSON object (else 415, or 400). A create is at most 256 KB (else 413). - Percent-encode the names in the path:
{p}the project,{q}the queue and{t}the task. - Every answer is JSON with
ok, and is never cached. - A missing, unknown or revoked key gets 401. An expired key gets 401
This API key has expired. Another project's key gets 403.
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
| Method | Path | What it does | Answers |
|---|---|---|---|
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}/tasks | Creates 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}/maintenance | Turns 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.
| Field | Needed | Rule |
|---|---|---|
name | Optional | 1 to 500 letters, digits, _ and -. Left out, the queue makes one (43 random base64url characters). Names are deduplicated per queue (below). |
scheduleTime | Optional | ISO 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. |
method | Optional | POST (the default), GET, HEAD, PUT, DELETE, PATCH or OPTIONS. |
url | Required | http:// 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/. |
headers | Optional | An object of text values, at most 50, each passing the header rules. No one sets Authorization here. {} when left out. |
body | Optional | Text (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. |
auth | Optional | One 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. |
attemptDeadline | Optional | 15 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
| Behaviour | What the app sees | Detail |
|---|---|---|
| Names are deduplicated | 409 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 quota | 429 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 key | 429 | 600 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 redirects | No 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 refused | 201, as usual | A 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
- What it does. It holds dispatch while a deploy or a database migration runs. Tasks are still accepted (201, counted against the quota) and wait. Scheduled job runs are skipped and logged, and runs under way wait.
- Safe to repeat. Switching on what is on answers the state as it is, with no new audit line or alert.
- At most one change every 10 seconds per switch, whoever asks: 429
rate_limited. - A key turns off only a key's hold. A hold a person switched on answers 409 and stays on. People with the
pausepermission can turn off any hold, a key's too. - Recorded. Each change writes
maintenance.onormaintenance.offwith the actorkey:<name>, and sends an alert. A switch left on for 24 hours sends one more.
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.
-
FlowDeck creates a taskA
POSTto/v1/projects/flowdeck/queues/<queue>/taskswith its project key. Its queues areemail,push,aiandtimer.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. -
What the task holdsA
POSTto FlowDeck's/api/workerwith a JSON body: the job's name and a payload FlowDeck seals itself. The task may have a name and ascheduleTimein ISO 8601. Its auth is OIDC as the project's service accountflowdeck-tasks, with the worker's URL as the audience.A missing queue answers 404, and FlowDeck runs the job itself. -
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. -
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.
-
FlowDeck checks the tokenIts
/api/workerverifies the token,subandemailincluded. Its verifier fetches the JWKS again for akidit lacks.2xx: the task is done.
- Its setup is seeded. A database migration makes the project
flowdeck, its queues, the service account and ten scheduler jobs, so none of it is made by hand. Every job is a POST to the same/api/workerwith OIDC asflowdeck-tasks, in UTC, with a 60-second attempt deadline. No secret is seeded. - Its settings, by name.
CONSOLE_URL,CONSOLE_PROJECT,CONSOLE_API_KEYandCONSOLE_SERVICE_ACCOUNT. FlowDeck sends tasks to the console only when all four are set. - Its share of the quota. The jobs run 1,417 times a day (1,418 on Sundays). That leaves about 3,580 of the default 5,000 for FlowDeck's tasks and their retries.
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>.
| Part | Value |
|---|---|
Header alg, typ | ES256, JWT |
Header kid | A key in the console's JWKS |
iss | The console's URL (its CONSOLE_URL setting) |
aud | The audience the task or job gave, or else the target URL without its query |
sub, email | Both the service account's email, <name>@<project>.cloud-console |
iat, exp | When it was issued, and 300 seconds later |
jti | The token's own id |
How the target checks it
- Find the key by
kidin/.well-known/jwks.json. The discovery document is at/.well-known/openid-configuration. - Check the signature, with
algES256. - Check
iss: the console's URL. - Check
aud: its own URL, or the audience it was given. - Check
exp, andiat. - Check
suboremailagainst 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_KEYis not published a day ahead. So a verifier that keeps the JWKS must fetch it again for akidit lacks, as FlowDeck's does.
What the target answers
- Only a 2xx succeeds. A 3xx is not followed, and fails.
- A status the queue or job never retries fails the task at once. By default that is 400, 401, 403, 404, 410 and 422.
- 408, 429, 5xx, and attempts with no answer, are retried. A
Retry-Afterdelays the next try, up to the maximum backoff. - The target must answer within the attempt deadline: 3 minutes by default. Of its answer, at most 1 MB is read and 1 KB kept.
- With the HMAC option instead, the target recomputes
X-Console-Signature:v1=and the lower-case hex HMAC-SHA256 of<timestamp>.<METHOD>.<url>.<body>. The URL is as stored and called; the body is as sent, empty for none. It rejects anX-Console-Timestampolder than 5 minutes.
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.
| Method | Path | Who | Answers |
|---|---|---|---|
POST | /api/projects/{p}/queues/{q}/tasks/batch-create | A signed-in person: session, CSRF token, create | 200 { 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 file is
{ "tasks": [ … ] }: 1 to 50 tasks, each the JSON of a single create, as above. - The whole file is checked before anything is made. One problem anywhere means 400 and no tasks, with each message at its path, such as
tasks.3.url. - The quota is checked for the whole file at once. One request to the queue then makes the tasks in the file's order.
- A name the queue still remembers is refused for that task only, and its quota unit is given back.
- It writes one audit line,
tasks.create. It counts one change per 10 tasks against the person's hourly 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.