API reference
Help centerGet an API key
API reference

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.

Base URL
https://api.reviewcomet.com
A single API key does everything. You mint it on the Developers screen in your settings and tick the scopes it carries, and from then on what it reaches follows from your account and those scopes and nothing else.

Quick start

  1. 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. 2

    Find your location and source ids

    Most calls need neither. A location is a workspace_id in 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/locations lists the locations your key covers and GET /v1/sources the review sources connected on them. Rules and saved replies take 1 workspace_id on a key that covers several.

    Where an endpoint does take a workspace_id or a source_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. 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.

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.

PlanReads a minuteWrites a minute
Every other plan60060
Agency1,200120
Whitelabel3,000300

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 conflict as 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:

CodeStatusWhat happened
unauthorized401No key, or an unknown, revoked or expired one.
forbidden403The key is real and may not do this: a missing scope, a location outside its allowlist, or a platform-only endpoint.
plan_required403The plan does not include this feature.
not_found404No such row, and also the answer for somebody else's row, so no refusal confirms a guessed id.
bad_request400A malformed cursor, an unknown tab, or a method the endpoint does not take.
validation_failed422A field value the handler will not take.
idempotency_key_required400A mutation arrived without the header.
reason_required400A credit grant arrived with no reason on it.
conflict409An idempotency key reused with a different body or still in flight, or a reply already sending.
credits_depleted409The account's credit pool is empty. Reads keep working.
allocation_depleted409The client's whole allocation for this cycle is spent.
allocation_unset409The client has no allocation yet, so none of their locations may spend.
processing_paused409Automation is paused on this account or by its plan.
capability_unsupported409The platform does not offer that action, and never a silent no-op.
pool_insufficient409The credit pool cannot back the allocation asked for.
rate_limited429This key's per-minute window is spent.
key_cap_reached429This key's monthly action cap is spent.
internal500Ours. 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 as type in 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 workspace

Assistants 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 #

The reviews feed, filtered and paged, over 1 location or several.

GET

List reviews

GET /v1/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.

Workspace keyreviews:read
NameInTypeDescription
workspace_idquerystring
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
ratingqueryinteger
optional
Only reviews with exactly this many stars, 1 to 5. 2
rating_minqueryinteger
optional
Only reviews with at least this many stars. A review with no rating never matches a bound. 1
rating_maxqueryinteger
optional
Only reviews with at most this many stars. 3
sitequeryenum: google | facebook | trustpilot | app_store | google_play | custom
optional
Only reviews from this review site. Repeat it for several. facebook
source_idquerystring
optional
Only reviews from this connected review source. ch_facebook_104928375610283
reply_statusqueryenum: none | draft | pending_approval | sending | sent | copied | failed
optional
Only reviews whose reply is in this state. Repeat it for several. none
is_private_feedbackqueryboolean
optional
True for private feedback only, false for published reviews only. Omit it for both. false
fromquerystring
optional
ISO 8601 date or timestamp. Only reviews created on or after it; a bare date covers the whole day. 2026-09-01
toquerystring
optional
ISO 8601 date or timestamp. Only reviews created on or before it. 2026-09-30
qquerystring
optional
Case-insensitive text match on the review text, its title, its translation or the author name. delivery
tabqueryenum: inbox | archive | all
optional
Which shelf to read. Defaults to all, which is the inbox and the archive. inbox
cursorquerystring
optional
The next_cursor from the previous page. MjAyNi0wOS0xMlQwOTo0MToxOC4wMDBafGNtdF8x
limitqueryinteger
optional
Page size, 1 to 200. Defaults to 50. 50
Response fields
dataarray
data[].idstring
data[].platformstring
data[].source_idstring
data[].workspace_idstring
data[].sitestring
data[].ratingnull
data[].recommendationstring
data[].titlenull
data[].post_idnull
data[].post_typenull
data[].external_idstring
data[].parent_idnull
data[].authorobject
data[].author.idstring
data[].author.namestring
data[].author.avatarnull
data[].messagestring
data[].created_timestring
data[].languagestring
data[].is_hiddenboolean
data[].statusstring
data[].sentimentstring
data[].intentnull
data[].matched_rulenull
data[].assigned_tonull
data[].reply_statusstring
data[].reply_textnull
data[].reply_updated_atnull
data[].replied_atnull
data[].replied_bynull
data[].is_private_feedbackboolean
data[].external_urlstring
data[].archivedboolean
data[].archived_atnull
data[].attachmentnull
data[].translationnull
data[].tabstring
has_moreboolean
next_cursorstring

Failure modes and quotas: Errors · Rate limits

GET

Read one review

GET /v1/reviews/:id

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.

Workspace keyreviews:read
NameInTypeDescription
idpathstring
required
The review id. cmt_1v6z0c3r7t2y5u9i4q8n
Response fields
dataobject
data.idstring
data.platformstring
data.source_idstring
data.workspace_idstring
data.sitestring
data.ratingnull
data.recommendationstring
data.titlenull
data.post_idnull
data.post_typenull
data.external_idstring
data.parent_idnull
data.authorobject
data.author.idstring
data.author.namestring
data.author.avatarnull
data.messagestring
data.created_timestring
data.languagestring
data.is_hiddenboolean
data.statusstring
data.sentimentstring
data.intentnull
data.matched_rulenull
data.assigned_tonull
data.reply_statusstring
data.reply_textnull
data.reply_updated_atnull
data.replied_atnull
data.replied_bynull
data.is_private_feedbackboolean
data.external_urlstring
data.archivedboolean
data.archived_atnull
data.attachmentnull
data.translationnull
data.tabstring

