---
title: "Import or replace a site from an archive"
method: POST
path: "/api/pages/ingest"
tags: ["pages"]
---

# Import or replace a site from an archive

`POST /api/pages/ingest`

## Request

- Base URL: `https://app.goosybear.ai`
- URL: `https://app.goosybear.ai/api/pages/ingest`
- Auth: HTTP bearer

Uploads one ZIP archive and either creates a site or replaces the code of an existing one. The service classifies the archive from its CONTENTS — a prebuilt bundle is staged immediately, a supported source project is queued for the build rail — and validates any `pages.deploy.json` it carries against the published `PagesDeployManifest` schema, returning the manifest it resolved on every success. The archive must be no larger than 41943040 bytes.

**A push onto a site that already holds a code project shows the diff first.** With `siteId` set and a current project on that site, the call answers `202` with `data.outcome: "pending_confirmation"`, the diff against the current project and a `handle` — nothing is written. Send the SAME archive again with `?confirm=<handle>` to apply it (`200`, `outcome: "ingested"`). Byte-identical bytes answer `200` with `outcome: "unchanged"` and write nothing — and where the site's stored source had no build that came up, that same answer carries a `buildRunId`, because re-pushing an unbuilt project queues its build rather than leaving the site dark. Once such a project has spent the rail's build attempts, the answer stays `200` `unchanged` and carries `buildBlocked` instead: re-pushing an identical archive is never an error, so an idempotent deploy stays green, and the field is where a person is told that only a changed file will build. A first upload proceeds directly. The replaced set is the code project only: the site's database, its storage, its media and its data tables are never touched by a push.

## Query parameters

- `workspace` string
- `confirm` string

## Response `200`

The archive was applied, or was identical to the current project. Read `data.outcome` first: `ingested` (then `data.kind` tells a staged upload from a queued build) or `unchanged` (nothing was written).

