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 /healthlives at the root, outside/api/v1, and needs no auth.GET /api/v1/invitations/:invitationIdneeds 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 stillpending— 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.
Reading the scope column
Section titled “Reading the scope column”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.
Request bodies
Section titled “Request bodies”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
Section titled “Query parameters”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.
Projects
Section titled “Projects”| 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. |
Tickets
Section titled “Tickets”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. |
The plan thread
Section titled “The plan thread”| 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. |
Building
Section titled “Building”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. |
Shipping
Section titled “Shipping”| 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. |
Capabilities
Section titled “Capabilities”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. |
Webhooks and ingest
Section titled “Webhooks and ingest”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.
Account activity
Section titled “Account activity”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. |
Session-only surfaces
Section titled “Session-only surfaces”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.