Failure modes and quotas: Errors · Rate limits

GET

Review statistics

GET /v1/reviews/stats

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.

Workspace keyinsights:read
NameInTypeDescription
workspace_idquerystring
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
daysqueryinteger
optional
The window: 7, 30 or 90 whole UTC days ending today. Other values snap to the nearest. Defaults to 30. 30
source_idquerystring
optional
Count one review source only. A source outside the set counts nothing. ch_facebook_104928375610283
Response fields
dataobject
data.windowobject
data.window.daysinteger
data.window.sincestring
data.window.untilstring
data.totalsobject
data.totals.reviewsinteger
data.totals.ratedinteger
data.totals.average_ratingnumber
data.totals.answeredinteger
data.totals.awaiting_replyinteger
data.totals.pending_approvalinteger
data.totals.private_feedbackinteger
data.breakdownsboolean
data.by_locationarray
data.by_location[].location_idstring
data.by_location[].reviewsinteger
data.by_location[].average_ratingnumber
data.by_location[].response_ratenumber
data.by_sitearray
data.by_site[].sitestring
data.by_site[].reviewsinteger
data.by_site[].average_ratingnumber
data.by_ratingarray
data.by_rating[].ratinginteger
data.by_rating[].reviewsinteger
data.by_montharray
data.by_month[].monthstring
data.by_month[].reviewsinteger
data.by_month[].ratedinteger
data.by_month[].average_ratingnumber
data.by_dayarray
data.by_day[].datestring
data.by_day[].reviewsinteger
data.by_day[].ratedinteger
data.by_day[].average_ratinginteger
data.response_ratenumber
data.median_response_minutesinteger
data.workspace_idsarray

Failure modes and quotas: Errors · Rate limits

Replies #

Answering a review: in public on a site with a reply API, by hand on any other site, or through a drafted reply waiting for approval.

POST

Reply to a review

POST /v1/reviews/:id/reply

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.

Workspace keyreviews:actIdempotent1 credit
NameInTypeDescription
idpathstring
required
The review to answer. cmt_1v6z0c3r7t2y5u9i4q8n
messagebodystring
required
What to say. Empty or whitespace only is a 422. Thank you, Dana. We are glad the delivery was quick.
Response fields
dataobject
data.kindstring
data.reply_statusstring
data.platform_syncedboolean
data.external_idstring
data.private_replynull
data.already_replied_bynull
credits_burnedinteger

Failure modes and quotas: Errors · Rate limits

POST

Mark a review as replied

POST /v1/reviews/:id/mark-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.

Workspace keyreviews:actIdempotent1 credit
NameInTypeDescription
idpathstring
required
The review that was answered. cmt_1v6z0c3r7t2y5u9i4q8n
textbodystring
required
The reply as it was posted on the site. Thank you, Dana. We are glad the delivery was quick.
Response fields
dataobject
data.idstring
data.platformstring
data.source_idstring
data.workspace_idstring
data.sitestring
data.ratingnull
data.recommendationstring
data.titlenull
data.post_idnull
data.post_typenull
data.external_idstring
data.parent_idnull
data.authorobject
data.author.idstring
data.author.namestring
data.author.avatarnull
data.messagestring
data.created_timestring
data.languagestring
data.is_hiddenboolean
data.statusstring
data.sentimentstring
data.intentnull
data.matched_rulenull
data.assigned_tonull
data.reply_statusstring
data.reply_textstring
data.reply_updated_atstring
data.replied_atstring
data.replied_bynull
data.is_private_feedbackboolean
data.external_urlstring
data.archivedboolean
data.archived_atnull
data.attachmentnull
data.translationnull
data.tabstring
credits_burnedinteger

Failure modes and quotas: Errors · Rate limits

POST

Approve a drafted reply

POST /v1/reviews/:id/approve

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.

Workspace keyreviews:actIdempotent1 credit
NameInTypeDescription
idpathstring
required
The review whose draft to approve. cmt_1v6z0c3r7t2y5u9i4q8n
reply_updated_atbodystring
required
The draft's version: reply_updated_at from the review you read. 2026-09-12T09:58:02.000Z
Response fields
dataobject
data.idstring
data.platformstring
data.source_idstring
data.workspace_idstring
data.sitestring
data.ratingnull
data.recommendationstring
data.titlenull
data.post_idnull
data.post_typenull
data.external_idstring
data.parent_idnull
data.authorobject
data.author.idstring
data.author.namestring
data.author.avatarnull
data.messagestring
data.created_timestring
data.languagestring
data.is_hiddenboolean
data.statusstring
data.sentimentstring
data.intentnull
data.matched_rulenull
data.assigned_tonull
data.reply_statusstring
data.reply_textstring
data.reply_updated_atstring
data.replied_atstring
data.replied_bynull
data.is_private_feedbackboolean
data.external_urlstring
data.archivedboolean
data.archived_atnull
data.attachmentnull
data.translationnull
data.tabstring
platform_syncedboolean
credits_burnedinteger

