Skip to main content
These examples show complete requests and the identifiers to carry from one call to the next. Tool and feature exposure still follows the environment you are calling; a documented request can return a typed rollout refusal before that surface is enabled there. They assume your key is in an environment variable and that you have picked a workspace, either by setting a default or by passing workspace as shown. See Quickstart if you have not made a key yet.
For an unattended project, Data or Pages setup, a key narrowed to one workspace keeps the workspace argument off every call. An account-wide key works everywhere too — including the Pages import, asset and preview-link endpoints — as long as you pass workspace when your holder reaches more than one.

1 · How many credits are left

The one call that takes no workspace — credits belong to the account, not to a single workspace.
Worth running on a schedule if you have automation that spends — a job that stops for want of credits is easier to prevent than to notice.

2 · What is going out this week

meta_line is the same sentence the schedule board shows you, so it is the right thing to put in a status message. To see where a specific piece has got to, content.status gives you counts by state plus the pieces behind them.

3 · Find something in the library, then open it

Two calls, because searching and fetching are different jobs. Search first:
Then fetch one in full, using both the kind and the id from the search result — an id alone is not enough, because different kinds of thing can share one:
An empty query returns the most recent items, which is a cheap way to see what a workspace has.

4 · Ask your knowledge base a question

Ask it the way you would ask a person — it matches on meaning, not on keywords, so a whole question works better than two words. relevance lets you drop weak hits before you show them to anyone.

5 · Create a project and move a board into it

Read the workspace tree before using any board id. kind and root_board_id are optional; use kind: "site" to narrow the result to Pages boards and the projects that contain them. Creating or moving a project requires an operator role.
Create a named project with a stable idempotency_key. Reuse that key only when retrying this same create. parent_board_id is optional and creates a nested project when present.
Move a board returned by board.tree into that project. Omit from_parent_board_id when the board has one current location; the service resolves it. Send to_parent_board_id: null to move a board back to the root.
If a board appears in several places, board.move refuses with boards_tree.parent_ambiguous. Read the tree again and retry with the exact from_parent_board_id it should leave.

6 · Create and maintain a Data-backed CMS

Create a typed table on the project board. name, columns, and idempotency_key are required. board_id is optional; omitting it creates a board for the table. Column keys use lower snake case, start with a letter, and are at most 64 characters. Supported types are text, number, boolean, date, email, phone, url, and json. All six Data actions require an operator role.
The six Data API actions accept these JSON payloads. Each can also take the adapter-owned optional workspace handle when the key is not pinned. data.describe returns table definitions, counts, row limits, sources, and the current published_columns. data.query returns count_line, total, returned, rows shaped as { "row_id", "values", "source" }, and next_cursor when another page exists. Its limit is 1 through 100. Equality filters take string values and can name only real columns. data.append validates and coerces the whole row against the table schema; unknown keys are refused. Reuse its idempotency_key only for the same append. data.update_cell accepts a JSON string, number, boolean, null, array, or object, then revalidates the complete row. data.set_published_columns replaces the entire public list; an empty array publishes no columns. It decides what a public Page may read, so it answers in two calls: without confirmation_id it proposes, returning state: "confirmation_required", a confirmation_id and a request_summary, and changes nothing; send the same payload again with that confirmation_id, on a member’s own API key, to apply it. A service account’s key can propose but never confirm, and a table whose columns or public list changed in between answers proposal_changed — propose again. After the Page itself is live, its server or Worker can read only those public columns. This read has no bearer credential and never permits writes:
The public read accepts limit from 1 through 1,000, optional cursor, and one equality filter expressed by the pair eqColumn and eqValue. Supplying only half of that pair is refused.

7 · Finish a Page setup without opening the app

