Skip to content

Endpoints

Every route /api/v1 serves, and the scope each one needs. A test walks the router and fails if a route is missing from this page, or if this page names a scope the code does not enforce — so the two cannot drift.

Paths are relative to https://<your-api-host>/api/v1. See API for auth, the response envelope, pagination, and error codes.

Two routes sit outside the table below, because neither takes a scope:

  • GET /health lives at the root, outside /api/v1, and needs no auth.
  • GET /api/v1/invitations/:invitationId needs no auth either. It is the public read behind an invitation link — the organization’s name, the inviter, the role, and whether the invitation is still pending — so the page can be shown to someone who has no account yet. Accepting an invitation is not here; that lives on the auth routes.

session means no API key reaches it, whatever its scopes — a browser session cookie only. Those routes either rotate an account credential or mint a new one, and a key can never mint another credential.

Everything else names the single scope a key must carry.

Every request body is validated against a schema before the handler runs. A body that fails answers 400 invalid_request, and the message names the field:

{
"ok": false,
"error": {
"code": "invalid_request",
"message": "enabled: Invalid input: expected boolean, received string"
}
}

Unknown fields are stripped, not rejected — adding a key you don’t recognize will not break your integration. Missing, mistyped, or out-of-range fields are rejected. Omitting the body entirely is the same as sending {}, which succeeds wherever every field is optional.

Query parameters go through the same check, and a bad one answers the same 400 invalid_request naming the parameter. Unknown parameters are stripped.

limit is not clamped. On an endpoint that accepts 1–100, ?limit=200 is an error rather than a silent 100, and ?limit=abc is an error rather than a silent default.

Parameter Where Rule
limit GET /projects Whole number, 1–100. Defaults to 50.
limit Ticket lists Whole number, 1–500. Defaults to 100, except on GET /tickets, where omitting it returns every ticket.
limit Capability lists Whole number, 1–1000. Defaults to 100.
limit GET /activity Whole number, 1–200. Defaults to 50.
projectId GET /activity, GET /activity/export Narrows the log to one project.
from, to GET /activity, GET /activity/export Milliseconds since the epoch, inclusive at both ends. Either may stand alone.
fresh GET /domain/status 1 re-reads the zone from Cloudflare instead of the cached view.
q GET /projects At most 48 characters. Each % or _ costs two, because the search escapes them before D1 sees the pattern.
q, label GET /tickets/search At most 100 characters.
status GET /tickets/search One of the ticket statuses. An unknown one is an error, not an empty result.
active GET /tickets/tree 0 or 1.
parent GET /tickets/tree A ticket id, or root.

GET /connections/:provider/callback is the one endpoint whose parameters are not validated this way: it is an OAuth redirect target, so a bad code or state sends the browser back to the console with an error rather than answering 400.

Endpoint Scope What it does
GET /projects projects:read Keyset-paginated. Takes limit (1–100), cursor, and q (substring match on name). The only paginated endpoint.
GET /projects/:projectId projects:read One project, by id.
POST /projects projects:write Create a project. name is optional; omit it and the project is created as “Untitled project” for the build agent to name. Provisions nothing. Honors Idempotency-Key. Answers 402 payment_required at your plan’s project cap — Free 10, Starter 50, Pro 200, Enterprise unlimited.
PATCH /projects/:projectId projects:write Rename a project with name, or set the address it answers on with slug. The slug follows the name until you set one yourself, and locks when the first commit creates the repo under it — after that only a custom domain changes the address. A slug is lowercase letters, digits and single dashes, must be unique in the organization, and cannot end in -preview, -admin, -live or -assets, which name the workers deployed alongside it.
DELETE /projects/:projectId deploy:write Delete the project and tear down its Cloudflare resources. Leaves your repo untouched. Teardown is bounded, so a project holding a large bucket answers with teardown.remaining above zero and the project still standing — call it again until remaining is zero, which is when the project itself goes.
POST /projects/:projectId/archive deploy:write Take the app offline and free its live-project slot. Deletes nothing — the database, files, code and history are kept. Returns the archived project.
POST /projects/:projectId/unarchive deploy:write Restore an archived project. It comes back offline; Ship to serve it again.
GET /projects/:projectId/source projects:read The files the agent last generated, and what generated them. data is null before the first build.
GET /projects/:projectId/files projects:read The app’s file paths, without their contents — { "paths": [...] }. Use this to list the source tree; use /source when you need the code itself.

