Sharing & feedback
Reviews & approvals
Ask for feedback, read comments and approvals, and wait for a reviewer from code.
A review is a drop that asks for feedback: reviewers open the link in any browser, pin comments on the exact spot (a pixel, a PDF page, a timestamp, a line of code) and approve it or request changes. From code you start the review, wait for that feedback, publish the next version behind the same link and repeat until it is approved. This is the same loop the MCP tools run for AI agents.
Feedback drops#
Every drop has a purpose. A "share" drop just delivers files. A "feedback" drop turns comments on by default. Approval is a separate switch, requireApproval, which adds the Approve and Request changes buttons for reviewers.
purpose"share" | "feedback"- REST default is
"feedback"; the SDK and CLI default to"share". With"feedback",allowCommentdefaults totrue. allowCommentboolean- Override the purpose's default for comments.
requireApprovalboolean- Defaults to
false. Reviewers can only record a decision when this is on. Starting a review (below) turns it on for you.
You can change all three later with PATCH /drops/{id}. Switching a drop to "share" turns comments and approval off unless the same update sets them. See Drops for every other setting.
Who can approve
People you shared the drop with record decisions. You can't approve your own drop, with one exception: a drop you started a review on with POST /drops/{id}/review, where you are the person reviewing what your agent or automation made.
The review loop#
Create a feedback drop and upload the files
POST /dropswith"purpose": "feedback", then upload each file to its target (the SDK does both indrops.create).Start the review
POST /drops/{id}/reviewlabels the drop with your agent's name and question, turns on comments and approval, records round 1 and notifies you.Wait for feedback
GET /drops/{id}/feedback?since=<cursor>&wait=60returns as soon as someone comments or decides, or after the wait.Send the next version
POST /drops/{id}/versionsplus uploads, thenPOST /drops/{id}/roundswith the comment ids the version addresses. Back to step 3 until the status isapproved.
Start a review#
/drops/{id}/reviewTurn an uploaded drop into a reviewagentNamestring, 1–60- Shown to reviewers as the author. Default
"Your AI agent". questionstring, up to 2,000- What you want the reviewer to look at or decide, shown at the top of the review.
clientstring, up to 120- Free-form name of the tool making the request, e.g.
"release-bot/2.1". createdVia"api" | "mcp"- Default
"api".
It answers 201 with the review state. Calling it again on the same drop updates the name and question.
# 1. Create a feedback drop (then upload the file to the returned target, see Drops)
curl https://api.slingdrop.my/v1/drops \
-H "Authorization: Bearer $SLINGDROP_API_KEY" -H "content-type: application/json" \
-d '{"title":"Homepage hero","purpose":"feedback","expiry":"30d","files":[{"name":"hero.png","size":482113,"type":"image/png"}]}'
curl -X PUT https://slingdrop.my/api/uploads/$FILE_ID \
-H "x-upload-token: $UPLOAD_TOKEN" --data-binary @hero.png
# 2. Start the review
curl https://api.slingdrop.my/v1/drops/$DROP_ID/review \
-H "Authorization: Bearer $SLINGDROP_API_KEY" -H "content-type: application/json" \
-d '{"agentName":"Release bot","question":"Is the headline readable on mobile?"}'import { openAsBlob } from "node:fs";
import { Slingdrop } from "@slingdrop/sdk";
const sd = new Slingdrop({ apiKey: process.env.SLINGDROP_API_KEY });
const { drop } = await sd.drops.create(
[{ name: "hero.png", data: await openAsBlob("hero.png") }],
{ title: "Homepage hero", purpose: "feedback", expiry: "30d" },
);
const review = await sd.reviews.start(drop.id, {
agentName: "Release bot",
question: "Is the headline readable on mobile?",
});
console.log(review.url); // send this to your reviewerRead and wait for feedback#
/drops/{id}/feedbackStatus, latest decision and commentssinceISO date-time (query)- The
cursorfrom your last response. Returns only new feedback, plus every comment that is still open, so you always see the full to-do list. waitseconds, 0–60 (query)- Hold the request open until a person comments or decides after
since, or until the time is up. Values above 60 are treated as 60.
Waits return early as soon as something arrives, and the response has newFeedback: true when it did. If the wait ended with nothing new, just call again with the same cursor. Your own replies don't count as new feedback. While you wait, reviewers see that your agent is listening.
curl "https://api.slingdrop.my/v1/drops/$DROP_ID/feedback?since=$CURSOR&wait=60" \
-H "Authorization: Bearer $SLINGDROP_API_KEY"// Without wait: the current state, right away
const now = await sd.reviews.feedback(drop.id);
// With wait (seconds, up to 60): returns early when there's something new
const next = await sd.reviews.feedback(drop.id, { since: now.cursor, wait: 60 });
if (next.newFeedback) console.log(next.status, next.comments);The review state#
POST /review, GET /feedback and POST /rounds all return the same object under data:
{
"data": {
"dropId": "k3f9xq",
"url": "https://slingdrop.my/d/k3f9xq",
"title": "Homepage hero",
"agent": { "name": "Release bot", "question": "Is the headline readable on mobile?" },
"status": "changes_requested",
"currentVersion": { "id": "v_01", "number": 1 },
"decision": { "decision": "changes_requested", "note": "Almost there", "by": "Ana", "version": 1, "at": "2026-10-21T09:14:03.000Z" },
"openComments": 1,
"comments": [
{
"id": "c_8k2",
"file": "hero.png",
"version": 1,
"author": "Ana",
"fromAgent": false,
"text": "Make the headline bigger",
"where": "pinned 30% from the left and 12% from the top",
"anchor": { "type": "image_pin", "x": 0.3, "y": 0.12 },
"resolved": false,
"createdAt": "2026-10-21T09:13:40.000Z",
"replies": []
}
],
"rounds": [{ "number": 1, "note": "Is the headline readable on mobile?", "resolved": [], "at": "2026-10-21T09:02:11.000Z" }],
"cursor": "2026-10-21T09:14:03.000Z",
"newFeedback": true
}
}| Field | Meaning |
|---|---|
status | The decision on the current version: waiting_for_review, changes_requested or approved. A new version starts as waiting_for_review again. |
decision | The latest decision on the current version, or null. |
openComments | Unresolved comments from people (not your own replies). |
comments[].where | The comment's location in words, e.g. at 1:42, page 3, 40% from the left…, lines 12–18 or the whole file. The raw position is in anchor. |
comments[].replies | Replies in the thread. A comment carried onto newer versions appears once, with all its replies. |
cursor | Pass as since next time. |
newFeedback | Only on calls with wait: whether something new arrived. |
Send a new version#
/drops/{id}/versionsUpload a new version behind the same link/drops/{id}/roundsRecord the round and resolve what it addressedPOST /versions takes { note?, files } (the same file list as creating a drop) and returns { version: { id, number }, files } with one upload target per file. The version goes live when its last file is uploaded; open comments are carried over to it. Then record the round:
notestring, up to 4,000- What changed, shown to the reviewer.
resolvesstring[], up to 200- Comment ids this version addresses. Each is resolved and gets a reply from your agent: "Addressed in version 2."
The reviewer is notified again. POST /rounds answers 409 with not_ready if the new version hasn't finished uploading (retry after a second), and 409 conflict if that version was already recorded or the drop isn't a review.
# 1. Create version 2 and upload its files to the returned targets
curl https://api.slingdrop.my/v1/drops/$DROP_ID/versions \
-H "Authorization: Bearer $SLINGDROP_API_KEY" -H "content-type: application/json" \
-d '{"note":"Bigger headline","files":[{"name":"hero.png","size":501877,"type":"image/png"}]}'
curl -X PUT https://slingdrop.my/api/uploads/$FILE_ID \
-H "x-upload-token: $UPLOAD_TOKEN" --data-binary @hero.png
# 2. Record the round
curl https://api.slingdrop.my/v1/drops/$DROP_ID/rounds \
-H "Authorization: Bearer $SLINGDROP_API_KEY" -H "content-type: application/json" \
-d '{"note":"Bigger headline, tighter line height","resolves":["c_8k2"]}'await sd.drops.addVersion(drop.id, [{ name: "hero.png", data: await openAsBlob("hero.png") }], "Bigger headline");
const state = await sd.reviews.round(drop.id, {
note: "Bigger headline, tighter line height",
resolves: ["c_8k2"],
});A complete loop#
import { openAsBlob } from "node:fs";
import { Slingdrop, type ReviewComment } from "@slingdrop/sdk";
const sd = new Slingdrop({ apiKey: process.env.SLINGDROP_API_KEY });
// Your own code: rebuild the file from the reviewer's comments.
declare function applyFeedback(comments: ReviewComment[]): Promise<string>;
const { drop } = await sd.drops.create(
[{ name: "hero.png", data: await openAsBlob("hero.png") }],
{ title: "Homepage hero", purpose: "feedback" },
);
let state = await sd.reviews.start(drop.id, { agentName: "Release bot" });
console.log("Review it at", state.url);
while (state.status !== "approved") {
state = await sd.reviews.feedback(drop.id, { since: state.cursor, wait: 60 });
if (state.status !== "changes_requested") continue; // still waiting
const open = state.comments.filter((c) => !c.fromAgent && !c.resolved);
const path = await applyFeedback(open);
await sd.drops.addVersion(drop.id, [{ name: "hero.png", data: await openAsBlob(path) }]);
state = await sd.reviews.round(drop.id, { note: "Addressed your comments", resolves: open.map((c) => c.id) });
}
console.log("Approved by", state.decision?.by);Reply to and resolve comments#
/drops/{id}/comments/{cid}/repliesReply as your agent/drops/{id}/comments/{cid}/resolveResolve, or reopen with resolved: false/drops/{id}/commentsEvery comment on the drop, with anchorsReplies ({ "text": "…" }, up to 4,000 characters) are labelled as coming from your agent, e.g. Release bot (AI), and only work on drops you started a review on. Resolving takes { "resolved": true | false } (default true) and works on any drop you own.
curl https://api.slingdrop.my/v1/drops/$DROP_ID/comments/$COMMENT_ID/replies \
-H "Authorization: Bearer $SLINGDROP_API_KEY" -H "content-type: application/json" \
-d '{"text":"Do you mean the headline or the subheading?"}'
curl https://api.slingdrop.my/v1/drops/$DROP_ID/comments/$COMMENT_ID/resolve \
-H "Authorization: Bearer $SLINGDROP_API_KEY" -H "content-type: application/json" \
-d '{"resolved":true}'await sd.reviews.reply(drop.id, "c_8k2", "Do you mean the headline or the subheading?");
await sd.reviews.resolve(drop.id, "c_8k2"); // resolve
await sd.reviews.resolve(drop.id, "c_8k2", false); // reopenApprovals#
/drops/{id}/approvalsAll decisions on a drop, newest firstEach decision belongs to the version that was current when it was made, so an old approval never carries over to a new version. Signers who were signed in to Slingdrop with a confirmed email have signerVerified: true.
curl https://api.slingdrop.my/v1/drops/$DROP_ID/approvals \
-H "Authorization: Bearer $SLINGDROP_API_KEY"const approvals = await sd.reviews.approvals(drop.id);{
"data": [
{
"id": "ap_1",
"versionId": "v_02",
"fileId": null,
"decision": "approved",
"signerName": "Ana",
"signerEmail": "ana@studio.com",
"signerVerified": true,
"note": "Ship it",
"createdAt": "2026-10-21T10:02:55.000Z"
}
]
}Prefer push over polling? Subscribe to comment.created, approval.created and version.created with a webhook.