Failure modes and quotas: Errors · Rate limits

POST

Discard a drafted reply

POST /v1/reviews/:id/reject

Discard the reply draft on a review. Nothing is posted, and the review goes back to having no reply.

Workspace keyreviews:actIdempotent1 credit
NameInTypeDescription
idpathstring
required
The review whose draft to discard. cmt_1v6z0c3r7t2y5u9i4q8n
Response fields
dataobject
data.idstring
data.platformstring
data.source_idstring
data.workspace_idstring
data.sitestring
data.ratingnull
data.recommendationstring
data.titlenull
data.post_idnull
data.post_typenull
data.external_idstring
data.parent_idnull
data.authorobject
data.author.idstring
data.author.namestring
data.author.avatarnull
data.messagestring
data.created_timestring
data.languagestring
data.is_hiddenboolean
data.statusstring
data.sentimentstring
data.intentnull
data.matched_rulenull
data.assigned_tonull
data.reply_statusstring
data.reply_textnull
data.reply_updated_atnull
data.replied_atnull
data.replied_bynull
data.is_private_feedbackboolean
data.external_urlstring
data.archivedboolean
data.archived_atnull
data.attachmentnull
data.translationnull
data.tabstring
platform_syncedboolean
credits_burnedinteger

Failure modes and quotas: Errors · Rate limits

POST

Draft a reply with AI

POST /v1/reviews/:id/draft

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.

Workspace keyreviews:actIdempotent
NameInTypeDescription
idpathstring
required
The review to draft a reply to. cmt_1v6z0c3r7t2y5u9i4q8n
Response fields
dataobject
data.review_idstring
data.textstring
credits_burnedinteger

Failure modes and quotas: Errors · Rate limits

Actions #

Housekeeping on a review. None of it changes anything on the review site.

POST

Act on a review

POST /v1/reviews/:id/actions

Archive, unarchive, assign, flag or unflag a single review. The charge lands only after the action succeeded.

Workspace keyreviews:actIdempotent1 credit
NameInTypeDescription
idpathstring
required
The review to act on. cmt_1v6z0c3r7t2y5u9i4q8n
actionbodyenum: archive | unarchive | assign | flag | unflag
required
What to do. Anything else is a 422. assign
assigned_tobodystring
optional
The user id to assign to, for the assign action. Null clears the assignment. usr_8n1v6z0c3r7t2y5u9i4q
reasonbodystring
optional
A note kept on the activity row, for the flag action and for your own audit trail. Mentions a refund
Response fields
dataobject
data.idstring
data.platformstring
data.source_idstring
data.workspace_idstring
data.sitestring
data.ratingnull
data.recommendationstring
data.titlenull
data.post_idnull
data.post_typenull
data.external_idstring
data.parent_idnull
data.authorobject
data.author.idstring
data.author.namestring
data.author.avatarnull
data.messagestring
data.created_timestring
data.languagestring
data.is_hiddenboolean
data.statusstring
data.sentimentstring
data.intentnull
data.matched_rulenull
data.assigned_tostring
data.reply_statusstring
data.reply_textnull
data.reply_updated_atnull
data.replied_atnull
data.replied_bynull
data.is_private_feedbackboolean
data.external_urlstring
data.archivedboolean
data.archived_atnull
data.attachmentnull
data.translationnull
data.tabstring
credits_burnedinteger

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 #

A location is what the app's location switcher lists. Its id is the workspace_id every endpoint takes.

GET

List locations

GET /v1/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.

Workspace keysources:readreviews:read
NameInTypeDescription
account_idquerystring
optional
Platform keys only: the account whose locations to list. A customer key reads its own account and may omit it. acc_7t2y5u9i4q8n1v6z0c3r
cursorquerystring
optional
The next_cursor from the previous page. The list is oldest first on (created_at, id). MjAyNi0wNi0wMlQwODo0NTowMC4wMDBafGNoXzE
limitqueryinteger
optional
Page size, 1 to 200. Defaults to 50. 50
Response fields
dataarray
data[].idstring
data[].namestring
data[].statusstring
data[].source_countinteger
data[].created_atstring
has_moreboolean
next_cursornull

Failure modes and quotas: Errors · Rate limits

Sources #

The review sources a location has connected, and what the API can do on each.

GET

List review sources

GET /v1/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.

Workspace keysources:read
NameInTypeDescription
workspace_idquerystring
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
cursorquerystring
optional
The next_cursor from the previous page. The list is oldest first on (created_at, id). MjAyNi0wNi0wMlQwODo0NTowMC4wMDBafGNoXzE
limitqueryinteger
optional
Page size, 1 to 200. Defaults to 50. 50
Response fields
dataarray
data[].idstring
data[].workspace_idstring
data[].sitestring
data[].site_namestring
data[].namestring
data[].handlestring
data[].statusstring
data[].can_replyboolean
data[].last_syncstring
data[].connected_atstring
has_moreboolean
next_cursornull

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 #

Create, read, change, delete and switch rules.

GET

List rules

