Promote a site's branch
Makes the branch’s published version the site’s live one. The branch’s published bundle records the source commit it was built from, and the site’s live bundle the commit it was built from. The promote goes ahead only when the branch’s commit contains the live commit and the site repository’s default branch: it moves the default branch to the branch’s commit (never forced), then publishes the branch’s bundle on the site, holding it to every rule of a bundle publish. So a promote never replaces a live version the branch does not have. When the site already serves the branch’s version, nothing changes and the answer has no replaced_bundle_id. Undo a promote by making replaced_bundle_id current again (POST /sites/{site_id}/bundles/{bundle_id}/current).
Anyone who may edit the site’s drafts promotes: its owner, and a member with an edit share, who on the branch has the owner’s rights. When the branch’s schema holds destructive migrations, the first promote is refused with 422 naming them; promote again with confirm_migrations once the person said yes.
Bearer only: a request on the cookie mount is refused with 403. It answers 404 unless staging and the sites gateway are released to the member’s company, and for a site or branch the member may not promote.
curl --request POST \
--url https://api.fluid.app/api/member/v2026-10/sites/{site_id}/branches/{slug}/promote \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"confirm_migrations": [
"2026-11-05-drop-notes"
],
"bundle_id": "0192f4e5-9c01-7b8c-9d0e-1f2a3b4c5d01"
}
'import requests
url = "https://api.fluid.app/api/member/v2026-10/sites/{site_id}/branches/{slug}/promote"
payload = {
"confirm_migrations": ["2026-11-05-drop-notes"],
"bundle_id": "0192f4e5-9c01-7b8c-9d0e-1f2a3b4c5d01"
}
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({
confirm_migrations: ['2026-11-05-drop-notes'],
bundle_id: '0192f4e5-9c01-7b8c-9d0e-1f2a3b4c5d01'
})
};
fetch('https://api.fluid.app/api/member/v2026-10/sites/{site_id}/branches/{slug}/promote', 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}/branches/{slug}/promote",
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([
'confirm_migrations' => [
'2026-11-05-drop-notes'
],
'bundle_id' => '0192f4e5-9c01-7b8c-9d0e-1f2a3b4c5d01'
]),
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}/branches/{slug}/promote"
payload := strings.NewReader("{\n \"confirm_migrations\": [\n \"2026-11-05-drop-notes\"\n ],\n \"bundle_id\": \"0192f4e5-9c01-7b8c-9d0e-1f2a3b4c5d01\"\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}/branches/{slug}/promote")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"confirm_migrations\": [\n \"2026-11-05-drop-notes\"\n ],\n \"bundle_id\": \"0192f4e5-9c01-7b8c-9d0e-1f2a3b4c5d01\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.fluid.app/api/member/v2026-10/sites/{site_id}/branches/{slug}/promote")
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 \"confirm_migrations\": [\n \"2026-11-05-drop-notes\"\n ],\n \"bundle_id\": \"0192f4e5-9c01-7b8c-9d0e-1f2a3b4c5d01\"\n}"
response = http.request(request)
puts response.read_body{
"status": 200,
"data": {
"bundle_id": "0192f4e5-9d01-7b8c-9d0e-1f2a3b4c5d01",
"replaced_bundle_id": "0192f4e5-9a01-7b8c-9d0e-1f2a3b4c5d01",
"commit": "9a8b7c6d5e4f3a2b1c0d9e8f7a6b5c4d3e2f1a0b"
},
"meta": {
"request_uuid": "0192f4c8-5b6c-7d74-a3e8-0f1a2b3c4d5e",
"timestamp": "2026-11-13T10: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": "The live site has changes the branch does not have; update the branch from live and publish it, then try again",
"details": {
"error": "live_changed"
}
},
"meta": {
"request_uuid": "0192f4c8-5b6c-7d74-a3e8-0f1a2b3c4d5e",
"timestamp": "2026-11-13T10:00:00Z"
}
}{
"status": 422,
"error": {
"message": "These destructive migrations need the owner's confirmation: 2026-11-05-drop-notes",
"details": {
"error": "destructive_change_unconfirmed",
"path": [
"schema",
"migrations"
],
"unconfirmed": [
"2026-11-05-drop-notes"
],
"bundle_id": "0192f4e5-9c01-7b8c-9d0e-1f2a3b4c5d01"
}
},
"meta": {
"request_uuid": "0192f4c8-5b6c-7d74-a3e8-0f1a2b3c4d5e",
"timestamp": "2026-11-13T10:00:00Z"
}
}{
"status": 123,
"error": {
"message": "<string>",
"details": {}
},
"meta": {
"request_uuid": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"timestamp": "2023-11-07T05:31:56Z"
}
}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
Body
The optional confirmation of a promote. Send confirm_migrations, from a 422 destructive_change_unconfirmed refusal, after the person said yes, with the refusal's bundle_id when you have it; omit both otherwise. Any other field is refused as an unknown field.
curl --request POST \
--url https://api.fluid.app/api/member/v2026-10/sites/{site_id}/branches/{slug}/promote \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"confirm_migrations": [
"2026-11-05-drop-notes"
],
"bundle_id": "0192f4e5-9c01-7b8c-9d0e-1f2a3b4c5d01"
}
'import requests
url = "https://api.fluid.app/api/member/v2026-10/sites/{site_id}/branches/{slug}/promote"
payload = {
"confirm_migrations": ["2026-11-05-drop-notes"],
"bundle_id": "0192f4e5-9c01-7b8c-9d0e-1f2a3b4c5d01"
}
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({
confirm_migrations: ['2026-11-05-drop-notes'],
bundle_id: '0192f4e5-9c01-7b8c-9d0e-1f2a3b4c5d01'
})
};
fetch('https://api.fluid.app/api/member/v2026-10/sites/{site_id}/branches/{slug}/promote', 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}/branches/{slug}/promote",
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([
'confirm_migrations' => [
'2026-11-05-drop-notes'
],
'bundle_id' => '0192f4e5-9c01-7b8c-9d0e-1f2a3b4c5d01'
]),
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}/branches/{slug}/promote"
payload := strings.NewReader("{\n \"confirm_migrations\": [\n \"2026-11-05-drop-notes\"\n ],\n \"bundle_id\": \"0192f4e5-9c01-7b8c-9d0e-1f2a3b4c5d01\"\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}/branches/{slug}/promote")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"confirm_migrations\": [\n \"2026-11-05-drop-notes\"\n ],\n \"bundle_id\": \"0192f4e5-9c01-7b8c-9d0e-1f2a3b4c5d01\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.fluid.app/api/member/v2026-10/sites/{site_id}/branches/{slug}/promote")
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 \"confirm_migrations\": [\n \"2026-11-05-drop-notes\"\n ],\n \"bundle_id\": \"0192f4e5-9c01-7b8c-9d0e-1f2a3b4c5d01\"\n}"
response = http.request(request)
puts response.read_body{
"status": 200,
"data": {
"bundle_id": "0192f4e5-9d01-7b8c-9d0e-1f2a3b4c5d01",
"replaced_bundle_id": "0192f4e5-9a01-7b8c-9d0e-1f2a3b4c5d01",
"commit": "9a8b7c6d5e4f3a2b1c0d9e8f7a6b5c4d3e2f1a0b"
},
"meta": {
"request_uuid": "0192f4c8-5b6c-7d74-a3e8-0f1a2b3c4d5e",
"timestamp": "2026-11-13T10: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": "The live site has changes the branch does not have; update the branch from live and publish it, then try again",
"details": {
"error": "live_changed"
}
},
"meta": {
"request_uuid": "0192f4c8-5b6c-7d74-a3e8-0f1a2b3c4d5e",
"timestamp": "2026-11-13T10:00:00Z"
}
}{
"status": 422,
"error": {
"message": "These destructive migrations need the owner's confirmation: 2026-11-05-drop-notes",
"details": {
"error": "destructive_change_unconfirmed",
"path": [
"schema",
"migrations"
],
"unconfirmed": [
"2026-11-05-drop-notes"
],
"bundle_id": "0192f4e5-9c01-7b8c-9d0e-1f2a3b4c5d01"
}
},
"meta": {
"request_uuid": "0192f4c8-5b6c-7d74-a3e8-0f1a2b3c4d5e",
"timestamp": "2026-11-13T10:00:00Z"
}
}{
"status": 123,
"error": {
"message": "<string>",
"details": {}
},
"meta": {
"request_uuid": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"timestamp": "2023-11-07T05:31:56Z"
}
}