The ticket is the only unit of work. A plain sentence, a plan-thread decision, and an ingested issue all land as one — with the words you typed kept on ticket.origin.

Endpoint Scope What it does
POST /projects/:projectId/tickets/capture tickets:write Capture a ticket from a plain sentence, kept verbatim on ticket.origin. Honors Idempotency-Key. 412 model_required when the organization has no model connected — the ticket is only worth capturing if an agent can act on it.
GET /projects/:projectId/tickets projects:read List tickets. Takes limit.
GET /projects/:projectId/tickets/:ticketId projects:read One ticket, by id.
POST /projects/:projectId/tickets tickets:write Create a ticket.
PATCH /projects/:projectId/tickets/:ticketId tickets:write Update status, type, priority, dueAt, title, body, parent, or labels.
GET /projects/:projectId/tickets/stats projects:read Ticket counts.
GET /projects/:projectId/tickets/tree projects:read Children of a ticket. Takes parent (or root), cursor, limit, active=1.
GET /projects/:projectId/tickets/search projects:read Takes q, status, label, cursor, limit.
GET /projects/:projectId/tickets/labels projects:read Every label in use on this project.
POST /projects/:projectId/tickets/rollups projects:read Rollup counts for the ticket ids in { "ids": [...] }, up to 500 of them — a page’s worth, the same ceiling limit has. A read; it is a POST only because the ids travel in the body.
Endpoint Scope What it does
GET /projects/:projectId/readme projects:read The product’s README.md — what it is, and what it will not do.
GET /projects/:projectId/plan/messages projects:read The brainstorm thread, newest first — { "items": [...], "nextCursor": ..., "total": ... }. Defaults to the latest 100; page back with ?cursor= (from nextCursor) and ?limit= (max 500).
POST /projects/:projectId/plan/message tickets:write Post to the thread; the Plan agent answers and may write tickets. 412 model_required with no model connected.
POST /projects/:projectId/plan/stream tickets:write The same turn, streamed as server-sent events: delta frames (text, reasoning, tool chars), a tool frame per tool call, then done with the persisted reply — or a terminal error frame with a kind. Body { "retry": true } re-runs the last failed exchange instead of posting new text.
POST /projects/:projectId/plan/stop tickets:write Stop the plan turn that is running now. The turn’s reply is recorded as stopped.
POST /projects/:projectId/plan/retry tickets:write Re-run the last failed exchange in place. 409 while a turn runs; 400 when nothing failed.

A build often runs for several minutes, and a connection held silently open that long can be cut by proxies between you and us. The run itself is durable — it continues server-side whether or not your request survives to see the answer. So for anything beyond a quick turn, prefer /build/stream, whose event frames keep the connection alive, or fire /build, tolerate the drop, and follow the run on GET /runs until its status settles.

That durability covers our own deploys too. A platform restart mid-build pauses the run and resumes it within seconds, automatically. If your stream is cut by one, reconnect with { "after": <last seq>, "run": "<run id>" }: while the resume is pending the stream answers with a { "type": "resuming" } frame and stays open, then carries the resumed run — a new run frame whose events start again at seq 1 — through to done.