GET /v1/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.

Workspace keyrules:readrules:write
NameInTypeDescription
workspace_idquerystring
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
cursorquerystring
optional
The next_cursor from the previous page. MjAyNi0wOC0wNFQxMTowMjo0NC4wMDBafHJ1bGVfMQ
limitqueryinteger
optional
Page size, 1 to 200. Defaults to 50. 50
Response fields
dataarray
data[].idstring
data[].workspace_idstring
data[].namestring
data[].enabledboolean
data[].trial_modeboolean
data[].definitionobject
data[].definition.triggerobject
data[].definition.trigger.typestring
data[].definition.trigger.keywordsarray
data[].definition.trigger.similar_countinteger
data[].definition.trigger.within_minutesinteger
data[].definition.actionsarray
data[].definition.actions[].idstring
data[].definition.actions[].typestring
data[].definition.delayobject
data[].definition.delay.modestring
data[].definition.delay.minutesinteger
data[].definition.scopeobject
data[].definition.scope.channel_idsarray
data[].definition.scope.placementstring
data[].definition.scope.post_idsarray
data[].created_atstring
data[].updated_atstring
data[].readableboolean
data[].positioninteger
has_moreboolean
next_cursornull

Failure modes and quotas: Errors · Rate limits

POST

Create a rule

POST /v1/rules

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.

Workspace keyrules:writeIdempotent
NameInTypeDescription
workspace_idbodystring
optional
Which location the rule belongs to. Required on a key that reaches more than one. ws_4q8n1v6z0c3r7t2y5u9i
namebodystring
required
What the rule is called in the app and in the activity trail. Flag refund requests
definitionbodyobject
required
The rule itself: a trigger, a list of actions, an optional delay and an optional scope. [object Object]
definition.trigger.typebodyenum: 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[].typebodyenum: 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.modebodyenum: none | fixed | random
optional
Wait before acting. Defaults to none. none
definition.scope.placementbodyenum: posts | ads | both
optional
Which placements the rule watches. Defaults to both. both
enabledbodyboolean
optional
Start the rule switched on. Defaults to true. true
trial_modebodyboolean
optional
Record what the rule would have done without doing it. Defaults to false. false
Response fields
dataobject
data.idstring
data.workspace_idstring
data.namestring
data.enabledboolean
data.trial_modeboolean
data.definitionobject
data.definition.triggerobject
data.definition.trigger.typestring
data.definition.trigger.keywordsarray
data.definition.trigger.similar_countinteger
data.definition.trigger.within_minutesinteger
data.definition.actionsarray
data.definition.actions[].idstring
data.definition.actions[].typestring
data.definition.delayobject
data.definition.delay.modestring
data.definition.delay.minutesinteger
data.definition.scopeobject
data.definition.scope.channel_idsarray
data.definition.scope.placementstring
data.definition.scope.post_idsarray
data.created_atstring
data.updated_atstring
data.readableboolean
data.positioninteger

Failure modes and quotas: Errors · Rate limits

GET

Read one rule

GET /v1/rules/:id

Read one automation rule with its conditions and actions. readable is false when the stored definition is a shape the engine cannot run.

Workspace keyrules:readrules:write
NameInTypeDescription
idpathstring
required
The rule id. rule_9i4q8n1v6z0c3r7t2y5u
Response fields
dataobject
data.idstring
data.workspace_idstring
data.namestring
data.enabledboolean
data.trial_modeboolean
data.definitionobject
data.definition.triggerobject
data.definition.trigger.typestring
data.definition.trigger.keywordsarray
data.definition.trigger.similar_countinteger
data.definition.trigger.within_minutesinteger
data.definition.actionsarray
data.definition.actions[].idstring
data.definition.actions[].typestring
data.definition.delayobject
data.definition.delay.modestring
data.definition.delay.minutesinteger
data.definition.scopeobject
data.definition.scope.channel_idsarray
data.definition.scope.placementstring
data.definition.scope.post_idsarray
data.created_atstring
data.updated_atstring
data.readableboolean
data.positioninteger

Failure modes and quotas: Errors · Rate limits

PATCH

Change a rule

PATCH /v1/rules/:id

Change an automation rule. Every field is optional and anything omitted keeps the value it had.

Workspace keyrules:writeIdempotent
NameInTypeDescription
idpathstring
required
The rule id. rule_9i4q8n1v6z0c3r7t2y5u
namebodystring
optional
A new name. Flag refund and chargeback requests
definitionbodyobject
optional
A whole new definition. It replaces the old one and is validated the same way a create is. [object Object]
enabledbodyboolean
optional
Switch the rule on or off. true
trial_modebodyboolean
optional
Switch recording-only mode on or off. false
Response fields
dataobject
data.idstring
data.workspace_idstring
data.namestring
data.enabledboolean
data.trial_modeboolean
data.definitionobject
data.definition.triggerobject
data.definition.trigger.typestring
data.definition.trigger.keywordsarray
data.definition.trigger.similar_countinteger
data.definition.trigger.within_minutesinteger
data.definition.actionsarray
data.definition.actions[].idstring
data.definition.actions[].typestring
data.definition.delayobject
data.definition.delay.modestring
data.definition.delay.minutesinteger
data.definition.scopeobject
data.definition.scope.channel_idsarray
data.definition.scope.placementstring
data.definition.scope.post_idsarray
data.created_atstring
data.updated_atstring
data.readableboolean
data.positioninteger