Use the same workspace-pinned key for the whole sequence:
  1. Create the project with board.create_folder.
  2. Create and populate the CMS table with data.create_table and data.append, then set its explicit public projection with data.set_published_columns (propose, then confirm on a member’s own key).
  3. Send the site ZIP to POST /api/pages/ingest. Supplying an existing siteId replaces that same site’s bundle or managed source; a source upload returns buildRunId.
  4. Follow a source build with page.build_status, passing that buildRunId as build_run_id, until it returns succeeded and a revision.
  5. Upload PNG, JPEG, WEBP, or GIF references with POST /api/pages/{siteId}/assets. Each upload is attached to that site’s board automatically.
  6. Read board.tree with kind: "site", then use board.move to place the site’s board in the project.
  7. Create a review link with POST /api/pages/{siteId}/preview-links.
  8. Call page.publish once to obtain the exact-revision confirmation. Show the returned site and revision to the approver, then call it again with the confirmation_id after approval.
The Pages API guide gives the exact multipart fields, status responses, preview-token handling, and two-call publication bodies; the deploy contract and site and project databases cover what a bundle declares. The complete run is below. Hosted Pages managed AI tools are still under construction and are not part of this setup flow.

The whole Pages lifecycle, from a clean directory

This is a real run, recorded verbatim on 2026-09-12 against the Dev environment, from an empty directory with only an API key and these pages — no repository, no Cloudflare account, no platform credential. The key used was account-wide (not narrowed to a workspace), which is why workspace appears on every call. The two multipart steps were re-run after workspace moved to the query string, so the commands below are the ones that were executed. Responses are trimmed to the fields the next step needs; identifiers are the real ones from the run.

0 · What was in the directory

Two files, written by hand:

1 · Which workspaces can this key reach?

The fastest way to find out is to name one that does not exist. The refusal lists the real ones.
Both workspace refusals answer HTTP 403, never a 5xx — the request is fine and so is your key; you only have to say where. On the import and asset endpoints, whose envelope has no workspaces array, the slugs are appended to error.message instead.

2 · Import

workspace is a query parameter on this endpoint, not a form field — the body is your archive, and reading a field out of it would mean decoding up to 40 MB before your key was checked.
deploySource: "declared" means our own pages.deploy.json was used. Omitting the file entirely returns "synthesized" and the manifest we wrote.

3 · Attach an image, and list what is attached

4 · Look at it

5 · Publish, in two calls

Show that to whoever is approving, then repeat the call with the confirmation_id:

6 · Where does it answer?

The free platform address is always active with no steps and no records — it is ours end to end. A domain you own is the one with a steps list to watch; connecting one returns the records to add:
(That is what page.domain_connect answers if you try to claim one of ours.) A real connect returns the records to add, routing first — that CNAME is the one that makes the hostname resolve, and the certificate records alone are not enough. Shown here as an illustration, not a recording: this run drove only the refusal paths above, because a success would claim a real hostname.

7 · Ship a second revision, then put the first one back

Re-import onto the same siteId, publish it the same two-call way, and the site now has two builds:
Rolling back takes the same two calls as publishing, and the first one tells you both ends of the swap:
The confirmation is bound to both the site and the revision. Spending it on a different one is refused:
Confirming it puts the earlier build back on the live address, and page.revisions shows the flag has moved:

The same surface over MCP

Every tool above answers identically over the MCP endpoint — same names, same arguments, same envelope. A tools/list on POST /api/mcp/mcp with the same bearer key returned 41 tools, ending:
and tools/call on page.revisions returned the same body the REST facade did. That is not a coincidence to re-check per tool: both transports run one dispatch, and a test walks the whole advertised list over both and deep-compares.

Two refusals worth wiring for

Also useful

connections.status answers which providers a workspace has connected and which need attention, split into connected, needs_attention and available. It is the call to make before an automation tries to publish somewhere, so a failure becomes a warning you saw coming.

Two habits worth keeping

  • Branch on ok, never on the status code. An action that declines still answers 200. Errors and refusals has the full list.
  • Log working_in alongside the result. It is the fastest way to catch an integration quietly working in the wrong workspace.
Every action, with its arguments and its answers, is in the API reference.