Jobs
Long-running operations (post generation, knowledge ingest, file source ingest, image generation) run as async jobs. The endpoint that submits one returns aJob envelope; you poll GET /v1/jobs/{jobId} —
or subscribe a webhook to job.completed /
job.failed — until the status leaves QUEUED/RUNNING.
Three submitting endpoints implement wait_for_completion_ms (server
cap 25,000 ms), which holds the connection for short jobs:
POST /v1/posts/generations, POST /v1/media/generations, and
POST /v1/media/carousels (though a carousel deck runs two to three
minutes, so waiting rarely helps there — poll instead).
POST /v1/knowledge and the file form of POST /v1/sources ignore the
field silently rather than rejecting it — you get the Job envelope
back immediately, so poll or subscribe a webhook for those two. The MCP
twins wait at most 18 seconds and answer with a RUNNING job to poll —
see MCP tools §5.
The jobs family needs no particular scope — any valid token can list,
read, and cancel the workspace’s jobs.
Job types
The type list grows additively — treat unknown values as opaque.
CAROUSEL_GENERATION’s documentKey is the S3 key of the rendered PDF —
pass it to PATCH /v1/posts/{postId}/media as
{ "kind": "document", "document": { "key": "…" } } to put the deck on a
post. mediaId is the same deck as a media-library asset, so it also shows
up in GET /v1/media and can be reopened in the carousel editor.
imageSlots is { pending, filled, budgetDeclined, failed } — how the
deck’s generated-image slots resolved. pending is how many the composed
deck asked for; anything not filled shipped with the design’s own stand-in
art, either because the workspace’s daily AI-spend cap ran out mid-job
(budgetDeclined) or because the image provider failed (failed). A
stand-in looks exactly like a design that wanted no imagery, so this is the
only way to tell a partially-filled deck from a complete one.
GET /v1/jobs
List jobs in the workspace, newest first.
The list endpoint omits live progress to stay cheap; fetch a single job
for the freshest snapshot.
GET /v1/jobs/{jobId}
Single job + latest progress snapshot.
progress is null when the job hasn’t started or has already
finished (progress snapshots expire an hour after the job settles).
Polling cadence: once per second while RUNNING is fine for
POST_GENERATION; ingest types emit fewer updates — back off to every
5 seconds after the first 30. Better: subscribe a
webhook to job.completed and skip polling entirely.
MCP hosts get progress pushed via
notifications.
POST /v1/jobs/{jobId}/cancel
Cancel a QUEUED job. Any other status — RUNNING (workers don’t poll
for cancellation mid-run), DONE, FAILED, or already CANCELLED
— returns 409 not_cancellable. Re-cancelling therefore leaves the job
untouched but does not return a quiet 200: treat the 409 on an
already-CANCELLED job as success if you are retrying a cancel.
The response is the freshly-cancelled job with status: "CANCELLED".
AI budget
Every job that runs an AI workload is gated at submit time by two budgets:- The weekly usage limit — the primary limit, shared with the
dashboard’s AI features. When the workspace pool (or a per-project
budget) is exhausted, submits fail with
402 usage_limit_exceeded; the error’sdetailscarry the scope, percentage used, and the reset time (Mondays 00:00 UTC). - The daily API spend cap — an API-only abuse valve, per workspace
per UTC day. Exceeding it fails with
402 spend_cap_exceeded; the error’sdetailscarrycapCents,spentCents, and the estimate that didn’t fit. Resets at 00:00 UTC. Current caps for the plans that can reach the API: Trial 20, Business 100, Enterprise uncapped (values may change; the error payload is authoritative).
POST /v1/media/generations reserves
atomically — its check and increment are one conditional statement, so
concurrent submits cannot jointly exceed the cap and the losers receive
the 402. The other producers (post generation, knowledge ingest, file
source ingest) read the bucket and then increment it, so two requests
racing on the last few cents can both pass the read and overshoot the
cap slightly. That is deliberate: the two-step path was built for ingest
verbs that cost at most a few cents each, where a small overshoot is
acceptable. Image generation is the most expensive per-call verb in the
catalog, which is why it got the atomic path instead.
A 402 is never retryable in a loop: wait for the reset, or route the
work through the dashboard. GET /v1/usage (MCP:
get_usage) reports both meters — and a canGenerate verdict — before
either fires, so a client never has to discover a limit by spending a
job on it.
CAROUSEL_GENERATION reserves in two phases, because how much imagery a
deck needs is only known once the deck has been composed: the pre-flight
reservation at submit, then each further image slot’s own cost mid-job,
before that slot is generated. A slot the cap refuses does not fail the
job — it keeps the design’s stand-in art and is counted in the result’s
imageSlots.budgetDeclined. The cap is never overshot.