ReviewComet API
Endpoints, parameters, code samples in 15 languages, and a console that runs them.
The ReviewComet API is a plain JSON API over HTTPS for everything the app does: reading reviews, acting on them, configuring the automation that acts for you, and provisioning the clients you sell to. Every path hangs off one base URL at https://api.reviewcomet.com, with a bearer key in a header, snake_case fields, ISO 8601 timestamps in UTC, and the same error shape everywhere.
Quick start
- 1
Get your API key
Open ReviewComet and go to Settings, then Developers. Mint a key, tick the scopes this integration needs, and copy the value straight into your secret store.
The key is shown once and never again, because only its hash is stored. If you lose it, revoke it and mint another.
- 2
Find your location and source ids
Most calls need neither. A location is a
workspace_idin the API: a key bound to 1 location already answers for it, and the reviews list reads every location a key covers unless you name some.GET /v1/locationslists the locations your key covers andGET /v1/sourcesthe review sources connected on them. Rules and saved replies take 1workspace_idon a key that covers several.Where an endpoint does take a
workspace_idor asource_id, you can also read it off a row you already have: every review carries both.{ "id": "cmt_1v6z0c3r7t2y5u9i4q8n", "workspace_id": "ws_4q8n1v6z0c3r7t2y5u9i", "source_id": "ch_facebook_104928375610283", "site": "facebook" } - 3
Make your first request
Ask for the 5 newest reviews the key can see:
curl "https://api.reviewcomet.com/v1/reviews?limit=5" \ -H "Authorization: Bearer ck_live_EXAMPLEKEY00000000000000000000000000000"
The answer is a page of data and a cursor to walk it with:
{ "data": [ { "id": "cmt_1v6z0c3r7t2y5u9i4q8n", "platform": "facebook", "message": "Do you ship to Norway, and how long does it take?", "status": "visible", "created_time": "2026-09-12T09:41:18.000Z" } ], "has_more": true, "next_cursor": "MjAyNi0wOS0xMlQwOTo0MToxOC4wMDBafGNtdF8x" }A 401 here means the header did not arrive as the server expected it. A 403 means the key is real but is missing
reviews:read.
Next steps
Concepts
How keys, limits, paging, retries and errors work, once, for every endpoint below.
Authentication #
Send your key as a bearer token, and know what a missing or too-narrow key answers.
Every call carries your key in an Authorization header:
Authorization: Bearer ck_live_EXAMPLEKEY00000000000000000000000000000
An X-API-Key header holding the bare key does the same job, and either form works. Use whichever your HTTP client makes easy.
The base URL for every path in this reference is https://api.reviewcomet.com, so a first call looks like this:
curl "https://api.reviewcomet.com/v1/reviews?limit=5" \ -H "Authorization: Bearer ck_live_EXAMPLEKEY00000000000000000000000000000"
A key is a credential, not a public token. It grants everything its scopes allow, on every workspace it reaches, with no second factor and no confirmation step. Keep it on a server you control, in an environment variable or a secret store, and never ship it in client-side code, a mobile app, or anything a browser downloads.
A key is 32 random bytes behind a cosmetic prefix, and only its hash is stored. The full value therefore exists in exactly 1 response body, once: copy it when it is shown, because ReviewComet cannot show it again. No key call ever answers a cookie, and a key never reaches the screens a person signs in to.
A call with no key, or with a key that is unknown, revoked or expired, answers 401:
{
"error": {
"code": "unauthorized",
"message": "This endpoint needs an API key."
}
}
A key that is real but does not reach this far answers 403, and the message says which limit it ran into:
{
"error": {
"code": "forbidden",
"message": "This key needs one of: reviews:act."
}
}What your key reaches #
Your account decides how far a key reaches, and its scopes decide what it may do there.
There is 1 API key, and 2 things decide what you can do with it.
- Your account. A key is minted on an account and reaches exactly as far as that account does. A key on a single location reaches that location. A key on an agency account also reaches the client locations on its book, which is what the Clients group is for. A key on the platform's own account reaches everything. You do not choose this and there is nothing to switch on: it already follows from who you are.
- The scopes you gave it. On the Developers screen you tick exactly what this key may do: read reviews, act on them, read sources and locations, read or write rules, read saved replies, usage and events, manage outbound webhooks, read or write clients, allocate credits.
Give a key only the scopes its job needs, and mint a separate key for a separate job rather than widening the first one. Revoking a narrow key costs you 1 integration. Revoking a wide one costs you all of them.
Beside the scopes you can pin a key to particular locations, give it an expiry date, and cap how many billable actions it may take in a month. A spent cap answers 429 key_cap_reached and clears with the billing cycle.
Reaching past any of these is 1 answer and never a surprise: 403 with the code forbidden, and a message naming which limit it was. There are 3 of them:
This key needs one of: reviews:act.A missing scope, naming every scope that would have done.That location is not in this key's allowlist.A location the key is not pinned to.That account is not yours.An account this key's own account does not reach.
Nothing else changes across any of them: the same key, the same header, the same base URL.
Rate limits #
Every key gets a per-minute budget, counted separately for reads and for writes.
Requests are counted per key in fixed windows of 60 seconds, with reads and writes counted separately. A write is also a read, so it counts against both, and the tighter of the 2 is what a refusal tells you about.
| Plan | Reads a minute | Writes a minute |
|---|---|---|
| Every other plan | 600 | 60 |
| Agency | 1,200 | 120 |
| Whitelabel | 3,000 | 300 |
The numbers come off the plan of the key's owner rather than off the key itself, because what an integration may spend is a property of what its owner bought. A whitelabel partner's workspace key gets the whitelabel row.
Every successful answer carries x-ratelimit-limit, x-ratelimit-remaining and x-ratelimit-reset. A refusal carries a Retry-After header in seconds beside this body:
{
"error": {
"code": "rate_limited",
"message": "Too many requests. Try again in 34 seconds.",
"retry_after": 34
}
}
Back off on the header rather than on a guess. A retry that ignores it spends the next window on the same refusal:
async function call(url, init, attempt = 0) {
const res = await fetch(url, init);
if (res.status !== 429 || attempt >= 5) return res;
const wait = Number(res.headers.get("retry-after") ?? 1);
await new Promise((done) => setTimeout(done, wait * 1000));
return call(url, init, attempt + 1);
}
The budget covers the automation endpoints, the Clients endpoints, the assistant endpoint, and the provisioning endpoints, which are metered at the platform tier. An assistant message costs 1 read whatever it goes on to do, and the write it dispatches is counted once, where the write is.
Pagination #
Lists walk by keyset cursor, newest first, so a page cannot skip or repeat a row.
Every list is a keyset walk on (created_time, id) or (created_at, id), newest first. That is what stops a page skipping or repeating a row when new rows land mid-walk, which an offset cannot do. 2 query parameters drive it:
limit, between 1 and 200, defaulting to 50.cursor, the opaque value the previous page handed back.
Every list answers the same envelope:
{
"data": [ ... ],
"has_more": true,
"next_cursor": "MjAyNi0wOS0xMlQwOTo0MToxOC4wMDBafGNtdF8x"
}
Walk it by passing next_cursor back as cursor until has_more is false and next_cursor is null:
let cursor = null;
do {
const url = new URL("https://api.reviewcomet.com/v1/reviews");
url.searchParams.set("limit", "100");
if (cursor) url.searchParams.set("cursor", cursor);
const page = await (await fetch(url, { headers })).json();
handle(page.data);
cursor = page.next_cursor;
} while (cursor);
A cursor is a base64url pair of the sort key and the id. Do not build one by hand, and do not carry one across a change of filters: the walk it describes no longer exists, and the answer is 400 bad_request.
Idempotency #
Every mutation carries an Idempotency-Key, so a retry cannot act twice.
Every mutation on the automation and Clients endpoints carries an Idempotency-Key header, and it is required: without one the call is refused with 400 idempotency_key_required before the handler runs.
Idempotency-Key: a2f1c9d4-8b76-4e05-9a31-77c0e4d21b8f
Anything from 8 to 128 URL-safe characters works, and a UUID is the obvious choice. The first call to arrive with a given key stores its exact response bytes, and a replay inside the next 24 hours reads those bytes back with an Idempotent-Replay: true header, without reaching the handler at all.
That is what makes "no second reply sent, nothing charged twice" structural rather than a promise. A replay never runs the handler, so there is nothing left to charge for.
- A key is scoped to 1 call: the method, the path and the body it first carried. Reuse it on a different endpoint, or with a different body, and the answer is 409
conflict, because replaying the first call's response would be the API doing 1 thing and reporting another. - A second call while the first is still in flight is refused with 409
conflictas well, so 2 racing retries cannot both proceed. - A failure is not remembered. If the handler threw, or answered 5xx, the claim is released so the same key can be retried once the cause is fixed. A considered 4xx is stored and replayed.
Generate a fresh key per logical operation and endpoint, and reuse it for every retry of that call. A new key per attempt protects nothing, and 1 key for a whole script run buys a 409 on the next endpoint it touches.
The provisioning endpoints are the exception: there the header is optional. Send one and it is honoured in full, with the same replay, the same 24-hour window and the same 409 conflict on a different body; send none and the call is never refused for it. The provisioning hook has a second guard of its own, a single claim per event per external_id, which is why a redelivery there is answered as a duplicate rather than provisioning again.
Errors #
Every refusal has the same shape, and code is the half your integration switches on.
Every refusal in this API has 1 shape:
{
"error": {
"code": "validation_failed",
"message": "message is required."
}
}
code is the stable half and is what you switch on. message is for a person reading a log and may be reworded at any time, so never match on it. Some refusals carry an extra member the caller is meant to act on: retry_after, credits_remaining, allocated and used, monthly_action_cap.
Every code the public endpoints send:
| Code | Status | What happened |
|---|---|---|
unauthorized | 401 | No key, or an unknown, revoked or expired one. |
forbidden | 403 | The key is real and may not do this: a missing scope, a location outside its allowlist, or a platform-only endpoint. |
plan_required | 403 | The plan does not include this feature. |
not_found | 404 | No such row, and also the answer for somebody else's row, so no refusal confirms a guessed id. |
bad_request | 400 | A malformed cursor, an unknown tab, or a method the endpoint does not take. |
validation_failed | 422 | A field value the handler will not take. |
idempotency_key_required | 400 | A mutation arrived without the header. |
reason_required | 400 | A credit grant arrived with no reason on it. |
conflict | 409 | An idempotency key reused with a different body or still in flight, or a reply already sending. |
credits_depleted | 409 | The account's credit pool is empty. Reads keep working. |
allocation_depleted | 409 | The client's whole allocation for this cycle is spent. |
allocation_unset | 409 | The client has no allocation yet, so none of their locations may spend. |
processing_paused | 409 | Automation is paused on this account or by its plan. |
capability_unsupported | 409 | The platform does not offer that action, and never a silent no-op. |
pool_insufficient | 409 | The credit pool cannot back the allocation asked for. |
rate_limited | 429 | This key's per-minute window is spent. |
key_cap_reached | 429 | This key's monthly action cap is spent. |
internal | 500 | Ours. Retry with the same idempotency key. |
Retry rate_limited and internal, honouring Retry-After where it is sent. The 409s are decisions rather than weather: retrying one without changing anything gets the same answer.
The assistant endpoint is the 1 place this shape is not used. It speaks JSON-RPC, so its refusals arrive as JSON-RPC errors.
Credits #
An action through the API costs what the same action costs in the app, and reads cost nothing.
An action taken through this API costs exactly what the same action costs inside the app, and nothing extra. There is no API surcharge and no second meter: a reply your integration sends and a reply somebody sends by clicking in ReviewComet draw on the same balance, at the same rate.
Reads cost nothing at all, and they stay free on an expired plan and an empty wallet. Listing reviews, reading a rule, reading sources, usage or events: none of it touches the balance.
The rate depends on what the action is. A rule action and an AI-generated reply are not priced the same, and the rate for each is the one on your billing screen, which is the single place those numbers are published. The endpoints that charge answer with credits_burned beside the data, read off the ledger row rather than off anything written here, so what you read always agrees with what you were charged.
The charge lands after the action succeeded. A refusal from the platform, a network failure, and an idempotent replay all cost nothing.
What never charges: creating, changing or deleting a rule, enabling and disabling one, backtesting one, reading sources, usage or events, and managing outbound webhooks. Configuring automation is free, and the charge lands when the automation acts, exactly as it does for a rule somebody built in the app.
3 separate things can pause actions, and each says so with its own code:
credits_depleted: the account's credit pool is empty, and a credit pack is the way out.allocation_depleted: the client's credit pool for this cycle is spent, and every location of that client draws on it. Under an agency this is reached long before the account's pool is, and it needs a bigger allocation rather than a pack.key_cap_reached: the monthly action cap set on this key is spent.
GET /v1/usage is where an integration reads all of it: the cycle, what has been spent, what is left, and the same figures per location.
Outbound webhooks #
Subscribe an https endpoint and each event is posted to it, signed, with a retry ladder.
Subscribe an https endpoint with POST /v1/webhooks and ReviewComet posts each event to it as it happens. Every delivery has the same body:
{
"id": "whd_3r7t2y5u9i4q8n1v6z0c",
"type": "review.actioned",
"created_at": "2026-09-14T10:04:22.000Z",
"workspace_id": "ws_4q8n1v6z0c3r7t2y5u9i",
"data": { ... }
}
and these headers:
x-signature-256:sha256=followed by the hex HMAC-SHA256 of the raw body under your signing secret.x-signature-256-previous: the same body signed with the secret you just rotated away. Sent for 24 hours after a rotation and then not at all, so a receiver that has not deployed the new secret keeps working and can watch the window close.x-timestamp: when this attempt was made.x-delivery-id: the attempt's own id, stable across retries of the same delivery.x-event-type: the same value astypein the body, for routing before you parse.
Verify before you trust, over the raw bytes you received. Re-serializing the JSON changes key order and spacing, and every check after that fails in a way that looks exactly like a wrong secret.
import crypto from "node:crypto";
function verify(rawBody, header, secret) {
const expected = "sha256=" + crypto
.createHmac("sha256", secret)
.update(rawBody, "utf8")
.digest("hex");
const a = Buffer.from(expected);
const b = Buffer.from(header ?? "");
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
Retries. A delivery that does not get a 2xx is retried after 1 minute, 5 minutes, 30 minutes, 2 hours and 6 hours, then hourly, and is given up 24 hours after it was queued. Answer 2xx as soon as you have stored the event and do your work afterwards: a slow receiver is a retried receiver.
Once per event. An event keeps its id on every retry and is raised once per version of what it describes (each edit, each reply), so a stored id makes a duplicate free to ignore.
The events. Subscribe to any of review.created, review.updated, review.replied, review.reply_failed, review.actioned, reply.pending_approval, private_feedback.received, source.connected, source.disconnected, source.access_lost, location.created, location.deleted, credits.low, credits.depleted, rule.fired and the 6 client.* lifecycle events, or send ["*"] for all of them. The review events carry ids, stars, site and state, never the review's text: read GET /v1/reviews/:id for the words. An endpoint scoped to some locations hears only about those, and its credits.* events carry no account figures. A Slack destination, set up in your settings, may also take digest.daily, a once-a-day summary.
review.* (but review.actioned), private_feedback.received, reply.pending_approval
data: review_id, workspace_id, source_id, site, rating (1-5 or null),
recommendation (positive | negative on a Facebook recommendation, else null),
is_private_feedback, reply_status, author_name (first name), created_time
review.updated + changed (any of "text", "rating", "recommendation")
review.replied + replied_at, method (posted | copied), by { kind, name }
review.reply_failed + failed_at, error_code
reply.pending_approval + reason (draft | platform_hold), queued_at
review.actioned review_id, action, actor, status, platform, platform_synced, at
source.connected source_id, workspace_id, site, reconnected
source.disconnected source_id, workspace_id, site
source.access_lost source_id, workspace_id, site
location.created/deleted location_id, name
credits.low/depleted account_id, workspace_id, allowance, burned, remaining, pct,
cycle_start, cycle_end (scoped endpoint: workspace_id, event)
rule.fired object, plus the rule's own detail
client.* the client and its workspaceAssistants and MCP #
A Model Context Protocol server over this same API, so an assistant can call it as tools.
POST https://api.reviewcomet.com/mcp is a Model Context Protocol server over this same API, so an assistant such as a chat client or an editor can call it as a set of tools. Point your client at that URL with your API key as the bearer token and nothing else is needed: it is stateless, so there is no session to open, no stream to keep and no second credential anywhere.
The plan feature mcp gates the endpoint as a whole. Every tool dispatches to one of the REST paths in this reference using your own key, so the scopes on that key mean here exactly what they mean there, a location outside its allowlist is refused the same way, and an action is metered once rather than twice.
The tools, which appear only when your account and your key's scopes reach them:
- Reviews:
reviews_list,reviews_get,reviews_stats,reviews_reply,reviews_mark_replied,reviews_approve,reviews_reject,reviews_draft_reply,reviews_assign,reviews_archive,reviews_unarchive,reviews_flag,reviews_unflag. - Context:
sources_list,locations_list,saved_replies_list,rules_list,rules_backtest,usage_get. - Clients, on an agency account:
clients_list,clients_get,clients_usage,clients_allocate_credits,clients_provision.
3 answers are HTTP rather than JSON-RPC, on purpose: 401 with a WWW-Authenticate: Bearer challenge when there is no key, 403 when the plan does not include this, and 405 on GET and DELETE, which a stateless server has nothing to do with.
A rate limit goes the other way and comes back as a JSON-RPC error carrying retry_after, because the client is mid-conversation and needs to match the refusal to the message it sent.
3 prompts ship with the server: triage_new_reviews, weekly_review_summary and unanswered_low_ratings. Each reads first and asks before it spends a credit.
Reviews
Read the reviews your locations have collected and act on them: reply, record a reply posted by hand, approve or discard a drafted reply, ask for an AI draft, assign, archive and flag. Reads cost nothing; an action costs what the same action costs in the app.
Reviews #
List reviews
List reviews across the locations this key covers, or the ones you name, filtered by stars, site, source, reply status, private feedback or date. The list is newest first on (created_time, id) and pages with a cursor.
| Name | In | Type | Description |
|---|---|---|---|
| workspace_id | query | string optional | Which locations to read. Repeat it for several. Omit it for every location the key covers; a platform key must name at least 1. ws_4q8n1v6z0c3r7t2y5u9i |
| rating | query | integer optional | Only reviews with exactly this many stars, 1 to 5. 2 |
| rating_min | query | integer optional | Only reviews with at least this many stars. A review with no rating never matches a bound. 1 |
| rating_max | query | integer optional | Only reviews with at most this many stars. 3 |
| site | query | enum: google | facebook | trustpilot | app_store | google_play | custom optional | Only reviews from this review site. Repeat it for several. facebook |
| source_id | query | string optional | Only reviews from this connected review source. ch_facebook_104928375610283 |
| reply_status | query | enum: none | draft | pending_approval | sending | sent | copied | failed optional | Only reviews whose reply is in this state. Repeat it for several. none |
| is_private_feedback | query | boolean optional | True for private feedback only, false for published reviews only. Omit it for both. false |
| from | query | string optional | ISO 8601 date or timestamp. Only reviews created on or after it; a bare date covers the whole day. 2026-09-01 |
| to | query | string optional | ISO 8601 date or timestamp. Only reviews created on or before it. 2026-09-30 |
| q | query | string optional | Case-insensitive text match on the review text, its title, its translation or the author name. delivery |
| tab | query | enum: inbox | archive | all optional | Which shelf to read. Defaults to all, which is the inbox and the archive. inbox |
| cursor | query | string optional | The next_cursor from the previous page. MjAyNi0wOS0xMlQwOTo0MToxOC4wMDBafGNtdF8x |
| limit | query | integer optional | Page size, 1 to 200. Defaults to 50. 50 |
Failure modes and quotas: Errors · Rate limits
Read one review
Read one review with its stars, its text, its site and the state of its reply. The body is the same object the list returns, and reply_updated_at is the draft version an approval sends back.
| Name | In | Type | Description |
|---|---|---|---|
| id | path | string required | The review id. cmt_1v6z0c3r7t2y5u9i4q8n |
Failure modes and quotas: Errors · Rate limits
Review statistics
The numbers behind the Overview and Insights screens, over the locations this key covers or the ones you name: totals, the average rating, how many are answered, and how many still wait for a reply or an approval (those 2 queues count every review, not the window). On a plan with insights or the client report it also splits them by location, site, star rating, month and day, with the response rate and the median minutes to a reply; without it those fields are null and breakdowns is false.
| Name | In | Type | Description |
|---|---|---|---|
| workspace_id | query | string optional | Which locations to count. Repeat it for several. Omit it for every location the key covers; a platform key must name at least 1. A location outside the key's set is a 403. ws_4q8n1v6z0c3r7t2y5u9i |
| days | query | integer optional | The window: 7, 30 or 90 whole UTC days ending today. Other values snap to the nearest. Defaults to 30. 30 |
| source_id | query | string optional | Count one review source only. A source outside the set counts nothing. ch_facebook_104928375610283 |
Failure modes and quotas: Errors · Rate limits
Replies #
Reply to a review
Post a public reply to one review on a site that takes replies through its API. A retry under the same Idempotency-Key replays the first answer and posts nothing twice.
| Name | In | Type | Description |
|---|---|---|---|
| id | path | string required | The review to answer. cmt_1v6z0c3r7t2y5u9i4q8n |
| message | body | string required | What to say. Empty or whitespace only is a 422. Thank you, Dana. We are glad the delivery was quick. |
Failure modes and quotas: Errors · Rate limits
Mark a review as replied
Record a reply that was posted on the review site by hand, with the text that went out. Nothing is sent anywhere, and the review reads copied from then on.
| Name | In | Type | Description |
|---|---|---|---|
| id | path | string required | The review that was answered. cmt_1v6z0c3r7t2y5u9i4q8n |
| text | body | string required | The reply as it was posted on the site. Thank you, Dana. We are glad the delivery was quick. |
Failure modes and quotas: Errors · Rate limits
Approve a drafted reply
Approve the reply draft waiting on a review and post it, exactly as the approvals screen does. Send the draft's reply_updated_at as you read it: a draft that changed since is refused, and a draft is posted once however often the call is retried.
| Name | In | Type | Description |
|---|---|---|---|
| id | path | string required | The review whose draft to approve. cmt_1v6z0c3r7t2y5u9i4q8n |
| reply_updated_at | body | string required | The draft's version: reply_updated_at from the review you read. 2026-09-12T09:58:02.000Z |
Failure modes and quotas: Errors · Rate limits
Discard a drafted reply
Discard the reply draft on a review. Nothing is posted, and the review goes back to having no reply.
| Name | In | Type | Description |
|---|---|---|---|
| id | path | string required | The review whose draft to discard. cmt_1v6z0c3r7t2y5u9i4q8n |
Failure modes and quotas: Errors · Rate limits
Draft a reply with AI
Write an AI draft of a reply to one review and return it to edit: nothing is posted or saved. It costs what an AI reply draft costs in the app, which the billing screen lists, and a retry under the same Idempotency-Key is never charged twice.
| Name | In | Type | Description |
|---|---|---|---|
| id | path | string required | The review to draft a reply to. cmt_1v6z0c3r7t2y5u9i4q8n |
Failure modes and quotas: Errors · Rate limits
Actions #
Act on a review
Archive, unarchive, assign, flag or unflag a single review. The charge lands only after the action succeeded.
| Name | In | Type | Description |
|---|---|---|---|
| id | path | string required | The review to act on. cmt_1v6z0c3r7t2y5u9i4q8n |
| action | body | enum: archive | unarchive | assign | flag | unflag required | What to do. Anything else is a 422. assign |
| assigned_to | body | string optional | The user id to assign to, for the assign action. Null clears the assignment. usr_8n1v6z0c3r7t2y5u9i4q |
| reason | body | string optional | A note kept on the activity row, for the flag action and for your own audit trail. Mentions a refund |
Failure modes and quotas: Errors · Rate limits
Sources and locations
Where reviews come from: the locations a key covers and the review sources connected on each. Both are free reads.
Locations #
List locations
List the locations this key covers, with how many review sources each has connected. A key pinned to some locations lists those and nothing else.
| Name | In | Type | Description |
|---|---|---|---|
| account_id | query | string optional | Platform keys only: the account whose locations to list. A customer key reads its own account and may omit it. acc_7t2y5u9i4q8n1v6z0c3r |
| cursor | query | string optional | The next_cursor from the previous page. The list is oldest first on (created_at, id). MjAyNi0wNi0wMlQwODo0NTowMC4wMDBafGNoXzE |
| limit | query | integer optional | Page size, 1 to 200. Defaults to 50. 50 |
Failure modes and quotas: Errors · Rate limits
Sources #
List review sources
List the review sources connected on the locations this key covers, with the site, the connection state and whether replies can be posted there through the API. Name locations with workspace_id to narrow it.
| Name | In | Type | Description |
|---|---|---|---|
| workspace_id | query | string optional | Which locations to read. Repeat it for several. Omit it for every location the key covers; a platform key must name at least 1. ws_4q8n1v6z0c3r7t2y5u9i |
| cursor | query | string optional | The next_cursor from the previous page. The list is oldest first on (created_at, id). MjAyNi0wNi0wMlQwODo0NTowMC4wMDBafGNoXzE |
| limit | query | integer optional | Page size, 1 to 200. Defaults to 50. 50 |
Failure modes and quotas: Errors · Rate limits
Rules
The automation rules that run against every review as it arrives. Configuring a rule is free; the charge lands when the rule acts, exactly as for a rule built in the app.
Rules #
List rules
List the automation rules in a location, in the order the engine runs them: first to run at the top, which is the order the Automation screen drags, grouped by location when the key reaches several and names none. Configuration only: what a rule has actually done is read from the events endpoint.
| Name | In | Type | Description |
|---|---|---|---|
| workspace_id | query | string optional | Which location to read. A key bound to a single location may omit it; a key that reaches several and omits it gets them all, grouped by location and each in its own running order. ws_4q8n1v6z0c3r7t2y5u9i |
| cursor | query | string optional | The next_cursor from the previous page. MjAyNi0wOC0wNFQxMTowMjo0NC4wMDBafHJ1bGVfMQ |
| limit | query | integer optional | Page size, 1 to 200. Defaults to 50. 50 |
Failure modes and quotas: Errors · Rate limits
Create a rule
Create an automation rule. A definition the engine cannot read is refused at the moment of the write rather than stored as a rule that never fires.
| Name | In | Type | Description |
|---|---|---|---|
| workspace_id | body | string optional | Which location the rule belongs to. Required on a key that reaches more than one. ws_4q8n1v6z0c3r7t2y5u9i |
| name | body | string required | What the rule is called in the app and in the activity trail. Flag refund requests |
| definition | body | object required | The rule itself: a trigger, a list of actions, an optional delay and an optional scope. [object Object] |
| definition.trigger.type | body | enum: rating_at_or_below | rating_at_or_above | contains_keyword | exact_keyword | no_text | site | suspicious | private_feedback required | What makes the rule fire. contains_keyword |
| definition.actions[].type | body | enum: reply | flag | assign | notify | webhook | archive required | What the rule does when it fires. A reply on a site without a reply API is saved as a draft to copy. A webhook step POSTs { event: "review.rule_webhook", review_id, ... } once to its https url; it is UNSIGNED (header X-Webhook-Signature: none), unlike integration events. flag |
| definition.delay.mode | body | enum: none | fixed | random optional | Wait before acting. Defaults to none. none |
| definition.scope.placement | body | enum: posts | ads | both optional | Which placements the rule watches. Defaults to both. both |
| enabled | body | boolean optional | Start the rule switched on. Defaults to true. true |
| trial_mode | body | boolean optional | Record what the rule would have done without doing it. Defaults to false. false |
Failure modes and quotas: Errors · Rate limits
Read one rule
Read one automation rule with its conditions and actions. readable is false when the stored definition is a shape the engine cannot run.
| Name | In | Type | Description |
|---|---|---|---|
| id | path | string required | The rule id. rule_9i4q8n1v6z0c3r7t2y5u |
Failure modes and quotas: Errors · Rate limits
Change a rule
Change an automation rule. Every field is optional and anything omitted keeps the value it had.
| Name | In | Type | Description |
|---|---|---|---|
| id | path | string required | The rule id. rule_9i4q8n1v6z0c3r7t2y5u |
| name | body | string optional | A new name. Flag refund and chargeback requests |
| definition | body | object optional | A whole new definition. It replaces the old one and is validated the same way a create is. [object Object] |
| enabled | body | boolean optional | Switch the rule on or off. true |
| trial_mode | body | boolean optional | Switch recording-only mode on or off. false |
Failure modes and quotas: Errors · Rate limits
Delete a rule
Delete an automation rule. The rule stops firing immediately and is not recoverable.
| Name | In | Type | Description |
|---|---|---|---|
| id | path | string required | The rule id. rule_9i4q8n1v6z0c3r7t2y5u |
Failure modes and quotas: Errors · Rate limits
Enable a rule
Turn an automation rule on. The body is ignored; the Idempotency-Key header is not.
| Name | In | Type | Description |
|---|---|---|---|
| id | path | string required | The rule id. rule_9i4q8n1v6z0c3r7t2y5u |
Failure modes and quotas: Errors · Rate limits
Disable a rule
Turn an automation rule off without deleting it. The body is ignored; the Idempotency-Key header is not.
| Name | In | Type | Description |
|---|---|---|---|
| id | path | string required | The rule id. rule_9i4q8n1v6z0c3r7t2y5u |
Failure modes and quotas: Errors · Rate limits
Backtest #
Backtest a rule
Run a rule against past reviews and see what it would have done, changing nothing. It takes a trigger and a scope rather than a rule id, so a rule that was never saved can be tested.
| Name | In | Type | Description |
|---|---|---|---|
| workspace_id | body | string optional | Which location's history to scan. Required on a key that reaches more than one. ws_4q8n1v6z0c3r7t2y5u9i |
| trigger | body | object required | The trigger to test, validated exactly as a create validates it. [object Object] |
| scope | body | object optional | Narrow the test to some review sources. [object Object] |
Failure modes and quotas: Errors · Rate limits
Saved replies
The saved reply library a location answers reviews with. A free read.
Saved replies #
List saved replies
List the saved replies a location can answer with, most recently edited first. There is no plan gate on this one.
| Name | In | Type | Description |
|---|---|---|---|
| workspace_id | query | string optional | Which location to read. Required on a key that reaches more than one. ws_4q8n1v6z0c3r7t2y5u9i |
| cursor | query | string optional | The next_cursor from the previous page. MjAyNi0wOS0xMFQxNToyMDowOS4wMDBafHNycF8x |
| limit | query | integer optional | Page size, 1 to 200. Defaults to 50. 50 |
Failure modes and quotas: Errors · Rate limits
Usage and insights
What this key's account has spent, what its locations did, and the trail of everything that happened. All free reads.
Usage #
Read usage
Read credits granted, burned and left this cycle, split by location, where allocated_credits and used_credits are the client's credit pool, shared by all of their locations, beside used_here for what that location alone spent and pool_workspace_id for which pool it draws on. A key bound to a single location reads its own locations only and never the account's credit pool behind them; group the rows by pool_workspace_id and read each pool once, because the siblings of a client repeat the same allocated_credits and used_credits and adding the rows up overcounts; allocated_credits is the monthly allocation and cycle_topup_credits any one-off extra credits given for this cycle only, so the cap this cycle is both added together.
Failure modes and quotas: Errors · Rate limits
Events #
List events
Walk the activity trail, newest first, with a cursor. These are the same events an outbound webhook delivers, so a system that cannot take a webhook can poll instead.
| Name | In | Type | Description |
|---|---|---|---|
| workspace_id | query | string optional | Only events about a single location. A key bound to one is narrowed to its own whether it names it or not. ws_4q8n1v6z0c3r7t2y5u9i |
| type | query | string optional | Only events of this kind, matched on the trail's own verb. reply_sent |
| since | query | ISO 8601 timestamp optional | Only events at or after this moment. 2026-09-01T00:00:00.000Z |
| cursor | query | string optional | The next_cursor from the previous page. MjAyNi0wOS0xM1QxNjowMjoxMS4wMDBafGV2dF8x |
| limit | query | integer optional | Page size, 1 to 200. Defaults to 50. 50 |
Failure modes and quotas: Errors · Rate limits
Webhooks
Push the same events the events endpoint serves to an endpoint of your own, signed and retried for 24 hours.
Subscriptions #
List subscriptions
List the endpoints this account is subscribed with. A key bound to a single location sees only subscriptions whose allowlist is entirely inside its own; an account-wide subscription belongs to the account and is invisible to it.
Failure modes and quotas: Errors · Rate limits
Create a subscription
Subscribe an endpoint to one or more events and get its signing secret once. The secret is stored encrypted and is never shown again.
| Name | In | Type | Description |
|---|---|---|---|
| url | body | string required | An absolute https URL. Plain http is refused rather than downgraded. https://hooks.example.com/reviews |
| events | body | string[] optional | Which events to receive. Omit it, or send ["*"], for all of them. review.actioned,rule.fired |
| workspace_ids | body | string[] optional | Narrow the subscription to some locations. Ignored on a key bound to a single location, which is pinned to its own. ws_4q8n1v6z0c3r7t2y5u9i |
| secret | body | string optional | Bring your own signing secret instead of having one minted. whsec_your_own_secret_value |
Failure modes and quotas: Errors · Rate limits
Delete a subscription
Remove a subscription and stop delivering to it. A subscription this key may not see answers 404 rather than 403, so the refusal cannot confirm that somebody else's endpoint exists.
| Name | In | Type | Description |
|---|---|---|---|
| id | path | string required | The subscription id. whs_3r7t2y5u9i4q8n1v6z0c |
Failure modes and quotas: Errors · Rate limits
Send a test delivery
Send one signed test delivery to a subscription and report what came back. It is targeted at the named subscription and never fans out to the rest of the account.
| Name | In | Type | Description |
|---|---|---|---|
| id | path | string required | The subscription to send to. whs_3r7t2y5u9i4q8n1v6z0c |
Failure modes and quotas: Errors · Rate limits
Clients
Tell us about a sale you made somewhere else, and the client location, plan, allocation and invite are created for you. Needs an agency account: a key bound to a single location has no book of clients to reach and is refused.
Clients #
List clients
List the client locations on this account with their plans and allocations. Keyset paged on (created_at, id), newest first.
| Name | In | Type | Description |
|---|---|---|---|
| account_id | query | string optional | Which account to read. A key on the platform account must name one; a key on an agency account may name its own and nothing else. acc_7t2y5u9i4q8n1v6z0c3r |
| cursor | query | string optional | The next_cursor from the previous page. MjAyNi0wOS0wMVQwNzoxNTozMC4wMDBafHdzXzE |
| limit | query | integer optional | Page size, 1 to 200. Defaults to 50. 50 |
Failure modes and quotas: Errors · Rate limits
Create a client
Create a client location on a retail plan. The sale is never refused: a credit pool that cannot back the allocation answers 201 with waiting_credits rather than an error, because the money already changed hands.
| Name | In | Type | Description |
|---|---|---|---|
| account_id | body | string optional | Which account the client belongs to. A key on the platform account must name one; a key on an agency account may name its own and nothing else. acc_7t2y5u9i4q8n1v6z0c3r |
| body | string optional | The client's email. A user is created and invited when it is free, and attached when it already belongs to them. owner@haldenbakery.example | |
| name | body | string optional | The client's own name, used in the invite. Halden Bakery |
| workspace_name | body | string optional | What to call the location. Defaults to the client's name. Halden Bakery |
| plan_id | body | string optional | One of your own retail plans. Either this or a custom plan object is required. rpl_6z0c3r7t2y5u9i4q8n1v |
| plan | body | object optional | A one-off plan: price_cents, allowance, and an optional name. This is the only endpoint where a caller may state a price and a number of credits outright. [object Object] |
| external_reference | body | string optional | Your own id for the sale. It makes a retry safe beyond the 24-hour idempotency window. PK-88214 |
| processor | body | string optional | The processor you were paid through, in your own vocabulary. Defaults to api. paykickstart |
| period_end | body | ISO 8601 timestamp optional | When the period you charged for ends. 2026-10-01T07:15:30.000Z |
| retail_price_cents | body | integer optional | What you charged, in cents, when it differs from the plan's list price. 9900 |
| require_backing | body | boolean optional | Measure the credit pool first and refuse an unbacked client, for a partner who calls before charging. Defaults to false. false |
Failure modes and quotas: Errors · Rate limits
Read one client
Read one client location with its plan, allocation and usage. A client belonging to another agency answers 404, never 403.
| Name | In | Type | Description |
|---|---|---|---|
| id | path | string required | The client location id. ws_0c3r7t2y5u9i4q8n1v6z |
| account_id | query | string optional | Which account to read. A key on the platform account must name one. acc_7t2y5u9i4q8n1v6z0c3r |
Failure modes and quotas: Errors · Rate limits
Resend a client invite
Invite a person to a client location by email. Re-minting revokes every unused link, so there is exactly one working invite afterwards, and the token itself never appears in a response.
| Name | In | Type | Description |
|---|---|---|---|
| id | path | string required | The client location id. ws_0c3r7t2y5u9i4q8n1v6z |
| account_id | body | string optional | Which account the client belongs to. A key on the platform account must name one. acc_7t2y5u9i4q8n1v6z0c3r |
Failure modes and quotas: Errors · Rate limits
Cancel a client
Cancel a client location at the end of its period or right away. Nothing is cut off at once: suspension is parked at the period end plus the platform's grace, and a sweep applies it.
| Name | In | Type | Description |
|---|---|---|---|
| id | path | string required | The client location id. ws_0c3r7t2y5u9i4q8n1v6z |
| account_id | body | string optional | Which account the client belongs to. A key on the platform account must name one. acc_7t2y5u9i4q8n1v6z0c3r |
| at_period_end | body | boolean optional | Defaults to true. False means the grace runs from now rather than from the end of the paid period, not that access stops immediately. true |
| period_end | body | ISO 8601 timestamp optional | Override the period end the subscription row holds. 2026-10-01T07:15:30.000Z |
| reason | body | string optional | Kept on the activity row. Defaults to api_cancel. card_declined |
Failure modes and quotas: Errors · Rate limits
Reactivate a client
Bring a canceled client location back onto a plan. The pending suspension is cleared, the subscription goes back to active, and the plan's credits are requested again from the credit pool.
| Name | In | Type | Description |
|---|---|---|---|
| id | path | string required | The client location id. ws_0c3r7t2y5u9i4q8n1v6z |
| account_id | body | string optional | Which account the client belongs to. A key on the platform account must name one. acc_7t2y5u9i4q8n1v6z0c3r |
| period_end | body | ISO 8601 timestamp optional | The new period end to record on the subscription. 2026-11-01T07:15:30.000Z |
Failure modes and quotas: Errors · Rate limits
Set a client allocation
Set how many credits a client may burn this cycle: the allocation is their credit pool, shared by all of their locations, so naming any of their locations sets the same number. Unlike a sale, this one is refused when the account's credit pool cannot back it: nobody has paid anybody, so there is still something to say no to.
| Name | In | Type | Description |
|---|---|---|---|
| id | path | string required | The client location id. ws_0c3r7t2y5u9i4q8n1v6z |
| account_id | body | string optional | Which account the client belongs to. A key on the platform account must name one. acc_7t2y5u9i4q8n1v6z0c3r |
| credits | body | integer required | The new allocation on the client's credit pool, a whole number of credits, zero or more. It is an allocation and not a transfer: no credits move. 3000 |
Failure modes and quotas: Errors · Rate limits
Accounts and workspaces
The provisioning surface: accounts, the workspaces under them, and what each has spent. Usage reads and the workspace create reach an agency key.
Accounts #
Read account usage
Read one account's credits bought and burned this cycle, split by location, where allocated_credits and used_credits are the client's credit pool, shared by all of their locations, and used_here is what that location alone spent. A key on an agency account may read its own account and no other; group the rows by pool_workspace_id and read each pool once, because the siblings of a client repeat the same allocated_credits and used_credits and adding the rows up overcounts; allocated_credits is the monthly allocation and cycle_topup_credits any one-off extra credits given for this cycle only, so the cap this cycle is both added together.
| Name | In | Type | Description |
|---|---|---|---|
| id | path | string required | The account id. acc_7t2y5u9i4q8n1v6z0c3r |
Failure modes and quotas: Errors · Rate limits
Workspaces #
Create a workspace
Add a location to an account with an optional credit allocation. A key on the platform account says which account; a key on an agency account cannot, because there is only one it could mean.
| Name | In | Type | Description |
|---|---|---|---|
| account_id | body | string optional | Which account. Required on a key on the platform account, refused on a key on an agency account naming another. acc_7t2y5u9i4q8n1v6z0c3r |
| name | body | string required | What the location is called. Halden Bakery |
| allocated_credits | body | integer optional | The credits this location may burn this cycle. Defaults to 0. 2000 |
| retail_price_cents | body | integer optional | What the location is sold for, in cents. <code>retail_price</code> is accepted as a synonym. 9900 |
| reference | body | string optional | Your own id for the sale, which is what makes a retry return the same location. order-88215 |
Failure modes and quotas: Errors · Rate limits
Read workspace usage
Read what a single location burned this cycle and how much of the allocation is left, the allocation being the client's credit pool, shared by all of their locations, with used_here for what this location alone spent. The review and reply counts are volume figures and not a bill: reviews are free and unlimited; allocated_credits is the monthly allocation and cycle_topup_credits any one-off extra credits given for this cycle only, so the cap this cycle is both added together.
| Name | In | Type | Description |
|---|---|---|---|
| id | path | string required | The location id. ws_0c3r7t2y5u9i4q8n1v6z |
Failure modes and quotas: Errors · Rate limits
Credits
The credit pool and the allocations against it. Granting adds credits and is ours alone; allocating moves no credits at all, it sets what a location may spend.
Credits #
Allocate credits
Set a client's allocation against the account's credit pool, which moves no credits: the allocation is their credit pool, shared by all of their locations, whichever of those locations the request names. It is refused when the account's credit pool cannot back the new allocation, so the allocations can never promise more than the pool holds.
| Name | In | Type | Description |
|---|---|---|---|
| workspace_id | body | string required | Any location of the client to allocate to. Their whole credit pool is set, whichever of their locations is named. ws_0c3r7t2y5u9i4q8n1v6z |
| credits | body | integer required | The new allocation on the client's credit pool, a whole number of credits, zero or more. 3000 |
Failure modes and quotas: Errors · Rate limits
Assistants
The same surface over the Model Context Protocol, so an AI assistant can use these endpoints as tools. One stateless POST endpoint, authenticated with the very same key.
Model Context Protocol #
Model Context Protocol endpoint
Answer Model Context Protocol requests so an AI assistant can use the endpoints above as tools. Every tool dispatches to one of the paths above with the caller's own key, so the scopes, tenancy, rate limits, credits and idempotency are the same ones a direct call gets.
| Name | In | Type | Description |
|---|---|---|---|
| MCP-Protocol-Version | header | string optional | The negotiated protocol version. Absent on the first message, where initialize carries the ask instead. 2025-06-18 |
| jsonrpc | body | string required | Always the literal 2.0. 2.0 |
| id | body | string or integer optional | The request id. Omit it for a notification, which is answered 202 with no body. 1 |
| method | body | string required | initialize, tools/list, tools/call, resources/list, resources/read, prompts/list or prompts/get. tools/call |
| params | body | object optional | The method's arguments. For tools/call: a name and an arguments object. [object Object] |
Failure modes and quotas: Errors · Rate limits
Provisioning hooks
A signed endpoint a cart, a checkout or an affiliate network POSTs to when one of your clients buys, renews, fails a payment, cancels or refunds. No key: each hook has its own secret, minted on the Developers screen, and the signature is over the timestamp and the raw body.
Inbound hook #
Receive a provisioning event
Record a sale, a renewal, a failed payment, a cancellation or a refund your own gateway made, and provision, extend or suspend the client location behind it. Authenticated by an HMAC over the timestamp and the raw body rather than by an API key, so no scope applies.
| Name | In | Type | Description |
|---|---|---|---|
| hook_id | path | string required | The hook's own id, part of the URL you copied out of the Developers screen. phk_9i4q8n1v6z0c3r7t2y5u |
| X-Signature-V2 | header | string optional | sha256= followed by the hex HMAC-SHA256, under the hook's secret, of the X-Timestamp value exactly as you send it, a period, and the raw request body: HMAC(secret, timestamp + "." + body). Use this one. When it is present it alone is checked, so a wrong V2 is refused even if X-Signature-256 is right. sha256=3c9e7a1f5b20d84e6a17c0f9b2d5e83a41f6c07b9d2e58a3f1c46b0e7d92a5f8 |
| X-Signature-256 | header | string optional | The older signature: sha256= followed by the hex HMAC-SHA256 of the raw request body alone under the hook's secret. Still accepted when X-Signature-V2 is absent (an empty X-Signature-V2 header counts as present and is judged, and refused, as V2), but it does not cover the timestamp, so a captured request can be replayed with a fresh one inside the 5-minute window. Move to X-Signature-V2: once every sender has, this header is refused. sha256=8f4b1c2d0e7a63958142b0cd7e3f9a25c6b81d04e72f5a396c0b1d8e4a7f2c53 |
| X-Timestamp | header | ISO 8601 timestamp or unix seconds required | When you sent it. More than 5 minutes either side of our clock is refused, which is what stops a captured request being useful tomorrow. X-Signature-V2 signs this exact value, so it cannot be changed in transit. 2026-09-14T10:04:22.000Z |
| event | body | enum: purchase | rebill | payment_failed | cancel | refund required | What happened. rebill extends the paid period by one interval and clears a failed payment; payment_failed marks the client past due and starts the grace period before suspension. purchase |
| external_id | body | string required | Your own id for the sale, the same one on every event about it. One claim is kept per event per id for purchase, cancel and refund, so a redelivery is answered as a duplicate rather than provisioning twice. A rebill and a payment_failed happen again and again under this same id, so those 2 are claimed per event_id instead. PK-88214 |
| event_id | body | string optional | Your processor's own id for THIS renewal or THIS decline, usually the invoice or transaction id. It is what makes one month distinct from the next under the same external_id: a redelivery carrying the same event_id is answered as a duplicate, a new month with a new event_id is acted on. Leave it out on a rebill or a payment_failed and the request is refused. INV-88214-03 |
| product_id | body | string optional | Your product id, on a purchase. It is looked up in this hook's own plan map. starter-monthly |
| body | string optional | The buyer's email, on a purchase. The client user is created or attached from it. owner@haldenbakery.example | |
| name | body | string optional | The buyer's name, on a purchase. Halden Bakery |
| interval | body | enum: month | year optional | How often this sale is charged, on a purchase or a rebill. Leave it out and the sale is monthly. Send year and every rebill of it moves the paid period forward 12 months instead of 1. We read your processor's own wording: month, monthly, m and 1 all mean monthly; year, yearly, annual, annually, y and 12 all mean yearly, in any case. A word we do not recognize is treated as monthly and the sale still goes through, with the value we dropped written to your account activity so you can correct it. On a rebill it is how you move a client who has switched to annual billing: the cadence is rewritten and that renewal already runs a year. On a cancel, a refund or a payment_failed it is ignored. year |
| period_end | body | ISO 8601 timestamp optional | When the period you charged for ends, as an ISO 8601 timestamp. On a purchase and a rebill it has to be in the future. On a rebill, leave it out and we extend the stored period end by one interval of this sale: 1 month for a monthly sale, 12 months for a yearly one. 2026-10-01T07:15:30.000Z |
| amount_cents | body | integer optional | What the processor tried to take, on a payment_failed. The client's stored retail price is used when you leave it out. 7900 |
Failure modes and quotas: Errors · Rate limits

