Skip to main content

Jobs

Long-running operations (post generation, knowledge ingest, file source ingest, image generation) run as async jobs. The endpoint that submits one returns a Job 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:
  1. 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’s details carry the scope, percentage used, and the reset time (Mondays 00:00 UTC).
  2. 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’s details carry capCents, spentCents, and the estimate that didn’t fit. Resets at 00:00 UTC. Current caps for the plans that can reach the API: Trial 1,Advanced1, Advanced 20, Business 100,grandfatheredAgency100, grandfathered Agency 100, Enterprise uncapped (values may change; the error payload is authoritative).
Estimates are reserved up front and reconciled with actual provider cost after the job runs. Only 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.