Place a preview comment's card on the Tasks board
Stores where the comment’s card sits on the Tasks board: a canonical fractional index the client computed between the card’s new neighbours, so moving one card rewrites only that card, or null to take it out of the order. Positions follow the fractional-indexing format (the first character gives the integer part’s length and the fraction never ends in 0), which always leaves room between two keys; a key that is not canonical is refused. They order the whole board, in the same order work items keep, and equal positions are ordered by id.
Its own operation because it is its own register, resolved exactly as
priority is. board_position_at_ms is compared only against the
stored position’s stamp, and placing a card never changes the
comment’s lock_version, so placing a card and moving its status in
the same moment both land. A write carrying an older stamp is discarded
and answered 200 with the comment unchanged; an equal stamp is taken by
the incoming write. A null still carries its stamp, so a cleared
position is not restored by a later write holding an older one.
A thread that belongs to a work item is not writable workflow here.
This returns 409 with the current comment in
error.details.preview_comment; the work item holds the card’s
position instead.
curl --request PUT \
--url https://api.fluid.app/api/v202604/themes/{theme_id}/preview_comments/{client_id}/position \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"position_change": {
"board_position_at_ms": 1789000000000,
"board_position": "a0V"
}
}
'import requests
url = "https://api.fluid.app/api/v202604/themes/{theme_id}/preview_comments/{client_id}/position"
payload = { "position_change": {
"board_position_at_ms": 1789000000000,
"board_position": "a0V"
} }
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.put(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'PUT',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({position_change: {board_position_at_ms: 1789000000000, board_position: 'a0V'}})
};
fetch('https://api.fluid.app/api/v202604/themes/{theme_id}/preview_comments/{client_id}/position', 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}/position",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "PUT",
CURLOPT_POSTFIELDS => json_encode([
'position_change' => [
'board_position_at_ms' => 1789000000000,
'board_position' => 'a0V'
]
]),
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}/position"
payload := strings.NewReader("{\n \"position_change\": {\n \"board_position_at_ms\": 1789000000000,\n \"board_position\": \"a0V\"\n }\n}")
req, _ := http.NewRequest("PUT", 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.put("https://api.fluid.app/api/v202604/themes/{theme_id}/preview_comments/{client_id}/position")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"position_change\": {\n \"board_position_at_ms\": 1789000000000,\n \"board_position\": \"a0V\"\n }\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.fluid.app/api/v202604/themes/{theme_id}/preview_comments/{client_id}/position")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Put.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"position_change\": {\n \"board_position_at_ms\": 1789000000000,\n \"board_position\": \"a0V\"\n }\n}"
response = http.request(request)
puts response.read_body{
"preview_comment": {
"id": 31877,
"run_id": null,
"lock_version": 3,
"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": "open",
"status_at_ms": 1789398131000,
"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": null,
"work_item": null,
"board_position": "a0V",
"board_position_at_ms": 1789000000000,
"assignee": null,
"assigner": null,
"assigned_at_ms": null,
"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
}
]
},
"status": 200,
"meta": {
"request_uuid": "7c1e9a52-4b3d-4f0e-9d2a-81f6c3b5e240",
"timestamp": "2026-09-15T09:26:05Z"
}
}{
"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
Response
The comment, with the position stored or left as it was.
A single comment, wrapped in the standard response envelope.
curl --request PUT \
--url https://api.fluid.app/api/v202604/themes/{theme_id}/preview_comments/{client_id}/position \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"position_change": {
"board_position_at_ms": 1789000000000,
"board_position": "a0V"
}
}
'import requests
url = "https://api.fluid.app/api/v202604/themes/{theme_id}/preview_comments/{client_id}/position"
payload = { "position_change": {
"board_position_at_ms": 1789000000000,
"board_position": "a0V"
} }
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.put(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'PUT',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({position_change: {board_position_at_ms: 1789000000000, board_position: 'a0V'}})
};
fetch('https://api.fluid.app/api/v202604/themes/{theme_id}/preview_comments/{client_id}/position', 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}/position",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "PUT",
CURLOPT_POSTFIELDS => json_encode([
'position_change' => [
'board_position_at_ms' => 1789000000000,
'board_position' => 'a0V'
]
]),
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}/position"
payload := strings.NewReader("{\n \"position_change\": {\n \"board_position_at_ms\": 1789000000000,\n \"board_position\": \"a0V\"\n }\n}")
req, _ := http.NewRequest("PUT", 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.put("https://api.fluid.app/api/v202604/themes/{theme_id}/preview_comments/{client_id}/position")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"position_change\": {\n \"board_position_at_ms\": 1789000000000,\n \"board_position\": \"a0V\"\n }\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.fluid.app/api/v202604/themes/{theme_id}/preview_comments/{client_id}/position")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Put.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"position_change\": {\n \"board_position_at_ms\": 1789000000000,\n \"board_position\": \"a0V\"\n }\n}"
response = http.request(request)
puts response.read_body{
"preview_comment": {
"id": 31877,
"run_id": null,
"lock_version": 3,
"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": "open",
"status_at_ms": 1789398131000,
"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": null,
"work_item": null,
"board_position": "a0V",
"board_position_at_ms": 1789000000000,
"assignee": null,
"assigner": null,
"assigned_at_ms": null,
"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
}
]
},
"status": 200,
"meta": {
"request_uuid": "7c1e9a52-4b3d-4f0e-9d2a-81f6c3b5e240",
"timestamp": "2026-09-15T09:26:05Z"
}
}{
"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"
}
}