Endpoint Scope What it does
GET /projects/:projectId/model projects:read The model this project builds on.
PUT /projects/:projectId/model projects:write Choose the model.
GET /projects/:projectId/runs projects:read Build runs, newest first — { "items": [...], "nextCursor": ..., "total": ... }. Defaults to the latest 100; page back with ?cursor= (from nextCursor) and ?limit= (max 500).
GET /projects/:projectId/usage projects:read Tokens the project’s agents have spent, total and per model.
GET /projects/:projectId/history projects:read The project’s history events.
GET /projects/:projectId/versions projects:read Commits in the project’s tree.
POST /projects/:projectId/versions/:sha/revert build:write Restore the tree as it stood at :sha.
POST /projects/:projectId/backup build:write Push the working tree to your repo now.
POST /projects/:projectId/preview build:write Body { "ticketId": "…" }. The current artifact plus a preview URL. To read only the files, use GET /source — it needs no write scope.
POST /projects/:projectId/tickets/:ticketId/build build:write Run the agent on a ticket. Answers with a status: built (plus files, generatedBy, previewUrl), awaiting (the agent asked questions — reply by posting again with answers), or replied (it wrote nothing and said why). 404 for an unknown ticket; 412 model_required with no model connected. The agent can ship mid-build only if the key also holds deploy:write.
POST /projects/:projectId/tickets/:ticketId/build/stream build:write The same build, streamed as server-sent events.
POST /projects/:projectId/tickets/:ticketId/build/cancel build:write Cancel the running build.
POST /projects/:projectId/tickets/:ticketId/build/steer build:write Send the agent a correction mid-build.
POST /projects/:projectId/build/queue build:write Body { "ticketIds": [...] }, 1 to 100 of them. Queue buildable tickets; they build one after another, in order, without holding a connection. Unknown ids, structure tickets, and tickets already queued or building are skipped.
GET /projects/:projectId/build/queue projects:read The queue: what is running and what is queued, in order.
DELETE /projects/:projectId/build/queue/:ticketId build:write Take a ticket out of the queue. The running build is not touched — use /build/cancel for that.
POST /projects/:projectId/preview-errors build:write Body { "ticketId": "…", "errors": [...] }. Mirror the runtime errors the live preview is throwing. The next build on that ticket is handed them as part of the app’s current state and is told to fix them before new work; they are cleared once it reads them. An empty errors array clears the buffer.
Endpoint Scope What it does
GET /projects/:projectId/deploy projects:read Current deployment state.
GET /projects/:projectId/deploy/readiness projects:read Re-probes the deployed app’s stateful capabilities and returns the deployment state with readiness.warming (capabilities still settling behind the cross-script DO propagation window).
POST /projects/:projectId/deploy/retry deploy:write Deploy the last build to the preview tier again, reusing its cached files — no rebuild, so it spends no inference. Use it when provision.state is error: that state is a record of the failed attempt and only a successful deploy clears it. Answers 400 when nothing is built yet.
GET /projects/:projectId/deployments projects:read An object whose managed array holds the project’s resources — not a bare array.
GET /projects/:projectId/analytics projects:read Live traffic for the deployed app — request and error counts over the last 24h, read from Cloudflare analytics on your own account. Returns { state: "unavailable" } when not yet deployed or the connection lacks analytics access.
GET /projects/:projectId/errors projects:read What the live app has thrown in the last 24h, grouped: message, stack, the route it happened on, how often, and when it last happened. Empty until something throws. Carries a state: ok means we read the app’s errors and this is all of them, unavailable means we could not look — never shipped, no Cloudflare connection, or the read failed — and says nothing about whether the app is healthy.
GET /traffic projects:read The same 24h request and error counts for every live project in the org, batched into as few Cloudflare queries as possible. Projects with no traffic are omitted, so read covered — the number of live projects the counts actually span, most recently deployed first — rather than inferring coverage from the array. Returns an empty projects array when nothing is live or the connection lacks analytics access.
POST /projects/:projectId/deploy deploy:write Ship the project’s built tree into your Cloudflare account and go live. Takes no ticket — Ship is project-level, and ships the tree as it stands. Answers 400 when nothing is built yet — build first; 402 payment_required at your plan’s live-project limit; 412 model_required with no model connected; 403 forbidden while the organization is suspended.
POST /projects/:projectId/resources/:resourceId/rollback deploy:write Roll one resource back to its previous version.
DELETE /projects/:projectId/resources/:resourceId deploy:write Destroy one managed resource.
POST /projects/:projectId/reconcile deploy:write Compare the registry against your Cloudflare account and repair the drift.

Turning a capability on creates the thing it needs — the database, the bucket, the index — in your Cloudflare account, right then, not at the next deploy. The PUT answers as soon as the setting is saved and the resource is created in the background, so read the capability back to see provision.state settle from creating to idle, or to error with the reason.

That is why turning one off does not delete anything: the resource and its contents stay, and the app simply stops being wired to it. To destroy a resource, use DELETE /projects/:projectId/resources/:resourceId, which is deploy:write.

These PUTs are projects:write because they configure the app, and the same capability set is what the build agent turns on for itself as it works. Creating a project, by contrast, really does provision nothing.

Reading through a capability — the rows in a collection, the objects in a bucket — reaches the deployed app, so it answers 404 not_enabled until the capability is both enabled and deployed.

