Publish a bundle
Makes an uploaded bundle the 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, so a new site stays private until its owner makes it public. 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.
Bearer only: a request on the cookie mount is refused with 403. It answers 404 until the sites gateway is released to the member’s company, for a site the member may not change, and for a bundle that is not the site’s.
curl --request POST \
--url https://api.fluid.app/api/member/v2026-10/sites/{site_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/member/v2026-10/sites/{site_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/member/v2026-10/sites/{site_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/member/v2026-10/sites/{site_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/member/v2026-10/sites/{site_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/member/v2026-10/sites/{site_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/member/v2026-10/sites/{site_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{
"status": 200,
"data": {
"bundle": {
"id": "0199537f-0000-7000-8000-000000000004",
"manifestSha256": "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"
}
},
"meta": {
"request_uuid": "0192f4c8-5b6c-7d74-a3e8-0f1a2b3c4d5e",
"timestamp": "2026-11-05T10:00:00Z"
}
}{
"status": 123,
"error": {
"message": "<string>",
"details": {}
},
"meta": {
"request_uuid": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"timestamp": "2023-11-07T05:31:56Z"
}
}{
"status": 123,
"error": {
"message": "<string>",
"details": {}
},
"meta": {
"request_uuid": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"timestamp": "2023-11-07T05:31:56Z"
}
}{
"status": 123,
"error": {
"message": "<string>",
"details": {}
},
"meta": {
"request_uuid": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"timestamp": "2023-11-07T05:31:56Z"
}
}{
"status": 409,
"error": {
"message": "A newer start replaced this upload",
"details": {
"error": "superseded"
}
},
"meta": {
"request_uuid": "0192f4c8-5b6c-7d74-a3e8-0f1a2b3c4d5e",
"timestamp": "2026-11-05T10:00:00Z"
}
}{
"status": 123,
"error": {
"message": "<string>",
"details": {}
},
"meta": {
"request_uuid": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"timestamp": "2023-11-07T05:31:56Z"
}
}{
"status": 123,
"error": {
"message": "<string>",
"details": {}
},
"meta": {
"request_uuid": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"timestamp": "2023-11-07T05:31:56Z"
}
}{
"status": 504,
"error": {
"message": "The publish ran out of time; send it again",
"details": {
"error": "publish_timeout"
}
},
"meta": {
"request_uuid": "0192f4c8-5b6c-7d74-a3e8-0f1a2b3c4d5e",
"timestamp": "2026-11-05T10:00:00Z"
}
}Authorizations
A member credential in the Authorization: Bearer header: a member
session JWT, a portal JWT whose login is linked to a membership in
that company, an opaque member token, or a verified One Auth identity
access token. The credential alone identifies the member and company.
Member session JWTs, and portal JWTs the server minted read-only, are
refused with 403 on every write.
Path Parameters
Public id of one of the member's sites.
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 owner 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/member/v2026-10/sites/{site_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/member/v2026-10/sites/{site_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/member/v2026-10/sites/{site_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/member/v2026-10/sites/{site_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/member/v2026-10/sites/{site_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/member/v2026-10/sites/{site_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/member/v2026-10/sites/{site_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{
"status": 200,
"data": {
"bundle": {
"id": "0199537f-0000-7000-8000-000000000004",
"manifestSha256": "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"
}
},
"meta": {
"request_uuid": "0192f4c8-5b6c-7d74-a3e8-0f1a2b3c4d5e",
"timestamp": "2026-11-05T10:00:00Z"
}
}{
"status": 123,
"error": {
"message": "<string>",
"details": {}
},
"meta": {
"request_uuid": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"timestamp": "2023-11-07T05:31:56Z"
}
}{
"status": 123,
"error": {
"message": "<string>",
"details": {}
},
"meta": {
"request_uuid": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"timestamp": "2023-11-07T05:31:56Z"
}
}{
"status": 123,
"error": {
"message": "<string>",
"details": {}
},
"meta": {
"request_uuid": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"timestamp": "2023-11-07T05:31:56Z"
}
}{
"status": 409,
"error": {
"message": "A newer start replaced this upload",
"details": {
"error": "superseded"
}
},
"meta": {
"request_uuid": "0192f4c8-5b6c-7d74-a3e8-0f1a2b3c4d5e",
"timestamp": "2026-11-05T10:00:00Z"
}
}{
"status": 123,
"error": {
"message": "<string>",
"details": {}
},
"meta": {
"request_uuid": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"timestamp": "2023-11-07T05:31:56Z"
}
}{
"status": 123,
"error": {
"message": "<string>",
"details": {}
},
"meta": {
"request_uuid": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"timestamp": "2023-11-07T05:31:56Z"
}
}{
"status": 504,
"error": {
"message": "The publish ran out of time; send it again",
"details": {
"error": "publish_timeout"
}
},
"meta": {
"request_uuid": "0192f4c8-5b6c-7d74-a3e8-0f1a2b3c4d5e",
"timestamp": "2026-11-05T10:00:00Z"
}
}