Failure modes and quotas: Errors · Rate limits

DELETE

Delete a rule

DELETE /v1/rules/:id

Delete an automation rule. The rule stops firing immediately and is not recoverable.

Workspace keyrules:writeIdempotent
NameInTypeDescription
idpathstring
required
The rule id. rule_9i4q8n1v6z0c3r7t2y5u
Response fields
dataobject
data.idstring
data.deletedboolean

Failure modes and quotas: Errors · Rate limits

POST

Enable a rule

POST /v1/rules/:id/enable

Turn an automation rule on. The body is ignored; the Idempotency-Key header is not.

Workspace keyrules:writeIdempotent
NameInTypeDescription
idpathstring
required
The rule id. rule_9i4q8n1v6z0c3r7t2y5u
Response fields
dataobject
data.idstring
data.workspace_idstring
data.namestring
data.enabledboolean
data.trial_modeboolean
data.definitionobject
data.definition.triggerobject
data.definition.trigger.typestring
data.definition.trigger.keywordsarray
data.definition.trigger.similar_countinteger
data.definition.trigger.within_minutesinteger
data.definition.actionsarray
data.definition.actions[].idstring
data.definition.actions[].typestring
data.definition.delayobject
data.definition.delay.modestring
data.definition.delay.minutesinteger
data.definition.scopeobject
data.definition.scope.channel_idsarray
data.definition.scope.placementstring
data.definition.scope.post_idsarray
data.created_atstring
data.updated_atstring
data.readableboolean
data.positioninteger

Failure modes and quotas: Errors · Rate limits

POST

Disable a rule

POST /v1/rules/:id/disable

Turn an automation rule off without deleting it. The body is ignored; the Idempotency-Key header is not.

Workspace keyrules:writeIdempotent
NameInTypeDescription
idpathstring
required
The rule id. rule_9i4q8n1v6z0c3r7t2y5u
Response fields
dataobject
data.idstring
data.workspace_idstring
data.namestring
data.enabledboolean
data.trial_modeboolean
data.definitionobject
data.definition.triggerobject
data.definition.trigger.typestring
data.definition.trigger.keywordsarray
data.definition.trigger.similar_countinteger
data.definition.trigger.within_minutesinteger
data.definition.actionsarray
data.definition.actions[].idstring
data.definition.actions[].typestring
data.definition.delayobject
data.definition.delay.modestring
data.definition.delay.minutesinteger
data.definition.scopeobject
data.definition.scope.channel_idsarray
data.definition.scope.placementstring
data.definition.scope.post_idsarray
data.created_atstring
data.updated_atstring
data.readableboolean
data.positioninteger

Failure modes and quotas: Errors · Rate limits

Backtest #

What a rule would have done, over the location's own recent history.

POST

Backtest a rule

POST /v1/rules/backtest

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.

Workspace keyrules:readrules:writeIdempotent
NameInTypeDescription
workspace_idbodystring
optional
Which location's history to scan. Required on a key that reaches more than one. ws_4q8n1v6z0c3r7t2y5u9i
triggerbodyobject
required
The trigger to test, validated exactly as a create validates it. [object Object]
scopebodyobject
optional
Narrow the test to some review sources. [object Object]
Response fields
dataobject
data.totalinteger
data.per_weekarray
data.samplesarray
data.samples[].idstring
data.samples[].platformstring
data.samples[].source_idstring
data.samples[].workspace_idstring
data.samples[].sitestring
data.samples[].ratingnull
data.samples[].recommendationstring
data.samples[].titlenull
data.samples[].post_idnull
data.samples[].post_typenull
data.samples[].external_idstring
data.samples[].parent_idnull
data.samples[].authorobject
data.samples[].author.idstring
data.samples[].author.namestring
data.samples[].author.avatarnull
data.samples[].messagestring
data.samples[].created_timestring
data.samples[].languagestring
data.samples[].is_hiddenboolean
data.samples[].statusstring
data.samples[].sentimentstring
data.samples[].intentnull
data.samples[].matched_rulenull
data.samples[].assigned_tonull
data.samples[].reply_statusstring
data.samples[].reply_textnull
data.samples[].reply_updated_atnull
data.samples[].replied_atnull
data.samples[].replied_bynull
data.samples[].is_private_feedbackboolean
data.samples[].external_urlstring
data.samples[].archivedboolean
data.samples[].archived_atnull
data.samples[].attachmentnull
data.samples[].translationnull
data.samples[].tabstring
data.by_channelarray
data.by_channel[].channel_idstring
data.by_channel[].countinteger
data.scannedinteger
data.cappedboolean

Failure modes and quotas: Errors · Rate limits

Saved replies

The saved reply library a location answers reviews with. A free read.


Saved replies #

The canned answers a location keeps.

GET

List saved replies

GET /v1/saved-replies

List the saved replies a location can answer with, most recently edited first. There is no plan gate on this one.