Endpoint Scope What it does
GET /projects/:projectId/capabilities projects:read Every capability and whether it is on.
GET /projects/:projectId/capabilities/:key projects:read One capability: enabled, its provisioning state, the resource it owns in your account, and its settings. :key is one of auth, team, data, files, assets, email, ai, pay, live, call, schedule, queue, flow, connect, hook, render, video, push, events, guard, meter.
PUT /projects/:projectId/capabilities/:key projects:write Turn one on or off, and set its settings and secrets: { "enabled": true, "settings": {...}, "secrets": {...} }. Secrets are write-only — they come back only as has* booleans. Visible dependencies turn on with it: signed-in surfaces turn on auth, call turns on live, and hook turns on data. Shared backing infrastructure, such as D1, is provisioned without enabling another app-facing capability.
GET /projects/:projectId/capabilities/data/collections projects:read Collections in the deployed app’s database.
GET /projects/:projectId/capabilities/data/records/:collection projects:read Rows in one collection. Takes limit.
PUT /projects/:projectId/capabilities/data/records/:collection/:partition/:id projects:write Replace one record’s fields in the live app. The body is the whole record, checked against the collection exactly as an app write is, so a required field left out is a 400. :partition is the partition the record came back with.
DELETE /projects/:projectId/capabilities/data/records/:collection/:partition/:id projects:write Delete one record from the live app. Its search entry goes with it, and any ?data= socket sees the delete.
GET /projects/:projectId/capabilities/auth/users projects:read The app’s signed-up users, newest first, each with its role and a count of its unexpired sessions. Never returns a password hash or a session token. Takes limit.
GET /projects/:projectId/capabilities/team/teams projects:read The app’s teams, newest first, each with its member and pending-invite counts. Takes limit.
POST /projects/:projectId/capabilities/auth/users/:userId/role projects:write Make one of the app’s users staff, or take it back: { "role": "staff" } or { "role": "member" }. Staff read every user’s records and can edit or delete anyone’s. Only you can appoint them — the app cannot promote anybody from its own code.
DELETE /projects/:projectId/capabilities/auth/users/:userId projects:write Erase one of the app’s users — the account, its sessions, its private records and their search entries, its files, its push devices, its team memberships and invitations, its meter counts, its entitlements and any API credentials it connected. Paid orders are kept but stripped of the identifiers that point back to a person, because tax law requires the transaction record. Returns what was removed, so you have evidence you acted. There is no undo.
GET /projects/:projectId/capabilities/files/objects projects:read Objects in the app’s bucket. Takes limit and cursor; returns the next cursor while more remain.
GET /projects/:projectId/capabilities/files/object projects:read One object’s bytes, streamed from the app’s bucket. Takes key.
GET /projects/:projectId/capabilities/schedule/jobs projects:read The app-wide jobs declared in schedule.json, soonest first, each with its next run, last run, repeat, attempts and last error. Per-user timers are not listed.
GET /projects/:projectId/capabilities/pay/orders projects:read The app’s Stripe orders, newest first, with a rollup of order count, paid count, and revenue by currency. Takes limit.
GET /projects/:projectId/capabilities/pay/entitlements projects:read The access customers currently hold — one row per user and product still in force. Takes limit.
GET /projects/:projectId/capabilities/data/query projects:read Run a live ?q= search over one collection, across every partition, for the operator. Takes collection and q.
POST /projects/:projectId/capabilities/ai/run projects:write Run one prompt through the deployed app’s write job, for trying it from the console. Body { "prompt": "…" }; returns { "text": "…" }. Spends your account’s inference and counts against the monthly budget.
GET /projects/:projectId/capabilities/ai/spend projects:read What the deployed app has spent on AI this month. Returns { "month": "2026-07", "used": 42 }, counted in runs.
GET /projects/:projectId/capabilities/render/spend projects:read How many documents the deployed app has rendered this month. Returns { "month": "2026-07", "used": 12, "monthly": 500 }.
GET /projects/:projectId/capabilities/video/spend projects:read How many videos the deployed app has taken this month. Returns { "month": "2026-07", "used": 4, "monthly": 100 }.
GET /projects/:projectId/capabilities/meter/usage projects:read Each allowance the deployed app declares, with how much of the current window has been spent and by how many people. Returns { "limits": [ … ] }.
GET /projects/:projectId/capabilities/events/summary projects:read What the deployed app has recorded over the last 30 days, one row per event name and group. Carries a state: ok returns { "state": "ok", "windowDays": 30, "items": [ … ] }, and unavailable means we could not read them — never shipped, no Cloudflare connection, or the read failed — which says nothing about what the app recorded.
GET /projects/:projectId/assets projects:read The files uploaded to this project.
GET /projects/:projectId/assets/:path{.+} projects:read Fetch one asset’s bytes. Served sandboxed, for previewing a file before the project is deployed.
POST /projects/:projectId/assets/upload-init projects:write Begin an upload. Returns a short-lived, single-key token and the URL of the upload worker in your own account; the browser sends the bytes straight there.
POST /projects/:projectId/assets/finalize projects:write Record a finished upload — writes the metadata row (name, size, type) once the bytes have landed in R2.
POST /projects/:projectId/attachments/upload-init projects:write Begin a prompt-attachment upload — a screenshot, mockup, or PDF the model will read, not an app asset. Body {"sha256":"…","contentType":"image/png","size":123} (types: png, jpeg, gif, webp, pdf); returns the same single-key token and upload-worker URL as asset uploads. The key is content-addressed under _prompt/ and the deployed app never serves it.
POST /projects/:projectId/attachments/finalize projects:write Confirm an attachment landed. Returns {"key","mediaType"} — pass the key in the attachments array of a ticket capture, build, steer, or plan message. Refused over 8 MiB.
GET /projects/:projectId/attachments/:file projects:read Fetch one attachment’s bytes. :file is the content-addressed name from the finalize key, without its _prompt/ prefix. Served sandboxed and immutable; the asset routes refuse the _prompt/ namespace entirely.
PATCH /projects/:projectId/assets/:path{.+} projects:write Set who can open the asset: {"access":{"kind":"public"}}, {"kind":"members"}, or {"kind":"product","productId":"prod_…"}. Re-stamps the file in place; the path and bytes don’t move.
DELETE /projects/:projectId/assets/:path{.+} projects:write Remove one asset.
POST /projects/:projectId/pay/key/check projects:write Re-test the stored Stripe key against every permission Payments needs, and record what it can’t do. Runs automatically when the key is saved; call this after widening a restricted key in Stripe.
GET /projects/:projectId/pay/products projects:read Your Stripe catalog — every product this app can sell, one-time or subscription, with the product id a gated asset or collection checks.
POST /projects/:projectId/pay/products projects:write Create a product and its price in your Stripe account. amount is in the currency’s smallest unit; pass an interval to make it a subscription.
POST /projects/:projectId/pay/products/:productId/price projects:write Change a product’s price. Stripe prices are immutable, so this mints a new one and points the product at it — existing subscribers keep the price they signed up at.
POST /projects/:projectId/pay/products/:productId/archive projects:write Stop selling a product. Access already bought is untouched, and existing subscriptions keep billing until the buyer cancels.
GET /projects/:projectId/pay/codes projects:read Live discount codes — the string a buyer types at checkout, what it takes off, and how many times it has been redeemed.
POST /projects/:projectId/pay/codes projects:write Create a discount code. Give percentOff or amountOff (with a currency), an optional maxRedemptions, and duration once or forever for subscriptions.
POST /projects/:projectId/pay/codes/:codeId/archive projects:write Turn a code off. Nobody can redeem it again; subscriptions that already redeemed it keep the discount.
Endpoint Scope What it does
GET /projects/:projectId/email/status projects:read Your zones, the sending domain, and any DNS records still pending.
POST /projects/:projectId/email/onboard deploy:write Body { "zoneId", "localPart" }. Starts sending-domain onboarding on a domain in your Cloudflare account.
POST /projects/:projectId/email/verify deploy:write Check the domain’s DNS. Returns { "verified": false, "dns": [...] } while records are missing, and pins the from-address once they land.
PUT /projects/:projectId/email/mailboxes deploy:write Body { "mailboxes": [{ "localPart": "support", "collection": "support_mail", "notify": false }] }. Replaces the project’s mailboxes wholesale. A non-empty list sets up routing on the sending subdomain and reports routing (ready or misconfigured); an empty list removes every routing rule. Each mailbox’s mail lands in its Data collection.
GET /projects/:projectId/domain/status projects:read Your custom domain’s state: no_domain, pending_nameservers (with the nameservers to set), pending_ship (zone is active, waiting on your first production deploy), pending_attach, provisioning, active, or one of no_connection, needs_reconnect, unreachable when Cloudflare can’t be read.
POST /projects/:projectId/domain/onboard deploy:write Body { "hostname" }. Finds the domain’s existing zone in your Cloudflare account and returns its nameservers. The domain must already be on Cloudflare — adding it repoints your nameservers, so Typillar won’t do that for you. Starter plan and up.
POST /projects/:projectId/domain/attach deploy:write Once the zone is active and the app is shipped, binds the domain to your production Worker and provisions its certificate. Shipping to production does this for you, so you only need this to attach a domain added after the app went live.
DELETE /projects/:projectId/domain deploy:write Detach the custom domain from your app.

