Record an unanswered turn without overriding a newer decision
Append an unanswered-turn reply only while the observed run and record version still own an active comment. The checks and reply share a row lock. A newer decision or answer returns 409 with the current comment. Reply identity prevents duplicate writes during retries.
The backstop is Mist’s own words on the run, so it must come from the person who started that run. From anyone else it receives 409 and stores nothing. A run started before the starter was recorded accepts any authorized caller, as it did before.
The reply’s client_id is validated before the run and version are
compared, so a malformed id is 422 whether or not they still match.
The comment lands in needs_info, not review: the turn ended without
an answer, so there is nothing to judge and the next move is its
author’s. Ownership goes back to the person who sent it, as an answer’s
does.
The reply may carry the same shot and proof an answer carries: the
page as the turn left it, and the before taken when the run started.
They are evidence of what the turn left behind, not a claim that the
comment was addressed.
When the thread belongs to a work item, the run and the active state are read off the work item rather than the comment; the version stays the comment’s own and is never translated between the two registers.
curl --request POST \
--url https://api.fluid.app/api/v202604/themes/{theme_id}/preview_comments/{client_id}/backstop-reply \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"reply": {
"client_id": "backstop-run_01J8Z3K4QF7M2V9XB6T0RC5HWD",
"body": "This turn ended without posting an answer. The run stopped before it could change the hero section; send it again to retry."
},
"run_id": "run_01J8Z3K4QF7M2V9XB6T0RC5HWD",
"expected_version": 4
}
'import requests
url = "https://api.fluid.app/api/v202604/themes/{theme_id}/preview_comments/{client_id}/backstop-reply"
payload = {
"reply": {
"client_id": "backstop-run_01J8Z3K4QF7M2V9XB6T0RC5HWD",
"body": "This turn ended without posting an answer. The run stopped before it could change the hero section; send it again to retry."
},
"run_id": "run_01J8Z3K4QF7M2V9XB6T0RC5HWD",
"expected_version": 4
}
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({
reply: {
client_id: 'backstop-run_01J8Z3K4QF7M2V9XB6T0RC5HWD',
body: JSON.stringify('This turn ended without posting an answer. The run stopped before it could change the hero section; send it again to retry.')
},
run_id: 'run_01J8Z3K4QF7M2V9XB6T0RC5HWD',
expected_version: 4
})
};
fetch('https://api.fluid.app/api/v202604/themes/{theme_id}/preview_comments/{client_id}/backstop-reply', 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/v202604/themes/{theme_id}/preview_comments/{client_id}/backstop-reply",
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([
'reply' => [
'client_id' => 'backstop-run_01J8Z3K4QF7M2V9XB6T0RC5HWD',
'body' => 'This turn ended without posting an answer. The run stopped before it could change the hero section; send it again to retry.'
],
'run_id' => 'run_01J8Z3K4QF7M2V9XB6T0RC5HWD',
'expected_version' => 4
]),
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/v202604/themes/{theme_id}/preview_comments/{client_id}/backstop-reply"
payload := strings.NewReader("{\n \"reply\": {\n \"client_id\": \"backstop-run_01J8Z3K4QF7M2V9XB6T0RC5HWD\",\n \"body\": \"This turn ended without posting an answer. The run stopped before it could change the hero section; send it again to retry.\"\n },\n \"run_id\": \"run_01J8Z3K4QF7M2V9XB6T0RC5HWD\",\n \"expected_version\": 4\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/v202604/themes/{theme_id}/preview_comments/{client_id}/backstop-reply")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"reply\": {\n \"client_id\": \"backstop-run_01J8Z3K4QF7M2V9XB6T0RC5HWD\",\n \"body\": \"This turn ended without posting an answer. The run stopped before it could change the hero section; send it again to retry.\"\n },\n \"run_id\": \"run_01J8Z3K4QF7M2V9XB6T0RC5HWD\",\n \"expected_version\": 4\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.fluid.app/api/v202604/themes/{theme_id}/preview_comments/{client_id}/backstop-reply")
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 \"reply\": {\n \"client_id\": \"backstop-run_01J8Z3K4QF7M2V9XB6T0RC5HWD\",\n \"body\": \"This turn ended without posting an answer. The run stopped before it could change the hero section; send it again to retry.\"\n },\n \"run_id\": \"run_01J8Z3K4QF7M2V9XB6T0RC5HWD\",\n \"expected_version\": 4\n}"
response = http.request(request)
puts response.read_body{
"preview_comment": {
"id": 31877,
"run_id": "run_01J8Z3K4QF7M2V9XB6T0RC5HWD",
"lock_version": 5,
"client_id": "c-3f9a2e71",
"n": 4,
"body": "The hero headline wraps onto three lines on mobile. Can we tighten it so it fits on two?",
"status": "review",
"status_at_ms": 1789467150000,
"page_url": "/",
"author_name": "Michael Scott",
"created_at_ms": 1789398131000,
"theme_changed_at": null,
"template_id": 902114,
"section": {
"id": "hero-banner-1",
"type": "hero-banner"
},
"block": null,
"target": {
"tag": "h1",
"selector": "section[data-section-id=\"hero-banner-1\"] h1.hero__title",
"text": "Limitless Paper in a Paperless World",
"html": "<h1 class=\"hero__title\">Limitless Paper in a Paperless World</h1>"
},
"pin": {
"x": 48.2,
"y": 22.7,
"ox": 0.41,
"oy": 0.63
},
"mentions": [
{
"id": "52018",
"name": "Pam Beesly"
}
],
"mentioned_project": null,
"proof_baseline": {
"url": "https://cdn.fluid.app/dunder-mifflin/assets/77104/preview-before.png",
"width": 1440,
"height": 900,
"captured_at_ms": 1789464360000
},
"work_item": null,
"board_position": null,
"board_position_at_ms": null,
"assignee": {
"id": "52001",
"name": "Michael Scott",
"kind": "user"
},
"assigner": {
"id": "52001",
"name": "Michael Scott",
"kind": "user"
},
"assigned_at_ms": 1789467150000,
"priority": null,
"priority_at_ms": null,
"replies": [
{
"client_id": "r-5c20e9b4",
"run_id": null,
"body": "Agreed. Let's drop the font size a step below 768px rather than rewording it.",
"author_name": "Pam Beesly",
"posted_by": null,
"created_at_ms": 1789398277000,
"mentions": [],
"shot": null,
"proof": null
},
{
"client_id": "backstop-run_01J8Z3K4QF7M2V9XB6T0RC5HWD",
"run_id": "run_01J8Z3K4QF7M2V9XB6T0RC5HWD",
"body": "This turn ended without posting an answer. The run stopped before it could change the hero section; send it again to retry.",
"author_name": "Mist",
"posted_by": {
"id": "52018",
"name": "Pam Beesly",
"kind": "user"
},
"created_at_ms": 1789467150000,
"mentions": [],
"shot": null,
"proof": null
}
]
},
"status": 200,
"meta": {
"request_uuid": "7c1e9a52-4b3d-4f0e-9d2a-81f6c3b5e240",
"timestamp": "2026-09-15T10:12:30Z"
}
}{
"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": "<string>",
"details": {}
},
"status": 123,
"meta": {
"request_uuid": "<string>",
"timestamp": "2023-11-07T05:31:56Z"
}
}Authorizations
Path Parameters
Body
Response
The current comment with the backstop recorded.
A single comment, wrapped in the standard response envelope.
curl --request POST \
--url https://api.fluid.app/api/v202604/themes/{theme_id}/preview_comments/{client_id}/backstop-reply \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"reply": {
"client_id": "backstop-run_01J8Z3K4QF7M2V9XB6T0RC5HWD",
"body": "This turn ended without posting an answer. The run stopped before it could change the hero section; send it again to retry."
},
"run_id": "run_01J8Z3K4QF7M2V9XB6T0RC5HWD",
"expected_version": 4
}
'import requests
url = "https://api.fluid.app/api/v202604/themes/{theme_id}/preview_comments/{client_id}/backstop-reply"
payload = {
"reply": {
"client_id": "backstop-run_01J8Z3K4QF7M2V9XB6T0RC5HWD",
"body": "This turn ended without posting an answer. The run stopped before it could change the hero section; send it again to retry."
},
"run_id": "run_01J8Z3K4QF7M2V9XB6T0RC5HWD",
"expected_version": 4
}
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({
reply: {
client_id: 'backstop-run_01J8Z3K4QF7M2V9XB6T0RC5HWD',
body: JSON.stringify('This turn ended without posting an answer. The run stopped before it could change the hero section; send it again to retry.')
},
run_id: 'run_01J8Z3K4QF7M2V9XB6T0RC5HWD',
expected_version: 4
})
};
fetch('https://api.fluid.app/api/v202604/themes/{theme_id}/preview_comments/{client_id}/backstop-reply', 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/v202604/themes/{theme_id}/preview_comments/{client_id}/backstop-reply",
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([
'reply' => [
'client_id' => 'backstop-run_01J8Z3K4QF7M2V9XB6T0RC5HWD',
'body' => 'This turn ended without posting an answer. The run stopped before it could change the hero section; send it again to retry.'
],
'run_id' => 'run_01J8Z3K4QF7M2V9XB6T0RC5HWD',
'expected_version' => 4
]),
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/v202604/themes/{theme_id}/preview_comments/{client_id}/backstop-reply"
payload := strings.NewReader("{\n \"reply\": {\n \"client_id\": \"backstop-run_01J8Z3K4QF7M2V9XB6T0RC5HWD\",\n \"body\": \"This turn ended without posting an answer. The run stopped before it could change the hero section; send it again to retry.\"\n },\n \"run_id\": \"run_01J8Z3K4QF7M2V9XB6T0RC5HWD\",\n \"expected_version\": 4\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/v202604/themes/{theme_id}/preview_comments/{client_id}/backstop-reply")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"reply\": {\n \"client_id\": \"backstop-run_01J8Z3K4QF7M2V9XB6T0RC5HWD\",\n \"body\": \"This turn ended without posting an answer. The run stopped before it could change the hero section; send it again to retry.\"\n },\n \"run_id\": \"run_01J8Z3K4QF7M2V9XB6T0RC5HWD\",\n \"expected_version\": 4\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.fluid.app/api/v202604/themes/{theme_id}/preview_comments/{client_id}/backstop-reply")
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 \"reply\": {\n \"client_id\": \"backstop-run_01J8Z3K4QF7M2V9XB6T0RC5HWD\",\n \"body\": \"This turn ended without posting an answer. The run stopped before it could change the hero section; send it again to retry.\"\n },\n \"run_id\": \"run_01J8Z3K4QF7M2V9XB6T0RC5HWD\",\n \"expected_version\": 4\n}"
response = http.request(request)
puts response.read_body{
"preview_comment": {
"id": 31877,
"run_id": "run_01J8Z3K4QF7M2V9XB6T0RC5HWD",
"lock_version": 5,
"client_id": "c-3f9a2e71",
"n": 4,
"body": "The hero headline wraps onto three lines on mobile. Can we tighten it so it fits on two?",
"status": "review",
"status_at_ms": 1789467150000,
"page_url": "/",
"author_name": "Michael Scott",
"created_at_ms": 1789398131000,
"theme_changed_at": null,
"template_id": 902114,
"section": {
"id": "hero-banner-1",
"type": "hero-banner"
},
"block": null,
"target": {
"tag": "h1",
"selector": "section[data-section-id=\"hero-banner-1\"] h1.hero__title",
"text": "Limitless Paper in a Paperless World",
"html": "<h1 class=\"hero__title\">Limitless Paper in a Paperless World</h1>"
},
"pin": {
"x": 48.2,
"y": 22.7,
"ox": 0.41,
"oy": 0.63
},
"mentions": [
{
"id": "52018",
"name": "Pam Beesly"
}
],
"mentioned_project": null,
"proof_baseline": {
"url": "https://cdn.fluid.app/dunder-mifflin/assets/77104/preview-before.png",
"width": 1440,
"height": 900,
"captured_at_ms": 1789464360000
},
"work_item": null,
"board_position": null,
"board_position_at_ms": null,
"assignee": {
"id": "52001",
"name": "Michael Scott",
"kind": "user"
},
"assigner": {
"id": "52001",
"name": "Michael Scott",
"kind": "user"
},
"assigned_at_ms": 1789467150000,
"priority": null,
"priority_at_ms": null,
"replies": [
{
"client_id": "r-5c20e9b4",
"run_id": null,
"body": "Agreed. Let's drop the font size a step below 768px rather than rewording it.",
"author_name": "Pam Beesly",
"posted_by": null,
"created_at_ms": 1789398277000,
"mentions": [],
"shot": null,
"proof": null
},
{
"client_id": "backstop-run_01J8Z3K4QF7M2V9XB6T0RC5HWD",
"run_id": "run_01J8Z3K4QF7M2V9XB6T0RC5HWD",
"body": "This turn ended without posting an answer. The run stopped before it could change the hero section; send it again to retry.",
"author_name": "Mist",
"posted_by": {
"id": "52018",
"name": "Pam Beesly",
"kind": "user"
},
"created_at_ms": 1789467150000,
"mentions": [],
"shot": null,
"proof": null
}
]
},
"status": 200,
"meta": {
"request_uuid": "7c1e9a52-4b3d-4f0e-9d2a-81f6c3b5e240",
"timestamp": "2026-09-15T10:12:30Z"
}
}{
"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": "<string>",
"details": {}
},
"status": 123,
"meta": {
"request_uuid": "<string>",
"timestamp": "2023-11-07T05:31:56Z"
}
}