curl --request POST \
--url https://app.goosybear.ai/api/pages/ingest \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: multipart/form-data' \
--form file=@example-file \
--form siteId=3c90c3cc-0d44-4b50-8888-8dd25736052a \
--form 'name=<string>'import requests
url = "https://app.goosybear.ai/api/pages/ingest"
files = { "file": ("example-file", open("example-file", "rb")) }
payload = {
"siteId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"name": "<string>"
}
headers = {"Authorization": "Bearer <token>"}
response = requests.post(url, data=payload, files=files, headers=headers)
print(response.text)const form = new FormData();
form.append('file', '<string>');
form.append('siteId', '3c90c3cc-0d44-4b50-8888-8dd25736052a');
form.append('name', '<string>');
const options = {method: 'POST', headers: {Authorization: 'Bearer <token>'}};
options.body = form;
fetch('https://app.goosybear.ai/api/pages/ingest', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://app.goosybear.ai/api/pages/ingest",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => "-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"file\"; filename=\"example-file\"\r\nContent-Type: application/octet-stream\r\n\r\n<string>\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"siteId\"\r\n\r\n3c90c3cc-0d44-4b50-8888-8dd25736052a\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"name\"\r\n\r\n<string>\r\n-----011000010111000001101001--",
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: multipart/form-data"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://app.goosybear.ai/api/pages/ingest"
payload := strings.NewReader("-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"file\"; filename=\"example-file\"\r\nContent-Type: application/octet-stream\r\n\r\n<string>\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"siteId\"\r\n\r\n3c90c3cc-0d44-4b50-8888-8dd25736052a\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"name\"\r\n\r\n<string>\r\n-----011000010111000001101001--")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://app.goosybear.ai/api/pages/ingest")
.header("Authorization", "Bearer <token>")
.body("-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"file\"; filename=\"example-file\"\r\nContent-Type: application/octet-stream\r\n\r\n<string>\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"siteId\"\r\n\r\n3c90c3cc-0d44-4b50-8888-8dd25736052a\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"name\"\r\n\r\n<string>\r\n-----011000010111000001101001--")
.asString();require 'uri'
require 'net/http'
url = URI("https://app.goosybear.ai/api/pages/ingest")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request.body = "-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"file\"; filename=\"example-file\"\r\nContent-Type: application/octet-stream\r\n\r\n<string>\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"siteId\"\r\n\r\n3c90c3cc-0d44-4b50-8888-8dd25736052a\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"name\"\r\n\r\n<string>\r\n-----011000010111000001101001--"
response = http.request(request)
puts response.read_body{
"ok": true,
"data": {
"outcome": "ingested",
"siteId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"siteName": "<string>",
"kind": "bundle",
"objectCount": 123,
"pageCount": 123,
"bytes": 123,
"findings": [
{
"rule": "<string>",
"severity": "<string>",
"message": "<string>",
"path": "<string>"
}
],
"report": {
"status": "<string>",
"kind": "<string>"
},
"deploySource": "declared",
"deploy": {
"entry": "<string>",
"assets": "<string>",
"routes": [
{
"pattern": "<string>",
"access": "public"
}
],
"bindings": {
"database": {
"migrations": "<string>",
"tables": [
"<string>"
],
"stagedMigrationsPrefix": "<string>"
},
"projectDatabase": "none",
"secrets": [
"<string>"
],
"submissions": "<string>",
"storage": {
"name": "<string>"
},
"images": true,
"kv": {
"name": "<string>"
}
},
"limits": {
"cpuMs": 123,
"subRequests": 123
},
"runtime": {
"compatibilityFlags": [
"<string>"
],
"compatibilityDate": "<string>"
},
"forms": [
{
"name": "<string>",
"event": "<string>"
}
]
},
"revision": "<string>",
"buildRunId": "<string>"
}
}{
"ok": true,
"data": {
"outcome": "pending_confirmation",
"siteId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"siteName": "<string>",
"kind": "bundle",
"handle": "<string>",
"base": {
"kind": "managed",
"ref": "<string>"
},
"diff": {
"identical": false,
"added": [
{
"path": "<string>",
"bytes": 123,
"sha256": "<string>"
}
],
"removed": [
{
"path": "<string>",
"bytes": 123,
"sha256": "<string>"
}
],
"changed": [
{
"path": "<string>",
"kind": "text",
"bytesBefore": 123,
"bytesAfter": 123,
"sha256Before": "<string>",
"sha256After": "<string>",
"hunks": [
"<string>"
],
"truncated": true
}
],
"unchangedCount": 123,
"summary": {
"added": 123,
"removed": 123,
"changed": 123,
"unchanged": 123,
"bytesBefore": 123,
"bytesAfter": 123
}
}
}
}{
"ok": false,
"error": {
"code": "<string>",
"message": "<string>"
}
}{
"ok": false,
"error": {
"code": "<string>",
"message": "<string>"
}
}{
"ok": false,
"error": {
"code": "<string>",
"message": "<string>"
}
}{
"ok": false,
"error": {
"code": "<string>",
"message": "<string>"
}
}{
"ok": false,
"error": {
"code": "<string>",
"message": "<string>"
}
}{
"ok": false,
"error": {
"code": "<string>",
"message": "<string>"
}
}Import or replace a site from an archive
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.
curl --request POST \
--url https://app.goosybear.ai/api/pages/ingest \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: multipart/form-data' \
--form file=@example-file \
--form siteId=3c90c3cc-0d44-4b50-8888-8dd25736052a \
--form 'name=<string>'import requests
url = "https://app.goosybear.ai/api/pages/ingest"
files = { "file": ("example-file", open("example-file", "rb")) }
payload = {
"siteId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"name": "<string>"
}
headers = {"Authorization": "Bearer <token>"}
response = requests.post(url, data=payload, files=files, headers=headers)
print(response.text)const form = new FormData();
form.append('file', '<string>');
form.append('siteId', '3c90c3cc-0d44-4b50-8888-8dd25736052a');
form.append('name', '<string>');
const options = {method: 'POST', headers: {Authorization: 'Bearer <token>'}};
options.body = form;
fetch('https://app.goosybear.ai/api/pages/ingest', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://app.goosybear.ai/api/pages/ingest",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => "-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"file\"; filename=\"example-file\"\r\nContent-Type: application/octet-stream\r\n\r\n<string>\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"siteId\"\r\n\r\n3c90c3cc-0d44-4b50-8888-8dd25736052a\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"name\"\r\n\r\n<string>\r\n-----011000010111000001101001--",
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: multipart/form-data"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://app.goosybear.ai/api/pages/ingest"
payload := strings.NewReader("-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"file\"; filename=\"example-file\"\r\nContent-Type: application/octet-stream\r\n\r\n<string>\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"siteId\"\r\n\r\n3c90c3cc-0d44-4b50-8888-8dd25736052a\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"name\"\r\n\r\n<string>\r\n-----011000010111000001101001--")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://app.goosybear.ai/api/pages/ingest")
.header("Authorization", "Bearer <token>")
.body("-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"file\"; filename=\"example-file\"\r\nContent-Type: application/octet-stream\r\n\r\n<string>\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"siteId\"\r\n\r\n3c90c3cc-0d44-4b50-8888-8dd25736052a\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"name\"\r\n\r\n<string>\r\n-----011000010111000001101001--")
.asString();require 'uri'
require 'net/http'
url = URI("https://app.goosybear.ai/api/pages/ingest")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request.body = "-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"file\"; filename=\"example-file\"\r\nContent-Type: application/octet-stream\r\n\r\n<string>\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"siteId\"\r\n\r\n3c90c3cc-0d44-4b50-8888-8dd25736052a\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"name\"\r\n\r\n<string>\r\n-----011000010111000001101001--"
response = http.request(request)
puts response.read_body{
"ok": true,
"data": {
"outcome": "ingested",
"siteId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"siteName": "<string>",
"kind": "bundle",
"objectCount": 123,
"pageCount": 123,
"bytes": 123,
"findings": [
{
"rule": "<string>",
"severity": "<string>",
"message": "<string>",
"path": "<string>"
}
],
"report": {
"status": "<string>",
"kind": "<string>"
},
"deploySource": "declared",
"deploy": {
"entry": "<string>",
"assets": "<string>",
"routes": [
{
"pattern": "<string>",
"access": "public"
}
],
"bindings": {
"database": {
"migrations": "<string>",
"tables": [
"<string>"
],
"stagedMigrationsPrefix": "<string>"
},
"projectDatabase": "none",
"secrets": [
"<string>"
],
"submissions": "<string>",
"storage": {
"name": "<string>"
},
"images": true,
"kv": {
"name": "<string>"
}
},
"limits": {
"cpuMs": 123,
"subRequests": 123
},
"runtime": {
"compatibilityFlags": [
"<string>"
],
"compatibilityDate": "<string>"
},
"forms": [
{
"name": "<string>",
"event": "<string>"
}
]
},
"revision": "<string>",
"buildRunId": "<string>"
}
}{
"ok": true,
"data": {
"outcome": "pending_confirmation",
"siteId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"siteName": "<string>",
"kind": "bundle",
"handle": "<string>",
"base": {
"kind": "managed",
"ref": "<string>"
},
"diff": {
"identical": false,
"added": [
{
"path": "<string>",
"bytes": 123,
"sha256": "<string>"
}
],
"removed": [
{
"path": "<string>",
"bytes": 123,
"sha256": "<string>"
}
],
"changed": [
{
"path": "<string>",
"kind": "text",
"bytesBefore": 123,
"bytesAfter": 123,
"sha256Before": "<string>",
"sha256After": "<string>",
"hunks": [
"<string>"
],
"truncated": true
}
],
"unchangedCount": 123,
"summary": {
"added": 123,
"removed": 123,
"changed": 123,
"unchanged": 123,
"bytesBefore": 123,
"bytesAfter": 123
}
}
}
}{
"ok": false,
"error": {
"code": "<string>",
"message": "<string>"
}
}{
"ok": false,
"error": {
"code": "<string>",
"message": "<string>"
}
}{
"ok": false,
"error": {
"code": "<string>",
"message": "<string>"
}
}{
"ok": false,
"error": {
"code": "<string>",
"message": "<string>"
}
}{
"ok": false,
"error": {
"code": "<string>",
"message": "<string>"
}
}{
"ok": false,
"error": {
"code": "<string>",
"message": "<string>"
}
}Authorizations
An API key minted at Settings › API & MCP. Send it as Authorization: Bearer <key>. A key carries its holder's own permissions, resolved on every call — revoking a membership closes the key's reach immediately. Keep it in an environment variable (GOOSY_API_KEY), never in a committed file.
Query Parameters
Which workspace to act in — its slug or id. The SAME argument every workspace-scoped tool takes, resolved the same way: an account-wide key may select any workspace its holder currently reaches, a workspace-pinned key may only name its own, and a call that names nothing falls through to your default. It is a QUERY parameter rather than a body field on purpose: this endpoint's body is a multipart upload, and reading a field out of it would decode that body before the bearer and the rate limiter had run. With more than one reachable workspace and no default, the call is refused as workspace_ambiguous and the choices are listed. Ignored for a signed-in session, whose workspace comes from its own claims.
1The handle a previous 202 answered with. Present ⇒ apply the diff that handle was issued for: the archive must be byte-identical to the one the diff was shown for, and the site's current project must still be the base it was shown against, else 400 (confirmation_stale). Ignored on a first upload.
1