Only the two POSTs mint a whsec_ signing secret, so only those are session-only. A listed endpoint never carries its secret.

Endpoint Scope What it does
GET /webhooks projects:read List the org’s webhook endpoints. Never includes the secret.
POST /webhooks session Create an endpoint. Returns the whsec_ secret once.
DELETE /webhooks/:id projects:write Delete an endpoint.
GET /ingest projects:read List the org’s ingest sources. Never includes the secret.
POST /ingest session Create a source. Returns the whsec_ secret and its webhookUrl once.
DELETE /ingest/:id projects:write Delete a source.

See Webhooks for the signing scheme in both directions.

Every change Typillar makes in your Cloudflare or GitHub account is recorded — Workers deployed, buckets and databases created, domains attached, repos pushed — together with what it targeted, whether it succeeded, and when. Reads are not recorded; this is a log of what changed, not of what we looked at.

The log is scoped to the organization and outlives the projects in it: deleting a project does not erase the record of what was done on its behalf.

Endpoint Scope What it does
GET /activity projects:read The org’s account-activity log, newest first. Cursor-paginated. Optional projectId narrows it to one project, and from/to narrow it to a date range.
GET /activity/export projects:read The same log as a CSV file rather than the JSON envelope — the one endpoint that answers text/csv, with a content-disposition filename. It streams every matching row rather than a page, so it takes no limit or cursor; narrow it with projectId, from and to. Columns: when (ISO 8601), provider, actor, action, target, status, project, detail. This is the endpoint to point a scheduled job at. Capped at about 3 exports a minute per organization, a budget it shares with GET /account/export — hourly is nowhere near it, a loop is.