Workspace keyreplies:readreplies:write
NameInTypeDescription
workspace_idquerystring
optional
Which location to read. Required on a key that reaches more than one. ws_4q8n1v6z0c3r7t2y5u9i
cursorquerystring
optional
The next_cursor from the previous page. MjAyNi0wOS0xMFQxNToyMDowOS4wMDBafHNycF8x
limitqueryinteger
optional
Page size, 1 to 200. Defaults to 50. 50
Response fields
dataarray
data[].idstring
data[].workspace_idstring
data[].namestring
data[].bodystring
data[].tagsarray
data[].channel_idsarray
data[].deliverystring
data[].created_bystring
data[].created_by_namestring
data[].created_atstring
data[].updated_atstring
has_moreboolean
next_cursornull

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 #

Credits granted, burned and left.

GET

Read usage

GET /v1/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.

Workspace keyusage:readread
Response fields
dataobject
data.account_idstring
data.planstring
data.cycle_startstring
data.cycle_endstring
data.credits_allowanceinteger
data.credits_burnedinteger
data.credits_remaininginteger
data.automation_pausedboolean
data.workspacesarray
data.workspaces[].idstring
data.workspaces[].namestring
data.workspaces[].allocated_creditsinteger
data.workspaces[].cycle_topup_creditsinteger
data.workspaces[].used_creditsinteger
data.workspaces[].used_hereinteger
data.workspaces[].pool_workspace_idstring
data.workspaces[].cap_modestring
data.workspaces[].automation_pausedboolean
data.workspaces[].paused_reasonnull

Failure modes and quotas: Errors · Rate limits

Events #

The activity trail, pulled rather than pushed.

GET

List events

GET /v1/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.

Workspace keyevents:readread
NameInTypeDescription
workspace_idquerystring
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
typequerystring
optional
Only events of this kind, matched on the trail's own verb. reply_sent
sincequeryISO 8601 timestamp
optional
Only events at or after this moment. 2026-09-01T00:00:00.000Z
cursorquerystring
optional
The next_cursor from the previous page. MjAyNi0wOS0xM1QxNjowMjoxMS4wMDBafGV2dF8x
limitqueryinteger
optional
Page size, 1 to 200. Defaults to 50. 50
Response fields
dataarray
data[].idstring
data[].typestring
data[].account_idstring
data[].workspace_idstring
data[].actorobject
data[].actor.kindstring
data[].actor.namestring
data[].objectstring
data[].detailobject
data[].detail.platformstring
data[].detail.channel_idstring
data[].detail.viastring
data[].created_atstring
has_moreboolean
next_cursorstring

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 #

Endpoints this account delivers to.

GET

List subscriptions

GET /v1/webhooks

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.

Workspace keywebhooks:write
Response fields
dataarray
data[].idstring
data[].urlstring
data[].eventsarray
data[].workspace_idsarray
data[].statusstring
data[].secret_hintstring
data[].failing_sincenull
data[].last_success_atstring
data[].created_atstring
data[].previous_secret_expires_atnull
has_moreboolean
next_cursornull

Failure modes and quotas: Errors · Rate limits

POST

Create a subscription

POST /v1/webhooks

Subscribe an endpoint to one or more events and get its signing secret once. The secret is stored encrypted and is never shown again.

Workspace keywebhooks:writeIdempotent
NameInTypeDescription
urlbodystring
required
An absolute https URL. Plain http is refused rather than downgraded. https://hooks.example.com/reviews
eventsbodystring[]
optional
Which events to receive. Omit it, or send ["*"], for all of them. review.actioned,rule.fired
workspace_idsbodystring[]
optional
Narrow the subscription to some locations. Ignored on a key bound to a single location, which is pinned to its own. ws_4q8n1v6z0c3r7t2y5u9i
secretbodystring
optional
Bring your own signing secret instead of having one minted. whsec_your_own_secret_value
Response fields
dataobject
data.idstring
data.created_atstring
secretstring
warningstring

Failure modes and quotas: Errors · Rate limits

DELETE

Delete a subscription

DELETE /v1/webhooks/:id

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.

Workspace keywebhooks:writeIdempotent
NameInTypeDescription
idpathstring
required
The subscription id. whs_3r7t2y5u9i4q8n1v6z0c
Response fields
dataobject
data.idstring
data.deletedboolean

Failure modes and quotas: Errors · Rate limits

POST

Send a test delivery

POST /v1/webhooks/:id/test

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.

Workspace keywebhooks:writeIdempotent
NameInTypeDescription
idpathstring
required
The subscription to send to. whs_3r7t2y5u9i4q8n1v6z0c
Response fields
dataobject
data.queuedinteger

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 #

A book of client locations on one account.

GET

List clients

GET /v1/clients

List the client locations on this account with their plans and allocations. Keyset paged on (created_at, id), newest first.

