Publish a company site's bundle
Makes an uploaded bundle the company site’s current version. It runs inside this one request: Rails asks the sites gateway to verify every blob, checks the manifest, asks the gateway to write the manifest, and then, in one transaction, gives the site its frame label if it has none, marks the bundle published and points the site at it. A site’s visibility never changes. A site with a database schema cannot be published yet and fails with no_database.
A failed step marks the bundle failed and publishes nothing, and the current version keeps serving; start a new bundle to try again. A step that runs out of time (the whole publish has 27 seconds) is a 504 with publish_timeout: the bundle stays uploading, the current version keeps serving, and sending the same publish again is safe and finishes what was started. Sending a publish again after it finished, because its answer was lost, is also safe: a bundle that is published and is the site’s current version answers the same 200, with no gateway call and no change.
Needs application_themes.update and a signed-in admin. A member’s site is refused with 404, as a site an admin may not edit: its owner publishes it. It answers 404 for every caller that passes those checks until the sites gateway is released to the company, and for a bundle that is not the site’s. A droplet installation token and an OAuth client token are refused with 403.
curl --request POST \
--url https://api.fluid.app/api/company/v2026-10/member-sites/{id}/bundles/{bundle_id}/publish \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"confirmMigrations": [
"2026-11-05-drop-notes"
]
}
'import requests
url = "https://api.fluid.app/api/company/v2026-10/member-sites/{id}/bundles/{bundle_id}/publish"
payload = { "confirmMigrations": ["2026-11-05-drop-notes"] }
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({confirmMigrations: ['2026-11-05-drop-notes']})
};
fetch('https://api.fluid.app/api/company/v2026-10/member-sites/{id}/bundles/{bundle_id}/publish', 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://api.fluid.app/api/company/v2026-10/member-sites/{id}/bundles/{bundle_id}/publish",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'confirmMigrations' => [
'2026-11-05-drop-notes'
]
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$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://api.fluid.app/api/company/v2026-10/member-sites/{id}/bundles/{bundle_id}/publish"
payload := strings.NewReader("{\n \"confirmMigrations\": [\n \"2026-11-05-drop-notes\"\n ]\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.fluid.app/api/company/v2026-10/member-sites/{id}/bundles/{bundle_id}/publish")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"confirmMigrations\": [\n \"2026-11-05-drop-notes\"\n ]\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.fluid.app/api/company/v2026-10/member-sites/{id}/bundles/{bundle_id}/publish")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"confirmMigrations\": [\n \"2026-11-05-drop-notes\"\n ]\n}"
response = http.request(request)
puts response.read_body{
"bundle": {
"id": "0199537f-0000-7000-8000-000000000004",
"manifestSha256": "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"
},
"status": 200,
"meta": {
"request_uuid": "3b8f1d27-6c4e-4a91-8e5d-2f7c9a1b0e64",
"timestamp": "2026-11-05T10:00:00Z"
}
}{}{}{
"error": {
"message": "<string>",
"details": {}
},
"status": 123,
"meta": {
"request_uuid": "<string>",
"timestamp": "2023-11-07T05:31:56Z"
}
}{
"error": {
"message": "A newer start replaced this upload",
"details": {
"error": "superseded"
}
},
"status": 409,
"meta": {
"request_uuid": "3b8f1d27-6c4e-4a91-8e5d-2f7c9a1b0e64",
"timestamp": "2026-11-05T10:00:00Z"
}
}{
"error": {
"message": "<string>",
"details": {}
},
"status": 123,
"meta": {
"request_uuid": "<string>",
"timestamp": "2023-11-07T05:31:56Z"
}
}{
"error": {
"message": "<string>",
"details": {}
},
"status": 123,
"meta": {
"request_uuid": "<string>",
"timestamp": "2023-11-07T05:31:56Z"
}
}{
"error": {
"message": "The publish ran out of time; send it again",
"details": {
"error": "publish_timeout"
}
},
"status": 504,
"meta": {
"request_uuid": "3b8f1d27-6c4e-4a91-8e5d-2f7c9a1b0e64",
"timestamp": "2026-11-05T10:00:00Z"
}
}Authorizations
A company admin credential for your company, or a droplet installation token with the website scope for reads, except on the share routes, which refuse a droplet token.
Path Parameters
The site's id.
The bundle's id, as the start of its upload answered it.
Body
What a publish may carry. The body is optional.
The ids of the destructive migration steps the admin confirmed. Nothing applies a schema yet, so they are only checked.
1 - 100Response
The bundle is the site's current version. A publish repeated after it finished answers the same.
curl --request POST \
--url https://api.fluid.app/api/company/v2026-10/member-sites/{id}/bundles/{bundle_id}/publish \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"confirmMigrations": [
"2026-11-05-drop-notes"
]
}
'import requests
url = "https://api.fluid.app/api/company/v2026-10/member-sites/{id}/bundles/{bundle_id}/publish"
payload = { "confirmMigrations": ["2026-11-05-drop-notes"] }
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({confirmMigrations: ['2026-11-05-drop-notes']})
};
fetch('https://api.fluid.app/api/company/v2026-10/member-sites/{id}/bundles/{bundle_id}/publish', 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://api.fluid.app/api/company/v2026-10/member-sites/{id}/bundles/{bundle_id}/publish",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'confirmMigrations' => [
'2026-11-05-drop-notes'
]
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$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://api.fluid.app/api/company/v2026-10/member-sites/{id}/bundles/{bundle_id}/publish"
payload := strings.NewReader("{\n \"confirmMigrations\": [\n \"2026-11-05-drop-notes\"\n ]\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.fluid.app/api/company/v2026-10/member-sites/{id}/bundles/{bundle_id}/publish")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"confirmMigrations\": [\n \"2026-11-05-drop-notes\"\n ]\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.fluid.app/api/company/v2026-10/member-sites/{id}/bundles/{bundle_id}/publish")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"confirmMigrations\": [\n \"2026-11-05-drop-notes\"\n ]\n}"
response = http.request(request)
puts response.read_body{
"bundle": {
"id": "0199537f-0000-7000-8000-000000000004",
"manifestSha256": "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"
},
"status": 200,
"meta": {
"request_uuid": "3b8f1d27-6c4e-4a91-8e5d-2f7c9a1b0e64",
"timestamp": "2026-11-05T10:00:00Z"
}
}{}{}{
"error": {
"message": "<string>",
"details": {}
},
"status": 123,
"meta": {
"request_uuid": "<string>",
"timestamp": "2023-11-07T05:31:56Z"
}
}{
"error": {
"message": "A newer start replaced this upload",
"details": {
"error": "superseded"
}
},
"status": 409,
"meta": {
"request_uuid": "3b8f1d27-6c4e-4a91-8e5d-2f7c9a1b0e64",
"timestamp": "2026-11-05T10:00:00Z"
}
}{
"error": {
"message": "<string>",
"details": {}
},
"status": 123,
"meta": {
"request_uuid": "<string>",
"timestamp": "2023-11-07T05:31:56Z"
}
}{
"error": {
"message": "<string>",
"details": {}
},
"status": 123,
"meta": {
"request_uuid": "<string>",
"timestamp": "2023-11-07T05:31:56Z"
}
}{
"error": {
"message": "The publish ran out of time; send it again",
"details": {
"error": "publish_timeout"
}
},
"status": 504,
"meta": {
"request_uuid": "3b8f1d27-6c4e-4a91-8e5d-2f7c9a1b0e64",
"timestamp": "2026-11-05T10:00:00Z"
}
}