> ## Documentation Index
> Fetch the complete documentation index at: https://docs.goosybear.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Pages API

> Import or replace a site, upload its reference assets, inspect builds, share a preview, and publish one confirmed revision.

The Pages API can take a site from an archive to a reviewable preview and then
publish one exact revision. Tool actions use
`POST https://app.goosybear.ai/api/v1/tools/{tool}`. Archive, asset, and
preview-link operations use the Pages-specific REST endpoints shown below.

Use a key narrowed to the workspace that owns the site for this whole flow.
Ingest, asset uploads, and preview-link management refuse account-wide keys
instead of choosing a workspace. The key holder also needs **Use the API and
MCP** and the relevant live Pages permission.

```bash theme={null}
export GOOSY_API_KEY="your-api-key"
export GOOSY_SITE_ID="your-site-id"
```

<Note>
  These examples describe request contracts. Deployment and account exposure
  are separate: an environment may still refuse a tool while its rollout or
  exposure switch is closed. Hosted sites use a separate policy, grant, and
  credential flow for AI and retrieval operations; see [Managed capabilities
  for Pages](/api/pages-capabilities).
</Note>

## Read a site

Call `page.read` before replacing, previewing, or publishing a site. It returns
the newest built revision separately from the currently published revision.
It also returns the site's canonical `board_id`; use that identifier for board
and Data operations instead of matching a board by its display title.

```bash theme={null}
curl --request POST \
  --url "https://app.goosybear.ai/api/v1/tools/page.read" \
  --header "Authorization: Bearer $GOOSY_API_KEY" \
  --header "Content-Type: application/json" \
  --data "{\"site_id\":\"$GOOSY_SITE_ID\"}"
```

```json theme={null}
{
  "ok": true,
  "site_id": "<site-id>",
  "board_id": "<board-id-or-null>",
  "name": "Imported site",
  "status": "draft",
  "hostname": null,
  "published_revision": null,
  "published_at": null,
  "updated_at": "<ISO-8601 timestamp>",
  "latest_revision": "<revision-or-null>",
  "has_build": true,
  "source_mode": "managed",
  "pixels": [],
  "other_live_count": 0,
  "workspace": "<workspace-handle>",
  "working_in": {
    "label": "working in: <workspace-name>",
    "note": "this key is pinned here"
  }
}
```

## Import or replace a site archive

`POST https://app.goosybear.ai/api/pages/ingest` accepts
`multipart/form-data`. Send exactly one ZIP archive as `file`. `name` and
`siteId` are optional text fields:

* Omit `siteId` to create a site. `name` supplies its display name.
* Send `siteId` to replace the managed source or staged bundle on that same
  workspace-scoped site. `name` is ignored in this case.

```bash theme={null}
curl --request POST \
  --url "https://app.goosybear.ai/api/pages/ingest" \
  --header "Authorization: Bearer $GOOSY_API_KEY" \
  --form "file=@./site.zip" \
  --form "siteId=$GOOSY_SITE_ID"
```

The service classifies the archive from its contents. A prebuilt static bundle
is staged immediately and returns `kind: "bundle"` plus `revision`. A supported
source project is stored and queued for the Pages build rail; it returns
`kind: "source"` plus `buildRunId`:

```json theme={null}
{
  "ok": true,
  "data": {
    "siteId": "<site-id>",
    "siteName": "Imported site",
    "kind": "source",
    "buildRunId": "<build-run-id>",
    "objectCount": 42,
    "pageCount": 0,
    "bytes": 184320,
    "findings": [],
    "report": {
      "status": "compiling",
      "kind": "source:astro",
      "markers": ["package.json", "astro.config.mjs"],
      "carried": {"pages": 0, "assets": 0, "routes": []},
      "approximated": [
        "We are building this Astro project with its own toolchain — 42 files — and serving what it writes.",
        "Response headers (content types and caching) will be ours, not the ones your original host sent."
      ],
      "notCarried": []
    }
  }
}
```

The full `report` shape describes what the importer detected and accepted.
Always read it. Server routes, unsupported dependencies, unsafe paths, or an
invalid bundle can produce a typed refusal rather than a partial deployment.

## Follow a source build

Use the `buildRunId` from a source ingest as `build_run_id`. The site and run
must belong together inside the key's resolved workspace.

```bash theme={null}
curl --request POST \
  --url "https://app.goosybear.ai/api/v1/tools/page.build_status" \
  --header "Authorization: Bearer $GOOSY_API_KEY" \
  --header "Content-Type: application/json" \
  --data "{\"site_id\":\"$GOOSY_SITE_ID\",\"build_run_id\":\"<build-run-id>\"}"
```

```json theme={null}
{
  "ok": true,
  "site_id": "<site-id>",
  "build_run_id": "<build-run-id>",
  "status": "running",
  "revision": null,
  "workspace": "<workspace-handle>",
  "working_in": {
    "label": "working in: <workspace-name>",
    "note": "this key is pinned here"
  }
}
```

`status` is `pending`, `running`, `retrying`, `succeeded`, or `failed`.
Immediately after enqueue, `pending` can mean the durable build record has not
appeared yet. Poll with normal backoff until the call returns a terminal state.
A successful build returns its `revision`.

## Upload a reference image

`POST https://app.goosybear.ai/api/pages/{siteId}/assets` stores one image and
attaches it to the site's own board. The multipart body accepts exactly one
field named `file`; a caller cannot supply a different board id.

