Skip to main content
Documentation

Reviews

reviews.reply

Publish the shop's reply to one public review
WritesSensitiveSend budget

REST

Shipping
POST /api/v1/reviews/reply

MCP tool

Live
reviews.reply

Exposed on: REST API · Shop MCP server · Claude connector · Dash (in-app copilot) · Zapier. Part of the Reviews domain.

Operating contract

PUBLISHES A PUBLIC STATEMENT UNDER THE SHOP'S NAME. It goes to the review platform itself, appears on the shop's listing where anybody can read it, and NOTHING IN THIS REGISTRY CAN TAKE IT DOWN. Removing a reply is a person at a screen.

REPLYING TO A REVIEW THAT ALREADY HAS A REPLY REPLACES IT. There is no second reply and no thread. Read reviews.get first.

WHAT NOT TO WRITE, and this is most of the job:

Never dispute the customer's account of what happened. A reply arguing with a one-star is read by every future customer as a shop that argues, whatever the facts were.

Never name what work they had done, what it cost, or anything from their record. The reviewer chose what to make public; the shop's file on them is not public, and quoting it is a privacy failure in front of an audience.

Never promise a refund, a re-do, or anything else that costs the shop money. Offer to talk, and let a person decide what to offer.

Never write a reply that could be any shop's. If you cannot say something specific and true, say something short and sincere — a generic paragraph is worse than three honest sentences.

Google caps a reply at 4096 characters. Aim for two or three sentences: a reply longer than the review reads as defensive.

If the review is rating-only with no words, there is nothing to answer. Say so rather than composing a reply to silence.

IDEMPOTENCY IS none: this is a live publish, and calling it twice publishes the second text over the first.

Who may call it

Permission
marketing.accessThe caller must hold Reviews & market at the ACT level. A read-only dashboard grant on the same section is refused.
Plan
Every planNo plan gate. Available on every Service VIN plan.
Retries
noneA repeat genuinely does it twice and no key can make it not. Read before you write; the operating contract says what to read.
Rate class
sendCounted against the send budget, the tightest one: what a runaway loop spends here is the shop's own sending reputation.

Input

FieldTypeDescription
review_idrequiredstringuuid

The review to answer, as returned by reviews.list or reviews.get.

replyrequiredstringmax 4096 chars, min 1 chars

The reply, exactly as the public will read it. Two or three sentences. No prices, no promises, no dispute.

Output

FieldTypeDescription
review_idstring

replystring | null

replied_atstring | null

replaced_existingboolean

Examples

Built from this capability's own schema — required fields and the ones carrying a default, and nothing invented. Paste one and it validates.

curl
export SERVICEVIN_API_KEY=svk_live_…

curl -X POST https://www.servicevin.com/api/v1/reviews/reply \
  -H "Authorization: Bearer $SERVICEVIN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"review_id":"9b2f1c6e-4a77-4d2b-9f31-0f1c9a8e5d20","reply":"…"}'

TypeScript (fetch)
const res = await fetch("https://www.servicevin.com/api/v1/reviews/reply", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.SERVICEVIN_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "review_id": "9b2f1c6e-4a77-4d2b-9f31-0f1c9a8e5d20",
    "reply": "…"
  }),
});

// Success and failure are both envelopes. Switch on error.code, never
// on error.message — the codes are stable, the messages are for people.
const payload = await res.json();
if (!res.ok) throw new Error(payload.error.code);
const data = payload.data;

Python (requests)
import os, requests

res = requests.post(
    "https://www.servicevin.com/api/v1/reviews/reply",
    headers={"Authorization": f"Bearer {os.environ['SERVICEVIN_API_KEY']}"},
    json={
    "review_id": "9b2f1c6e-4a77-4d2b-9f31-0f1c9a8e5d20",
    "reply": "…"
},
    timeout=30,
)
payload = res.json()
if not res.ok:
    raise RuntimeError(payload["error"]["code"])
data = payload["data"]

MCP tools/call — https://www.servicevin.com/api/mcp
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "reviews.reply",
    "arguments": {
      "review_id": "9b2f1c6e-4a77-4d2b-9f31-0f1c9a8e5d20",
      "reply": "…"
    }
  }
}

Refusals

The four gates run in this order on every surface, and the order is not arbitrary — see Authentication.

StatusCodeWhen
404not_foundThe id is unknown, or the feature is not enabled for this account. Deliberately the same answer for both.
403forbiddenThis login does not hold marketing.access.
403insufficient_scopeThe credential is read-only and this capability writes.
422validation_errorAn argument was wrong. The message names the field.
429rate_limitedToo many send calls. Back off and retry.
500internal_errorSomething failed on our side. Nothing was changed.