Agency keyclients:read
NameInTypeDescription
account_idquerystring
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
cursorquerystring
optional
The next_cursor from the previous page. MjAyNi0wOS0wMVQwNzoxNTozMC4wMDBafHdzXzE
limitqueryinteger
optional
Page size, 1 to 200. Defaults to 50. 50
Response fields
dataarray
data[].idstring
data[].namestring
data[].statusstring
data[].created_atstring
data[].provisioned_viastring
data[].planobject
data[].plan.idstring
data[].plan.namestring
data[].plan.price_centsinteger
data[].plan.allowanceinteger
data[].allocationobject
data[].allocation.grantedinteger
data[].allocation.pendinginteger
data[].allocation.statusstring
data[].userobject
data[].user.idstring
data[].user.emailstring
data[].user.attachedstring
data[].inviteobject
data[].invite.holdnull
data[].subscriptionobject
data[].subscription.statusstring
data[].subscription.processorstring
data[].subscription.external_referencestring
data[].subscription.current_period_endstring
data[].subscription.canceled_atnull
data[].suspend_atnull
has_moreboolean
next_cursornull

Failure modes and quotas: Errors · Rate limits

POST

Create a client

POST /v1/clients

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.

Agency keyclients:writeIdempotent
NameInTypeDescription
account_idbodystring
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
emailbodystring
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
namebodystring
optional
The client's own name, used in the invite. Halden Bakery
workspace_namebodystring
optional
What to call the location. Defaults to the client's name. Halden Bakery
plan_idbodystring
optional
One of your own retail plans. Either this or a custom plan object is required. rpl_6z0c3r7t2y5u9i4q8n1v
planbodyobject
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_referencebodystring
optional
Your own id for the sale. It makes a retry safe beyond the 24-hour idempotency window. PK-88214
processorbodystring
optional
The processor you were paid through, in your own vocabulary. Defaults to api. paykickstart
period_endbodyISO 8601 timestamp
optional
When the period you charged for ends. 2026-10-01T07:15:30.000Z
retail_price_centsbodyinteger
optional
What you charged, in cents, when it differs from the plan's list price. 9900
require_backingbodyboolean
optional
Measure the credit pool first and refuse an unbacked client, for a partner who calls before charging. Defaults to false. false
Response fields
idstring
createdboolean
statusstring
planobject
plan.idstring
plan.namestring
plan.price_centsinteger
plan.allowanceinteger
allocationobject
allocation.requestedinteger
allocation.grantedinteger
allocation.statusstring
allocation.short_byinteger
userobject
user.idstring
user.emailstring
user.attachedstring
inviteobject
invite.statusstring
invite.expires_atnull

Failure modes and quotas: Errors · Rate limits

GET

Read one client

GET /v1/clients/:id

Read one client location with its plan, allocation and usage. A client belonging to another agency answers 404, never 403.

Agency keyclients:read
NameInTypeDescription
idpathstring
required
The client location id. ws_0c3r7t2y5u9i4q8n1v6z
account_idquerystring
optional
Which account to read. A key on the platform account must name one. acc_7t2y5u9i4q8n1v6z0c3r
Response fields
idstring
namestring
statusstring
created_atstring
provisioned_viastring
planobject
plan.idstring
plan.namestring
plan.price_centsinteger
plan.allowanceinteger
allocationobject
allocation.grantedinteger
allocation.pendinginteger
allocation.statusstring
userobject
user.idstring
user.emailstring
user.attachedstring
inviteobject
invite.holdnull
subscriptionobject
subscription.statusstring
subscription.processorstring
subscription.external_referencestring
subscription.current_period_endstring
subscription.canceled_atnull
suspend_atnull

Failure modes and quotas: Errors · Rate limits

POST

Resend a client invite

POST /v1/clients/:id/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.

Agency keyclients:writeIdempotent
NameInTypeDescription
idpathstring
required
The client location id. ws_0c3r7t2y5u9i4q8n1v6z
account_idbodystring
optional
Which account the client belongs to. A key on the platform account must name one. acc_7t2y5u9i4q8n1v6z0c3r
Response fields
sentboolean
expires_atstring
hoststring

Failure modes and quotas: Errors · Rate limits

POST

Cancel a client

POST /v1/clients/:id/cancel

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.

Agency keyclients:writeIdempotent
NameInTypeDescription
idpathstring
required
The client location id. ws_0c3r7t2y5u9i4q8n1v6z
account_idbodystring
optional
Which account the client belongs to. A key on the platform account must name one. acc_7t2y5u9i4q8n1v6z0c3r
at_period_endbodyboolean
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_endbodyISO 8601 timestamp
optional
Override the period end the subscription row holds. 2026-10-01T07:15:30.000Z
reasonbodystring
optional
Kept on the activity row. Defaults to api_cancel. card_declined
Response fields
idstring
statusstring
suspend_atstring
period_endstring

Failure modes and quotas: Errors · Rate limits

POST

Reactivate a client

POST /v1/clients/:id/reactivate

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.

Agency keyclients:writeIdempotent
NameInTypeDescription
idpathstring
required
The client location id. ws_0c3r7t2y5u9i4q8n1v6z
account_idbodystring
optional
Which account the client belongs to. A key on the platform account must name one. acc_7t2y5u9i4q8n1v6z0c3r
period_endbodyISO 8601 timestamp
optional
The new period end to record on the subscription. 2026-11-01T07:15:30.000Z
Response fields
idstring
statusstring
suspend_atnull
allocationobject
allocation.grantedinteger
allocation.pendinginteger

Failure modes and quotas: Errors · Rate limits

POST

Set a client allocation

POST /v1/clients/:id/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.