```bash theme={null}
curl --request POST \
  --url "https://app.goosybear.ai/api/pages/$GOOSY_SITE_ID/assets" \
  --header "Authorization: Bearer $GOOSY_API_KEY" \
  --form "file=@./cover.webp;type=image/webp"
```

```json theme={null}
{
  "ok": true,
  "data": {
    "boardItemId": "<board-item-id>",
    "storageObjectId": "<storage-object-id>",
    "displayName": "cover.webp"
  }
}
```

Accepted types are PNG, JPEG, WEBP, and GIF. The file must be nonempty and no
larger than 40 MB. SVG is not accepted. This endpoint requires an operator role
in addition to `pages.manage`.

## Create a preview link

`POST https://app.goosybear.ai/api/pages/{siteId}/preview-links` creates a
link for the site's current previewable revision. `ttlMinutes` is optional; the
default is 10,080 minutes (seven days), and accepted values are whole minutes
from 5 through 43,200.

```bash theme={null}
curl --request POST \
  --url "https://app.goosybear.ai/api/pages/$GOOSY_SITE_ID/preview-links" \
  --header "Authorization: Bearer $GOOSY_API_KEY" \
  --header "Content-Type: application/json" \
  --data '{"ttlMinutes":60}'
```

```json theme={null}
{
  "ok": true,
  "data": {
    "siteId": "<site-id>",
    "revision": "<revision>",
    "url": "https://<pages-host>/__preview/<preview-token>/",
    "token": "<preview-token>",
    "expiresAt": "<ISO-8601 timestamp>"
  }
}
```

The token is the preview capability. Store it only where you can protect a
credential, keep it out of logs and analytics, and share its URL only with the
intended recipient. The response is `no-store` and `no-referrer`. Customers do
not need Cloudflare credentials; the service owns the preview route.

Creating a preview does not publish the site or attach a custom domain. A draft
remains a draft.

## Revoke a preview link

`DELETE https://app.goosybear.ai/api/pages/{siteId}/preview-links` takes the
43-character preview token in a strict JSON body. Keep it out of a query
string.

```bash theme={null}
curl --request DELETE \
  --url "https://app.goosybear.ai/api/pages/$GOOSY_SITE_ID/preview-links" \
  --header "Authorization: Bearer $GOOSY_API_KEY" \
  --header "Content-Type: application/json" \
  --data '{"token":"<preview-token>"}'
```

```json theme={null}
{ "ok": true, "data": { "siteId": "<site-id>" } }
```

Revocation is idempotent: a missing or foreign token still returns the site id
after the key's workspace boundary has been applied.

## Publish one exact revision

`page.publish` always takes two calls. The first call reads the site's latest
built revision and returns a short-lived confirmation without publishing:

```bash theme={null}
curl --request POST \
  --url "https://app.goosybear.ai/api/v1/tools/page.publish" \
  --header "Authorization: Bearer $GOOSY_API_KEY" \
  --header "Content-Type: application/json" \
  --data "{\"site_id\":\"$GOOSY_SITE_ID\"}"
```

```json theme={null}
{
  "ok": true,
  "state": "confirmation_required",
  "site_id": "<site-id>",
  "revision": "<revision>",
  "confirmation_id": "<confirmation-id>",
  "expires_in_seconds": 1800,
  "workspace": "<workspace-handle>",
  "working_in": {
    "label": "working in: <workspace-name>",
    "note": "this key is pinned here"
  }
}
```

Present the site and revision to the person approving the launch. After they
approve, repeat the same call with the returned `confirmation_id`:

```bash theme={null}
curl --request POST \
  --url "https://app.goosybear.ai/api/v1/tools/page.publish" \
  --header "Authorization: Bearer $GOOSY_API_KEY" \
  --header "Content-Type: application/json" \
  --data "{\"site_id\":\"$GOOSY_SITE_ID\",\"confirmation_id\":\"<confirmation-id>\"}"
```

```json theme={null}
{
  "ok": true,
  "state": "published",
  "site_id": "<site-id>",
  "revision": "<revision>",
  "hostname": "<published-hostname>",
  "url": "https://<published-hostname>",
  "shared_dev_host": false,
  "workspace": "<workspace-handle>",
  "working_in": {
    "label": "working in: <workspace-name>",
    "note": "this key is pinned here"
  }
}
```

The confirmation is bound to the key holder, tenant, workspace, site, tool, and
revision. It expires after 30 minutes and is single-use after a successful
publish. If a newer build becomes ready first, the call returns
`pages.publish_revision_stale`, consumes the stale confirmation, and requires a
new first call so the newer revision can be reviewed.

## Handle responses

Tool actions return a flattened body such as `{ "ok": true, ... }` and answer
HTTP `200` once the tool ran, including typed tool refusals with `"ok": false`.
The ingest, asset, and preview-link endpoints return
`{ "ok": true, "data": ... }`; their failures use
`{ "ok": false, "error": { "code", "message" } }` and an appropriate HTTP
status.

A missing or invalid bearer key is `401`; access, workspace, role, or Pages
permission denial is `403`; invalid input is `400`; and rate limiting is `429`.
On `429`, wait for `Retry-After` before trying again. Asset and preview-link
responses are `no-store` and `no-referrer`.