- union
  - object
    - `ok` true, required
    - `data` object, required
      - `outcome` 'ingested', required: The door's outcome. `ingested` — the archive was staged or queued. A push onto a site that already holds a code project answers `202` with `pending_confirmation` first (the diff and a handle), and identical bytes answer `unchanged`; see those responses.
      - `siteId` string, uuid, required: The site this upload landed on.
      - `siteName` string, required
      - `kind` 'bundle' | 'prebuilt' | 'source', required: What the archive turned out to be. `bundle` and `prebuilt` are staged immediately and carry `revision`; `source` is queued for the build rail and carries `buildRunId`.
      - `revision` string: Present for a staged upload: the revision now staged.
      - `buildRunId` string: Present for a source upload: pass it to `page.build_status` as `build_run_id`.
      - `objectCount` integer, required
      - `pageCount` integer, required
      - `bytes` integer, required
      - `findings` object[], required: Conformance findings. ALWAYS present — an empty array means 'nothing to fix', not 'we did not check'.
        - `rule` string, required
        - `severity` string, required
        - `path` string, nullable
        - `message` string, required
      - `report` object, required: What the importer detected and accepted. Always read it: server routes, unsupported dependencies and unsafe paths surface here.
        - `status` string, required
        - `kind` string, required
      - `deploySource` 'declared' | 'legacy' | 'synthesized', required: Where the deploy manifest came from — the author's `pages.deploy.json`, a translated legacy `pages.functions.json`, or one we synthesized.
      - `deploy` PagesDeployManifest, required: `pages.deploy.json` — the one file a bundle uses to say what it is and what it needs. Put it at the bundle root.
        - `entry` string: The Worker module that answers `routes`, relative to the bundle root. Omit it for a static site.
        - `assets` string: The directory holding the site's static files, relative to the bundle root.
        - `routes` object[]: Which paths the Worker answers; everything else is served as a static file. A route whose `access` is `members` is gated by the platform before anything answers it, so a static site with no `entry` may declare members routes and nothing else.
          - `pattern` string, required: A site-root-relative path the Worker answers. `*` is allowed only as a trailing `/*`.
          - `access` 'public' | 'members': Who may reach this route. `members` requires a member of this site's workspace, signed in to Goosy; the platform enforces it before your code runs. Defaults to `public`.
        - `bindings` object: What this bundle asks to be wired to.
          - `database` object: The site's own database. Declaring it provisions one on the first build.
            - `migrations` string: Directory of `.sql` migrations for this site's own database, applied in filename order.
            - `tables` string[]: The tables this site's code owns.
            - `stagedMigrationsPrefix` string: PLATFORM-WRITTEN. Where the migrations above were actually staged, once the bundle was relocated into the reserved prefix. Never write this yourself — a manifest that carries it on upload is refused.
          - `projectDatabase` 'none' | 'read' | 'readwrite': How this site reaches the PROJECT database shared by its project's sites. Defaults to `none`.
          - `secrets` string[]: Secret NAMES this Worker reads. Values are set on the site, never in this file.
          - `submissions` string: A table that platform form posts are also written to, so a static site keeps its own entries.
          - `storage` object: The site's own object store. Declaring it provisions one bucket on the first publish and binds it.
            - `name` string: The identifier your Worker reads the bucket through. Defaults to `BUCKET`.
          - `images` boolean: Attach the image-transform binding, so your Worker can resize and re-encode bytes it already holds. Defaults to `false`.
          - `kv` object: The site's own key-value store. Declaring it provisions one namespace on the first publish and binds it.
            - `name` string: The identifier your Worker reads the key-value store through. Defaults to `KV`.
        - `limits` object: The caps this bundle asks for. The platform decides the ceilings and refuses anything over them.
          - `cpuMs` integer: CPU milliseconds per invocation. Defaults to 10000; the platform ceiling is 30000.
          - `subRequests` integer: Outbound sub-requests per invocation. Defaults to 20; the platform ceiling is 50.
        - `runtime` object: What this bundle's Worker needs from the Workers runtime. Read from your bundle's own `wrangler.json` when it ships one; declare it here otherwise.
          - `compatibilityFlags` string[]: Workers compatibility flags your Worker needs to start — global_fetch_strictly_public, nodejs_compat. Anything else is refused by name. Set this only when your bundle ships no JSON Wrangler config for us to read it from.
          - `compatibilityDate` string: The Workers compatibility date your bundle was COMPILED against, as `YYYY-MM-DD`. It is ignored unless it is newer than the platform's own pin — it can only move the date forward, never back.
        - `forms` object[]: The forms whose leads start an automation. A lead from a form named here raises its `event` beside `pages.lead.captured`; a form not named here raises only `pages.lead.captured`.
          - `name` string, required: The form's name, as its lead carries it in the `form` field: lowercase letters, digits, `-` and `_`.
          - `event` string, required: The automation event a lead from this form starts: `custom.` then a lowercase name of letters, digits and underscores, starting with a letter.
  - object
    - `ok` true, required
    - `data` object, required
      - `outcome` 'unchanged', required
      - `siteId` string, uuid, required
      - `siteName` string, required
      - `kind` 'bundle' | 'prebuilt' | 'source', required
      - `base` object, required
        - `kind` 'managed' | 'bundle', required
        - `ref` string, required
      - `buildRunId` string: Present only when the current project had no build that came up, so re-pushing it queued one. Still `unchanged`: not a byte was written — the stored tree already IS this archive — and a build of it is now running under this id. Absent on the ordinary no-op.
      - `buildBlocked` object: Present only when the current project has no successful build AND has spent every build attempt the rail allows, so re-pushing it queued nothing. The call still succeeds and is still `unchanged` — the archive was byte-identical and replaced nothing, so an idempotent re-push is not an error — but no further build will be attempted for these bytes: change a file and push again, which makes a new project the rail will build.
        - `reason` 'attempts_exhausted', required
        - `message` string, required: The sentence to show a person, and their next step.
        - `attempts` integer, required: The rail's ceiling, which this project has now spent.

## Other responses

- `202`: The diff, shown first. The site already holds a code project and the archive differs from it; nothing was written. Read `data.diff`, then send the SAME archive again with `?confirm=<data.handle>` to apply it.
- `400`: The body was not multipart/form-data with a `file` field, the archive failed the size or content guards, its `pages.deploy.json` did not match the contract, or `confirm` named a handle that no longer matches (`confirmation_stale`: the bytes differ or the current project moved — push again without it to see the new diff). The refusal names the field.
- `401`: No bearer credential, or one that did not resolve.
- `403`: Programmatic access is unavailable to this holder, the named workspace is outside the key's scope, or the holder lacks `pages.manage`. `workspace_ambiguous` and `workspace_unreachable` also answer here, and the slugs this key may pass are appended to `error.message`.
- `404`: The `siteId` is not a site in the resolved workspace.
- `429`: The API key is over its rate limit. Wait for `Retry-After` before retrying.
- `500`: An unexpected server error occurred while handling the import.

---

[API](https://skmtc.dev/goosybear/apis/goosy-bear-api.md) · [All operations](https://skmtc.dev/goosybear/apis/goosy-bear-api/llms.txt) · [OpenAPI document](https://skmtc.dev/goosybear/apis/goosy-bear-api/revisions/6d82fb124064?raw)