No API key reaches these, whatever its scopes. Use the console, or a session-cookie request.

Endpoint Scope What it does
GET /account/export session Everything the control plane holds for the organization, as one newline-delimited JSON file — the second endpoint that does not answer the envelope. Every line is an object with a kind: export (a header carrying the format and who asked), organization, member, invitation, connection, api_key, webhook, ingest_source, project, ticket, help_thread, help_message, activity, and finally end, which carries a count per kind. It streams rather than buffering, and it walks every page of every collection, so it takes no limit or cursor. Capped at about 3 exports a minute per organization, a budget it shares with GET /activity/export. Check for the end line. The status is sent before the first row, so a read that fails part way through cannot answer an error — the file simply stops, and the missing trailer is how you tell that apart from a small organization. No secret is in it: never a key’s hash, a webhook or ingest signing secret, or a connection’s OAuth tokens. Owners and admins only — a member gets 403. It exports the organization you are signed in to, and is recorded in the activity log as account.exported. Your generated code is not in it; that comes out through the GitHub repo you connect, or GET /projects/:projectId/source.
GET /account/deletion session What deleting the account would take down: every organization with its disposition (delete, leave, blocked), how many projects are left in projectsLeft, and whether Cloudflare is still connected. projects carries the resource detail for the next batch only, never the whole account. Reads only.
POST /account/deletion/step session Run one bounded slice of the deletion and return the plan as it now stands. Body { "confirm": "<your email>" }. Call it until deletion comes back non-null; spent is how many Cloudflare operations the slice used and cleared how many projects it finished. A resource Cloudflare refuses to remove halts the run and comes back in failed; send leaveStuck: true to finish anyway, which strands exactly those resources in your own Cloudflare account and takes the project down around them. A project’s row is removed once its resources are down, so projectsLeft is the work still ahead and an interrupted run resumes where it stopped. Nothing is erased until every project is clear. Refused while you own an organization that has other members in it.
GET /keys session List the org’s API keys, redacted.
POST /keys session Mint a key. Returns the plaintext once.
DELETE /keys/:id session Revoke a key.
GET /device/grant session What a waiting device sign-in is asking for, so the browser can show it before granting: the code, the name the device gave itself, the scopes it wants, and when the request expires. Query ?code=XXXX-XXXX. Reads only — approving is the next call. A code that was already used answers 409.
POST /device/approve session Grant a waiting device sign-in and mint its key. Body { "code": "XXXX-XXXX" }. The key is minted here, under your session, and handed to the device on its next POST /device/token poll — a key never mints another key. Named after whatever the device called itself, scoped to what GET /device/grant showed, expires in 90 days, counts against the 50-key limit, and is recorded in the activity log as api_key.created.
GET /billing session The org’s plan and its live-project slots.
POST /billing/checkout session Start a Stripe Checkout session. Body { "plan": "starter" | "pro" }.
POST /billing/portal session Open the Stripe billing portal.
GET /connections session Whether Cloudflare and GitHub are connected, and your role.
GET /connections/cloudflare/r2 session R2 status in your Cloudflare account.
GET /connections/cloudflare/stream session Stream status in your Cloudflare account.
GET /connections/cloudflare/accounts session The Cloudflare accounts this connection can reach, and which one is chosen.
PUT /connections/cloudflare/account session Choose the Cloudflare account this org deploys into. Refused if projects are already deployed in another one.
GET /connections/:provider/start session Begin the OAuth flow. Owners and admins only.
GET /connections/:provider/callback session The OAuth redirect target. Owners and admins only.
DELETE /connections/:provider session Disconnect a provider. Owners and admins only.
GET /security session The org’s sign-in policy — requireSso, requireTwoFactor, sessionLifetimeDays and allowedIps — and ssoReady, which says whether a domain-verified provider exists to require.
PUT /security session Set the policy. Body { "requireSso", "requireTwoFactor", "sessionLifetimeDays", "allowedIps" }, all four required. allowedIps is a list of addresses or CIDR ranges, v4 or v6, at most 32; empty means anywhere. It is refused unless it contains the address you are calling from, so you cannot lock yourself out in one request, and it governs API keys as well as browser sessions — a key presented from an address off the list answers 403 ip_not_allowed. Owners and admins only. requireSso is refused unless the plan carries single sign-on and the domain is already verified, so a misconfigured provider cannot lock the organization out. sessionLifetimeDays is 1, 7, 30, or null for no limit — any other number is a 400. Tightening any of the three signs every member out, including you — the rule binds on the next sign-in, not whenever old sessions happen to expire. Sessions are cached in a cookie for 60 seconds, so a signed-in member can keep reading for up to a minute after the change. A session past its window is ended, not merely refused, so the next read reports it signed out.
GET /sso session The org’s single sign-on provider, or null. Names the issuer, the email domains it claims, whether it is oidc or saml, and whether the domain is verified. Never returns the client secret or the signing certificate.
POST /sso session Configure the provider. Body { "issuer", "domain", "oidcConfig" } or { "issuer", "domain", "samlConfig" } — exactly one of the two. domain is comma-separated for more than one. Returns the provider and the TXT value to publish at the domain. Owners and admins only, Enterprise plan only. Replaces any provider already configured.
POST /sso/verify-domain session Re-read the domain’s TXT record and return { "domainVerified" }. Owners and admins only. Until it comes back true the provider signs nobody in.
DELETE /sso session Remove the provider. Owners and admins only. People go back to a password or a social account.
GET /help session The org’s help conversations, most recently active first.
POST /help session Open a conversation. Body { "subject", "body", "projectId"?, "attachments"?, "console"? }. Your plan and the named project are attached automatically. attachments is up to three ids from POST /help/attachments. console is what the console saw when you sent it — route, ticket, build, viewport, time zone, locale, and the last five API failures; the server never trusts it for plan or project.
POST /help/attachments session Upload a screenshot. The body is the raw bytes, Content-Type is image/png, image/jpeg, image/webp or image/gif, and ?name= is the filename. Capped at 5 MB. Returns { "id", "name", "contentType", "size" } — pass the id in attachments on the next message, or it is never shown to anyone.
GET /help/attachments/:attachmentId session The attachment’s bytes, served nosniff and sandboxed. Scoped to your organization: an id from another org is a 404. This is also how you read a screenshot we sent you — an operator’s reply attaches under the same organization, so the same route serves both directions.
GET /help/:threadId session One conversation with every message in it.
POST /help/:threadId/messages session Reply. Body { "body", "attachments"? }. Replying reopens an answered or closed conversation.

Organizations and members are not part of /api/v1 — they live on the auth routes, and are session-only too. So is accepting an invitation; the one invitation route /api/v1 does serve is the public read described at the top of this page.