Agency keyclients:writecredits:allocateIdempotent
NameInTypeDescription
idpathstring
required
The client location id. ws_0c3r7t2y5u9i4q8n1v6z
account_idbodystring
optional
Which account the client belongs to. A key on the platform account must name one. acc_7t2y5u9i4q8n1v6z0c3r
creditsbodyinteger
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
Response fields
workspace_idstring
allocated_creditsinteger
total_allocatedinteger
pool_balanceinteger
pool_workspace_idstring
zeroed_workspace_idsarray

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 #

Top-level tenants.

GET

Read account usage

GET /v1/accounts/:id/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.

Agency keyread
NameInTypeDescription
idpathstring
required
The account id. acc_7t2y5u9i4q8n1v6z0c3r
Response fields
account_idstring
planstring
cycle_startstring
cycle_endstring
credits_allowanceinteger
credits_purchasedinteger
credits_burnedinteger
credits_remaininginteger
ai_pausedboolean
workspacesarray
workspaces[].idstring
workspaces[].namestring
workspaces[].allocated_creditsinteger
workspaces[].cycle_topup_creditsinteger
workspaces[].used_creditsinteger
workspaces[].used_hereinteger
workspaces[].pool_workspace_idstring

Failure modes and quotas: Errors · Rate limits

Workspaces #

The locations under an account.

POST

Create a workspace

POST /v1/workspaces

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.

Agency keyworkspaces.write
NameInTypeDescription
account_idbodystring
optional
Which account. Required on a key on the platform account, refused on a key on an agency account naming another. acc_7t2y5u9i4q8n1v6z0c3r
namebodystring
required
What the location is called. Halden Bakery
allocated_creditsbodyinteger
optional
The credits this location may burn this cycle. Defaults to 0. 2000
retail_price_centsbodyinteger
optional
What the location is sold for, in cents. <code>retail_price</code> is accepted as a synonym. 9900
referencebodystring
optional
Your own id for the sale, which is what makes a retry return the same location. order-88215
Response fields
idstring
account_idstring
namestring
statusstring
allocated_creditsinteger
used_creditsinteger
createdboolean

Failure modes and quotas: Errors · Rate limits

GET

Read workspace usage

GET /v1/workspaces/:id/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.

Agency keyread
NameInTypeDescription
idpathstring
required
The location id. ws_0c3r7t2y5u9i4q8n1v6z
Response fields
workspace_idstring
namestring
statusstring
cycle_startstring
cycle_endstring
allocated_creditsinteger
cycle_topup_creditsinteger
used_creditsinteger
remaining_creditsinteger
used_hereinteger
pool_workspace_idstring
reviewsinteger
repliesinteger

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 #

Grants and allocations.

POST

Allocate credits

POST /v1/credits/allocate

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.

Agency keycredits.allocatecredits.write
NameInTypeDescription
workspace_idbodystring
required
Any location of the client to allocate to. Their whole credit pool is set, whichever of their locations is named. ws_0c3r7t2y5u9i4q8n1v6z
creditsbodyinteger
required
The new allocation on the client's credit pool, a whole number of credits, zero or more. 3000
Response fields
workspace_idstring
allocated_creditsinteger
total_allocatedinteger
pool_balanceinteger
pool_workspace_idstring
zeroed_workspace_idsarray

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 #

JSON-RPC over one HTTP endpoint.

POST

Model Context Protocol endpoint

POST /mcp

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.

Workspace keyread
NameInTypeDescription
MCP-Protocol-Versionheaderstring
optional
The negotiated protocol version. Absent on the first message, where initialize carries the ask instead. 2025-06-18
jsonrpcbodystring
required
Always the literal 2.0. 2.0
idbodystring or integer
optional
The request id. Omit it for a notification, which is answered 202 with no body. 1
methodbodystring
required
initialize, tools/list, tools/call, resources/list, resources/read, prompts/list or prompts/get. tools/call
paramsbodyobject
optional
The method's arguments. For tools/call: a name and an arguments object. [object Object]
Response fields
jsonrpcstring
idinteger
resultobject
result.contentarray
result.content[].typestring
result.content[].textstring

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 #

One endpoint per hook, with a product-to-plan map behind it.

POST

Receive a provisioning event

POST /webhooks/provisioning/:hook_id

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.

Agency key
NameInTypeDescription
hook_idpathstring
required
The hook's own id, part of the URL you copied out of the Developers screen. phk_9i4q8n1v6z0c3r7t2y5u
X-Signature-V2headerstring
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-256headerstring
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-TimestampheaderISO 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
eventbodyenum: 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_idbodystring
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_idbodystring
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_idbodystring
optional
Your product id, on a purchase. It is looked up in this hook's own plan map. starter-monthly
emailbodystring
optional
The buyer's email, on a purchase. The client user is created or attached from it. owner@haldenbakery.example
namebodystring
optional
The buyer's name, on a purchase. Halden Bakery
intervalbodyenum: 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_endbodyISO 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_centsbodyinteger
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
Response fields
receivedboolean
workspace_idstring
createdboolean
statusstring

Failure modes and quotas: Errors · Rate limits

ReviewComet API reference. Select an endpoint to see code samples and try it in the console.