# Prisma Reviews · full documentation > Prisma Reviews is a unified reviews API: one REST API to read and reply to the reviews of the businesses and apps you own, across Google Business Profile, App Store, Google Play, Judge.me, Trustpilot, Yelp, Tripadvisor, Booking.com, Yotpo, REVIEWS.io and G2. > Prisma changelog: new platforms, new endpoints and changes in the Google, Trustpilot, App Store and Google Play APIs that affect your integration. # Changelog Every change to the Prisma reviews API, newest first: new platforms, new endpoints, and changes in the Google, Trustpilot, App Store and Google Play APIs that affect your integration. Within `v1` changes are additive (new endpoints, optional parameters, response fields and event types), so write your client to ignore fields it doesn't know. The one exception so far is the 2026-10-26 entry, which removed reply drafts before they had customers; it says what to change.

2026-10-26 · Changed: Replies by API on 5 platforms, read only on 6

Prisma connects APIs: it reads reviews and publishes the reply text you send (written by you, your software or your own model) through the platform's official API. It doesn't write replies or keep unpublished ones. - **Replies by API** on Google Business Profile, App Store, Google Play, Judge.me and Trustpilot (beta). `POST /v1/reviews/{id}/reply` returns `202` with `reply.status: "queued"`, then `published` (or `failed`). - **Read only** on Yelp, Tripadvisor, Booking.com, Yotpo, REVIEWS.io and G2 (beta, Beam and up). These platforms have no reply API for third parties. Prisma reads their reviews, and each review's `url` links to where you answer it on the platform (for some platforms, the listing or product page). - **New error `422 reply_not_supported`** for `POST` and `DELETE /v1/reviews/{id}/reply` on read-only platforms, with the extra fields `platform` and `url`. See [Errors](/docs/errors#reply_not_supported). - **Reply statuses** are now `published`, `queued` and `failed`. There is no `draft` status. - **`GET /v1/usage`** no longer returns the `ai_drafts` field. Reply drafts, listed as not available yet in v1, won't ship, and plans don't include them. **Action needed:** if you sent replies for Yelp, Tripadvisor, Booking.com, Yotpo, REVIEWS.io or G2 reviews, handle `422 reply_not_supported` and send users to the review's `url`. Stop reading `ai_drafts` from `/v1/usage`. Details on [Replies](/docs/api-reference/replies) and [Platforms](/docs/api-reference/platforms).

2026-10-23 · New: Accounts and dashboard

Early access is over: anyone can [create an account](https://app.prismareviews.com/signup) and start building. - **Sign up** with your email and a password (we send a link to confirm your email), or with Google, GitHub or Apple. Then [sign in](https://app.prismareviews.com/login) at any time. - **Your workspace** starts on the free Spark plan, with a live key (`pk_live_…`) and a test key (`pk_test_…`). The [dashboard](https://app.prismareviews.com) shows both in full once; after that, only their prefix. The test key works right away with sample data. - **Roll a key** from the dashboard to replace it: the new key is shown once and the old one stops working at once. This is also how you revoke a key you think was exposed. - **The dashboard** shows your plan, your keys, your usage and your connections. - **Paid plans** can't be bought from the dashboard yet. To move to Beam, Spectrum or Aurora, email hello@prismareviews.com. **Action needed:** none. Start with the [Quickstart](/docs/quickstart) once your keys are in the dashboard.

2026-10-23 · New: Prisma API v1

The first version of the API is available at `https://api.prismareviews.com/v1` for every workspace, with live keys (`pk_live_…`) and test keys (`pk_test_…`). Test keys work with every platform on sample data and never touch a real account. **Platforms.** Eleven platforms, each with a live connector: - Replies by the platform's official API: Google Business Profile (OAuth), App Store (App Store Connect API key), Google Play (Prisma's service account invited in Play Console), Judge.me (shop domain and private token) and Trustpilot (OAuth, beta). - Replies as drafts the owner publishes: Yelp, Tripadvisor and Booking.com (public listing URL), Yotpo, REVIEWS.io and G2 (API credentials). All in beta. How each one connects, with the exact fields, is on [Platforms](/docs/api-reference/platforms). **Endpoints.** - Reviews: `GET /v1/reviews` (filters `location_id`, `platform`, `since`, `has_reply`; cursor pagination, up to 100 per page) and `GET /v1/reviews/{id}`. - Replies: `POST /v1/reviews/{id}/reply` (create or replace, with `Idempotency-Key`) and `DELETE /v1/reviews/{id}/reply`. - Locations: `GET /v1/locations`, `GET /v1/locations/{id}`. - Connections: `POST /v1/connections` (OAuth, credentials or listing URL), `GET /v1/connections`, `DELETE /v1/connections/{id}`. - Webhooks: `POST /v1/webhooks`, `GET /v1/webhooks`, `DELETE /v1/webhooks/{id}`, with the events `review.created`, `review.updated`, `reply.published`, `reply.failed` and `connection.updated`, signed with `Prisma-Signature` (HMAC-SHA256). - Usage: `GET /v1/usage`. **Limits you should know about.** - Reply length is checked before queueing: Google Business Profile 4,096 bytes, App Store 5,970 characters, Google Play 350 characters (`422 reply_too_long`). - Google Play and Judge.me can't delete replies through their APIs (`422 operation_not_supported`), and Judge.me can't edit them either. - The Google Play API only returns reviews from the last 7 days; Prisma keeps the history from the day you connect. See [Google Play reply-to-reviews documentation](https://developers.google.com/android-publisher/reply-to-reviews). - Google limits edits per Business Profile (see [Google Business Profile API usage limits](https://developers.google.com/my-business/content/limits)). Replies are queued and retried. - Per-key rate limits by plan: Spark 60, Beam 120, Spectrum 600 and Aurora 1,200 requests per minute. **Not available yet:** SDKs, an OpenAPI spec and AI reply drafts. They will appear here when they ship. **Action needed:** none. Start with the [Quickstart](/docs/quickstart), then the [API reference](/docs/api-reference) and [Errors](/docs/errors). Platform guides: [Google Business Profile API](/google-business-profile-api), [Google Play reviews API](/google-play-reviews-api), [Yelp](/yelp-reviews-api), [Tripadvisor](/tripadvisor-reviews-api). --- > Prisma API docs: quickstart, core concepts and supported platforms for reading and replying to Google, App Store, Google Play, Trustpilot, Yelp and Tripadvisor reviews. ## Choose your path
Quickstart 5 minGet a key, connect a location, list reviews and send your first reply in curl, Node and Python. Test keys No setuppk_test_ keys return sample reviews on every platform, so you can build before connecting a real one. Connect a platformOAuth for Google and Trustpilot, API credentials for the app stores and shops, a public URL for Yelp, Tripadvisor and Booking.com. WebhooksSigned review.created, review.updated and reply.published events, so you never poll.
## Everything the API covers
Reviews GETEvery review in one shape: rating, title, text, author, language, reply and url. Replies POST · DELETEPublish your reply through the platform's official API on Google, App Store, Google Play, Judge.me and Trustpilot. Locations GETThe businesses, apps and listings you connected, with their sync status. Usage GETYour plan, connected locations, requests and replies this month. Platforms 11How each platform connects, what it allows and how often Prisma checks it. ErrorsEvery error code with its meaning and what to do, plus retry advice.
More: [Integrations](/integrations) · [Pricing](/pricing) · [Security](/security) · [Changelog](/changelog) · [llms.txt](/llms.txt) · [llms-full.txt](/llms-full.txt) The endpoints you'll use most are `/connections`, `/locations`, `/reviews`, `/reviews/{id}/reply`, `/webhooks` and `/usage`, plus the webhook events `review.created`, `review.updated`, `reply.published`, `reply.failed` and `connection.updated`. ## Core concepts in 60 seconds - **API key.** A secret for your workspace, sent as `Authorization: Bearer `. Live keys (`pk_live_…`) work on your real accounts; test keys (`pk_test_…`) work on sample data, and the two never mix. Never ship a key in a browser or mobile app. - **Connection.** The access a business gives Prisma on one platform: OAuth (Google, Trustpilot), the platform's API credentials (App Store, Google Play, Judge.me, Yotpo, REVIEWS.io, G2) or a public listing URL (Yelp, Tripadvisor, Booking.com). Created with `POST /v1/connections`. - **Location.** A Google Business Profile location, an app in one store, a Judge.me shop, a Trustpilot business unit or a listing on another platform. Plans are priced by connected locations; the Spark plan is free for 2. - **Review.** One normalized object, whatever the platform: `id`, `platform`, `location_id`, `rating`, `title`, `text`, `author`, `language`, `url`, `created_at`, `updated_at` and `reply`. - **Reply.** The public response from the business. Create or replace it with `POST`, remove it with `DELETE`. You write the text, or your software does; Prisma queues it and publishes it through the platform's official API. Read-only platforms have no reply API, so each review's `url` links to where you answer it on the platform. Prisma only works with accounts you own or manage. It does not collect reviews of other businesses. ## Supported platforms | Platform | Read reviews | Reply to reviews | Status | | --- | --- | --- | --- | | Google Business Profile | Yes | By API | At launch | | App Store | Yes | By API | At launch | | Google Play | Yes | By API | At launch | | Judge.me | Yes | By API | At launch | | Trustpilot | Yes | By API | Beta | | Yelp, Tripadvisor, Booking.com | Yes | Read only (link to answer on the platform) | Beta | | Yotpo, REVIEWS.io, G2 | Yes | Read only (link to answer on the platform) | Beta | Each platform has its own limits; they are all on [Platforms](/docs/api-reference/platforms). The platform pages go deeper: [Google Business Profile API](/google-business-profile-api), [Trustpilot API](/trustpilot-api), [App Store reviews API](/app-store-reviews-api) and [Google Play reviews API](/google-play-reviews-api). The full list is on [Integrations](/integrations). ## Docs for AI agents and LLMs A plain-text index of Prisma and its docs lives at [/llms.txt](/llms.txt), following the [llms.txt specification](https://llmstxt.org/), so coding assistants and agents can find the right page without parsing HTML. ## Need help? Read the `error.code` and `error.message` in the response first, and look the code up on [Errors](/docs/errors): most problems are a connection that needs reauthorization or the per-key rate limit. If you are still stuck, email hello@prismareviews.com with the `request_id` from the error response. ## FAQ ### Is there an SDK? Not yet. The API is plain REST and JSON, so any HTTP client works. The quickstart shows curl, Node (fetch) and Python (requests). ### Do I need my own Google Business Profile API approval? No. Prisma uses its own Google Cloud project for the Business Profile API. You only authorize Prisma on your profile with OAuth. ### Can I read reviews of a business I don't own? No. Prisma reads and replies only on accounts that you, or your client, have authorized as owner or manager. ### Is there a sandbox? Yes. Every plan includes test keys (`pk_test_…`) that connect any platform without real credentials, return sample reviews and accept replies without publishing them, so you can build and test your integration before connecting a real location. --- > Reviews API reference for Prisma: locations, reviews, replies, connections, usage and webhooks, with request and response examples and the normalized JSON. # Reviews API reference The Prisma reviews API is a REST API at `https://api.prismareviews.com/v1` that reads and replies to reviews of the businesses and apps you own or manage on Google Business Profile, the App Store, Google Play, Judge.me, Trustpilot, Yelp, Tripadvisor, Booking.com, Yotpo, REVIEWS.io and G2. It uses Bearer API keys, JSON request and response bodies, cursor pagination and signed webhooks. Every review comes back in one normalized shape, whatever the platform. New here? The [Quickstart](/docs/quickstart) takes you to your first call. This reference describes every endpoint as it works today. ## Resources | Page | Endpoints | | --- | --- | | [Authentication](/docs/api-reference/authentication) | API keys, live and test mode, rate limit headers | | [Reviews](/docs/api-reference/reviews) | `GET /v1/reviews`, `GET /v1/reviews/{id}` | | [Replies](/docs/api-reference/replies) | `POST /v1/reviews/{id}/reply`, `DELETE /v1/reviews/{id}/reply` | | [Locations](/docs/api-reference/locations) | `GET /v1/locations`, `GET /v1/locations/{id}` | | [Connections](/docs/api-reference/connections) | `POST /v1/connections`, `GET /v1/connections`, `DELETE /v1/connections/{id}` | | [Webhooks](/docs/api-reference/webhooks) | `POST /v1/webhooks`, `GET /v1/webhooks`, `DELETE /v1/webhooks/{id}`, events and signatures | | [Usage](/docs/api-reference/usage) | `GET /v1/usage` | | [Platforms](/docs/api-reference/platforms) | How each platform connects, reply support, limits and sync frequency | Error codes are on [Errors](/docs/errors) and changes to the API on the [Changelog](/changelog). ## Basics **Base URL:** `https://api.prismareviews.com/v1` **Authentication:** send your secret key in the `Authorization` header of every request. ```bash curl https://api.prismareviews.com/v1/locations \ -H "Authorization: Bearer $PRISMA_API_KEY" ``` ```javascript const res = await fetch("https://api.prismareviews.com/v1/locations", { headers: { Authorization: "Bearer " + process.env.PRISMA_API_KEY }, }); console.log(res.status, await res.json()); ``` ```python import os import requests resp = requests.get( "https://api.prismareviews.com/v1/locations", headers={"Authorization": f"Bearer {os.environ['PRISMA_API_KEY']}"}, timeout=30, ) print(resp.status_code, resp.json()) ``` A request without a well-formed key returns `401 missing_api_key`; a key that doesn't exist or was revoked returns `401 invalid_api_key`. Keys are secret: call Prisma from your server, never from a browser or mobile app. Details on [Authentication](/docs/api-reference/authentication). **Format:** JSON in, JSON out. Send `Content-Type: application/json` with a body. Timestamps are ISO 8601 in UTC with milliseconds (`2026-10-19T12:41:07.000Z`). A body that isn't valid JSON returns `422 invalid_request`. **IDs:** every object has a prefixed, URL-safe ID: `rev_` (review), `loc_` (location), `con_` (connection), `we_` (webhook endpoint), `evt_` (event), `req_` (request). Treat them as opaque strings. **Request IDs:** every response carries a `Request-Id` header, and every error body repeats it as `request_id`. Log it: support can trace any call with it. ## Pagination `GET /v1/reviews` is paginated with a cursor. It returns `data` and `next_cursor`: ```json { "data": [ { "id": "rev_01KJ8Q2M4T7HX3VB9CWD6NRZ5E", "...": "..." } ], "next_cursor": "MjAyNi0xMC0xOVQxMjo0MTowNy4wMDBafHJldl8wMUtKOFEyTTRUN0hYM1ZCOUNXRDZOUlo1RQ" } ``` - Results are ordered newest first (by the date the review was written on the platform). - `limit` sets the page size: 1 to 100, default 50. - Pass `cursor=` with the same filters to get the next page. `next_cursor: null` means the last page. - A cursor that can't be decoded returns `422 invalid_request`. `GET /v1/locations` and `GET /v1/connections` return every item in one page, with `next_cursor: null`. `GET /v1/webhooks` returns `data` only. ## Idempotency `POST /v1/reviews/{id}/reply` accepts an `Idempotency-Key` header. Send a unique value (a UUID works) per reply you intend to post: - The same key with the same body returns the stored response, with the original status code, and does not reply twice. - The same key with a different body or a different review returns `409 idempotency_conflict`. - Keys are scoped to your workspace. Only successful responses are stored, so a request that failed with an error can be retried with the same key. The other `POST` endpoints don't read the header. Retrying `POST /v1/connections` or `POST /v1/webhooks` creates a second object, so check with the matching `GET` first. ## Rate limits Each API key has a limit of requests per minute, set by your plan, counted in fixed one-minute windows: | Plan | Requests per minute, per key | | --- | --- | | Spark (free) | 60 | | Beam | 120 | | Spectrum | 600 | | Aurora | 1,200 | Every authenticated response carries `X-RateLimit-Limit` and `X-RateLimit-Remaining`. Above the limit you get `429 rate_limited` with a `Retry-After` header (seconds until the window resets). Requests over the limit are not counted in your usage. Plans and prices are on [Pricing](/pricing). Platform limits apply on top, for example Google's per-profile edit limit (see [Google Business Profile API usage limits](https://developers.google.com/my-business/content/limits)) or the [App Store Connect API rate limits](https://developer.apple.com/documentation/appstoreconnectapi/identifying-rate-limits). Prisma never calls a platform inside your request: syncs and replies run in a background queue that retries when a platform is busy. Per-platform details are on [Platforms](/docs/api-reference/platforms). ## Versioning The version is in the path: `/v1`. Within `v1` we only make additive changes, such as new endpoints, new optional parameters, new fields in responses and new webhook event types. Write your client so that unknown fields and event types are ignored. Every change is listed on the [Changelog](/changelog). ## The review object Every platform maps to the same fields. The full description is on [Reviews](/docs/api-reference/reviews). ```json { "id": "rev_01KJ8Q2M4T7HX3VB9CWD6NRZ5E", "platform": "google", "location_id": "loc_01KJ8P9W2C4D6F8G0H2J4K6M8N", "rating": 4, "title": null, "text": "Great flat white, a bit slow at lunchtime.", "author": "Marta G.", "language": "en", "url": null, "created_at": "2026-10-19T12:41:07.000Z", "updated_at": "2026-10-19T12:41:07.000Z", "reply": null } ``` ## Machine-readable docs A plain-text index of Prisma and its docs lives at [/llms.txt](/llms.txt), following the [llms.txt specification](https://llmstxt.org/), so coding assistants and agents can find the right page without parsing HTML. ## FAQ ### Is there an OpenAPI spec? Not yet. This reference is the contract for v1. The API is plain REST and JSON, so any HTTP client works. ### Is there an SDK? Not yet. Use curl, `fetch` in Node.js 18+ or `requests` in Python, as in the examples. ### Can I filter reviews by rating? Not in the API today. `GET /v1/reviews` filters by `location_id`, `platform`, `since` and `has_reply`; filter by rating on your side. ### Do test keys behave like live keys? Yes, with sample data. A `pk_test_` key can connect any platform without real credentials, gets two sample locations with sample reviews, and accepts replies without publishing them anywhere. Test and live data never mix. See [Authentication](/docs/api-reference/authentication). --- > How to authenticate with the Prisma reviews API: Bearer API keys, live and test keys, rate limit headers and the 401 errors you can get. # Authentication Every request to `https://api.prismareviews.com/v1` needs an API key in the `Authorization` header, as a Bearer token. There are two kinds of keys: live keys (`pk_live_…`) for real platforms and test keys (`pk_test_…`) for sample data. ```bash curl https://api.prismareviews.com/v1/usage \ -H "Authorization: Bearer $PRISMA_API_KEY" ``` ```javascript const res = await fetch("https://api.prismareviews.com/v1/usage", { headers: { Authorization: "Bearer " + process.env.PRISMA_API_KEY }, }); console.log(await res.json()); ``` ```python import os import requests resp = requests.get( "https://api.prismareviews.com/v1/usage", headers={"Authorization": f"Bearer {os.environ['PRISMA_API_KEY']}"}, timeout=30, ) print(resp.json()) ``` ## API keys - A key belongs to one workspace. Everything it creates (connections, locations, reviews, webhooks) belongs to that workspace. - Keys look like `pk_live_` or `pk_test_` followed by a random string. Prisma stores only a SHA-256 hash of each key, so a lost key can't be recovered: you get a new one. - You get your keys when you [create an account](https://app.prismareviews.com/signup) confirm your email and answer four short questions: your workspace then gets one live key and one test key, shown in full once. After that, the [dashboard](https://app.prismareviews.com/api-keys) only shows each key's prefix, and you can roll or revoke each key there. - To replace a key, or to revoke one you think was exposed, click **Roll** next to it in the dashboard. Prisma creates a new key, shows it once, and the old key stops working at once (requests with it get `invalid_api_key`). - Keep keys on your server, in an environment variable such as `PRISMA_API_KEY`. Never ship them in a browser, a mobile app or a public repository. ## Live and test mode The prefix of the key decides the mode of every request: | | Live key `pk_live_…` | Test key `pk_test_…` | | --- | --- | --- | | Connections | Real OAuth, credentials or listing URLs | Any platform, no credentials needed, active at once | | Locations | Your real locations and apps | Two sample locations per connection | | Reviews | Real reviews from the platform | Five sample reviews per location | | Replies | Published on the platform (read-only platforms return `422 reply_not_supported`) | Accepted, never published anywhere | | Plan limits | Apply | Apply (locations and platforms included in your plan) | Test and live data never mix: a test key never sees live connections, locations or reviews, and a live key never sees test ones. Asking for an object of the other mode returns `404 resource_not_found`. Two things are shared by both modes in a workspace: webhook endpoints (see [Webhooks](/docs/api-reference/webhooks)) and the monthly request counters in [Usage](/docs/api-reference/usage). ## Rate limit headers Every authenticated response includes: | Header | Meaning | | --- | --- | | `X-RateLimit-Limit` | Requests allowed per minute for this key, from your plan (Spark 60, Beam 120, Spectrum 600, Aurora 1,200). | | `X-RateLimit-Remaining` | Requests left in the current one-minute window. | | `Retry-After` | Only on `429 rate_limited`: seconds until the window resets. | | `Request-Id` | On every response: the `req_…` ID of the request. | The limit is per key and counted in fixed one-minute windows. Requests over the limit are not counted in your usage. ## Authentication errors ```json { "error": { "type": "authentication_error", "code": "invalid_api_key", "message": "This API key does not exist or was revoked.", "retryable": false, "request_id": "req_01KJ8T3C5D7E9F1G3H5J7K9M1N", "docs_url": "https://prismareviews.com/docs/errors#invalid_api_key" } } ``` | Status | `code` | When | | --- | --- | --- | | 401 | `missing_api_key` | No `Authorization` header, a scheme other than `Bearer`, or a value that isn't shaped like a Prisma key. | | 401 | `invalid_api_key` | The key is well formed but doesn't exist or was revoked. | | 429 | `rate_limited` | More requests in one minute than your plan allows. | All codes are on [Errors](/docs/errors). How Prisma protects keys and platform tokens is on [Security](/security). --- > POST, GET and DELETE /v1/connections: connect Google, Trustpilot, App Store, Google Play, Judge.me, Yelp and more to Prisma by OAuth, credentials or listing URL. # Connections A connection is the access a business gives Prisma to its account on one platform. Creating one imports its [locations](/docs/api-reference/locations) and starts the first review sync. There are three ways to connect, depending on the platform: | Mode | Platforms | What you send | | --- | --- | --- | | `oauth` | Google Business Profile, Trustpilot | `redirect_url`; you get an `authorization_url` for the owner to approve | | `credentials` | App Store, Google Play, Judge.me, Yotpo, REVIEWS.io, G2 | `credentials` with the platform's keys or IDs | | `listing_url` | Yelp, Tripadvisor, Booking.com | `listing_url`, the public page of the listing | The exact fields per platform are on [Platforms](/docs/api-reference/platforms). Prisma only connects accounts the business owns or manages, and never asks for a platform password. ## The connection object ```json { "id": "con_01KJ8P7R5S3T1V9W7X5Y3Z1A2B", "platform": "google", "status": "active", "metadata": { "client": "acme" }, "created_at": "2026-10-21T08:55:00.000Z", "updated_at": "2026-10-21T08:56:41.000Z" } ``` | Field | Type | Description | | --- | --- | --- | | `id` | string | Connection ID (`con_…`). | | `platform` | enum | Platform ID. | | `status` | enum | `pending` (waiting for the owner to approve OAuth), `active`, `reauth_required` (the platform rejected the stored token or key) or `disconnected`. | | `metadata` | object | Your own key-value pairs, returned as sent. | | `created_at`, `updated_at` | timestamp | | Credentials and tokens are never returned. They are stored encrypted (AES-256-GCM); see [Security](/security). ## Create a connection `POST /v1/connections` | Body param | Required | Description | | --- | --- | --- | | `platform` | yes | `google`, `app_store`, `google_play`, `judge_me`, `trustpilot`, `yelp`, `tripadvisor`, `booking`, `yotpo`, `reviews_io` or `g2`. | | `redirect_url` | OAuth platforms | Where the owner lands after approving. Must be a URL. | | `credentials` | credentials platforms, and Trustpilot | Object with the platform's fields. | | `listing_url` | Yelp, Tripadvisor, Booking.com | Public URL of the listing. | | `metadata` | no | Your own key-value pairs, for example your client ID. | The response is `201` with the connection object plus extra fields that depend on the mode. ### OAuth (Google Business Profile, Trustpilot) ```bash curl -X POST https://api.prismareviews.com/v1/connections \ -H "Authorization: Bearer $PRISMA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"platform": "google", "redirect_url": "https://yourapp.com/prisma/callback", "metadata": {"client": "acme"}}' ``` ```javascript const res = await fetch("https://api.prismareviews.com/v1/connections", { method: "POST", headers: { Authorization: "Bearer " + process.env.PRISMA_API_KEY, "Content-Type": "application/json", }, body: JSON.stringify({ platform: "google", redirect_url: "https://yourapp.com/prisma/callback", metadata: { client: "acme" }, }), }); const connection = await res.json(); // Send the business owner to connection.authorization_url ``` ```python import os import requests resp = requests.post( "https://api.prismareviews.com/v1/connections", json={ "platform": "google", "redirect_url": "https://yourapp.com/prisma/callback", "metadata": {"client": "acme"}, }, headers={"Authorization": f"Bearer {os.environ['PRISMA_API_KEY']}"}, timeout=30, ) connection = resp.json() print(connection["authorization_url"]) ``` **Response `201`** ```json { "id": "con_01KJ8P7R5S3T1V9W7X5Y3Z1A2B", "platform": "google", "status": "pending", "metadata": { "client": "acme" }, "created_at": "2026-10-21T08:55:00.000Z", "updated_at": "2026-10-21T08:55:00.000Z", "authorization_url": "https://accounts.google.com/o/oauth2/v2/auth?client_id=...&state=..." } ``` Send the owner of the account to `authorization_url`. They approve access on the platform, the platform sends them to Prisma, and Prisma sends them on to your `redirect_url` with these query parameters: | Outcome | Query parameters added to `redirect_url` | | --- | --- | | Approved | `connection_id=con_…&status=active&locations_created=3` | | Refused or failed | `connection_id=con_…&status=error&error=` (the platform's error, `missing_code` or `authorization_failed`) | On success the connection becomes `active`, its locations are created and you get a `connection.updated` [webhook](/docs/api-reference/webhooks). On failure the connection becomes `disconnected`; create a new one to try again. An authorization link works once. For Google, the account that approves must be an owner or manager of the profile; Prisma imports every location it can see. Trustpilot also needs `credentials.domain`, the domain of the Trustpilot profile, sent with the request (see [Platforms](/docs/api-reference/platforms)). ### Credentials (App Store, Google Play, Judge.me, Yotpo, REVIEWS.io, G2) ```bash curl -X POST https://api.prismareviews.com/v1/connections \ -H "Authorization: Bearer $PRISMA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"platform": "judge_me", "credentials": {"shop_domain": "example-store.myshopify.com", "api_token": "'"$JUDGEME_API_TOKEN"'"}}' ``` ```javascript const res = await fetch("https://api.prismareviews.com/v1/connections", { method: "POST", headers: { Authorization: "Bearer " + process.env.PRISMA_API_KEY, "Content-Type": "application/json", }, body: JSON.stringify({ platform: "judge_me", credentials: { shop_domain: "example-store.myshopify.com", api_token: process.env.JUDGEME_API_TOKEN, }, }), }); console.log(res.status, await res.json()); ``` ```python resp = requests.post( "https://api.prismareviews.com/v1/connections", json={ "platform": "judge_me", "credentials": { "shop_domain": "example-store.myshopify.com", "api_token": os.environ["JUDGEME_API_TOKEN"], }, }, headers={"Authorization": f"Bearer {os.environ['PRISMA_API_KEY']}"}, timeout=30, ) print(resp.status_code, resp.json()) ``` Prisma checks the shape of the credentials, then tries them against the platform before storing anything. If they work, the connection is `active` at once: **Response `201`** ```json { "id": "con_01KJ8Q0A2B4C6D8E0F2G4H6J8K", "platform": "judge_me", "status": "active", "metadata": {}, "created_at": "2026-10-21T09:10:00.000Z", "updated_at": "2026-10-21T09:10:00.000Z", "locations_created": 1, "locations_over_plan_limit": 0 } ``` If the shape is wrong or the platform rejects them, you get `422 invalid_credentials` and nothing is stored. The `message` says which field or what the platform answered. ### Listing URL (Yelp, Tripadvisor, Booking.com) ```bash curl -X POST https://api.prismareviews.com/v1/connections \ -H "Authorization: Bearer $PRISMA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"platform": "yelp", "listing_url": "https://www.yelp.com/biz/example-cafe-san-francisco"}' ``` ```javascript const res = await fetch("https://api.prismareviews.com/v1/connections", { method: "POST", headers: { Authorization: "Bearer " + process.env.PRISMA_API_KEY, "Content-Type": "application/json", }, body: JSON.stringify({ platform: "yelp", listing_url: "https://www.yelp.com/biz/example-cafe-san-francisco", }), }); console.log(res.status, await res.json()); ``` ```python resp = requests.post( "https://api.prismareviews.com/v1/connections", json={"platform": "yelp", "listing_url": "https://www.yelp.com/biz/example-cafe-san-francisco"}, headers={"Authorization": f"Bearer {os.environ['PRISMA_API_KEY']}"}, timeout=30, ) print(resp.status_code, resp.json()) ``` Each connection is one listing, and the response is `201` with `status: "active"`, `locations_created` and `locations_over_plan_limit`. The URL must be the public page of a listing on that platform, or you get `422 invalid_request`. Connect only listings of businesses you own or manage. These platforms are read only: Prisma reads the reviews, and each review's `url` links to where you answer it on the platform. ### With a test key With a `pk_test_` key, any platform connects at once without credentials, OAuth or a listing URL: the connection is `active` and gets two sample locations with sample reviews. Use it to build your integration before touching a real account. ```bash curl -X POST https://api.prismareviews.com/v1/connections \ -H "Authorization: Bearer $PRISMA_TEST_KEY" \ -H "Content-Type: application/json" \ -d '{"platform": "trustpilot"}' ``` ### Errors | Status | `code` | When | | --- | --- | --- | | 403 | `plan_upgrade_required` | The platform isn't in your plan: Yelp, Tripadvisor, Booking.com, Yotpo, REVIEWS.io and G2 start at Beam. Includes `platform`. | | 422 | `invalid_request` | Unknown `platform`, missing `redirect_url`, `credentials` or `listing_url` for that platform, a listing URL from another site, or a body that isn't valid JSON. | | 422 | `invalid_credentials` | Credentials with the wrong shape, or rejected by the platform. | | 501 | `platform_not_available` | The platform can't be connected with live keys yet. Every platform listed here is available today. | ## List connections `GET /v1/connections` Returns every connection of your workspace in the mode of your key, oldest first, including `pending`, `reauth_required` and `disconnected` ones. Not paginated: `next_cursor` is always `null`. ```bash curl https://api.prismareviews.com/v1/connections \ -H "Authorization: Bearer $PRISMA_API_KEY" ``` ```json { "data": [ { "id": "con_01KJ8P7R5S3T1V9W7X5Y3Z1A2B", "platform": "google", "status": "active", "metadata": { "client": "acme" }, "created_at": "2026-10-21T08:55:00.000Z", "updated_at": "2026-10-21T08:56:41.000Z" } ], "next_cursor": null } ``` ## Delete a connection `DELETE /v1/connections/{id}` ```bash curl -X DELETE https://api.prismareviews.com/v1/connections/con_01KJ8P7R5S3T1V9W7X5Y3Z1A2B \ -H "Authorization: Bearer $PRISMA_API_KEY" ``` **Response `200`** ```json { "id": "con_01KJ8P7R5S3T1V9W7X5Y3Z1A2B", "platform": "google", "status": "disconnected", "metadata": { "client": "acme" }, "created_at": "2026-10-21T08:55:00.000Z", "updated_at": "2026-10-21T08:56:41.000Z", "locations_disconnected": 3 } ``` - Stored tokens and credentials are deleted immediately, and Prisma stops checking its locations. - Its locations become `disconnected` and stop counting toward your plan. Their reviews stay readable for 30 days and are then deleted. - `404 resource_not_found` if there is no connection with that ID in your workspace and mode. To revoke access on the platform's side too, remove Prisma from the account's settings on that platform (for example, the connected apps of the Google account). ## When a connection needs reauthorization If a platform rejects Prisma's token or key (the owner revoked access, a key was deleted, a token expired for good), the connection becomes `reauth_required`, Prisma stops publishing replies for it (`POST /v1/reviews/{id}/reply` returns `409 connection_reauth_required`) and you get a `connection.updated` webhook with a `reason`. There is no endpoint to reauthorize an existing connection. Delete it and create a new one. The new connection creates new locations, with new `loc_` and `rev_` IDs, and imports the reviews again; delete the old connection first so its locations don't count toward your plan limit. --- > GET /v1/locations and GET /v1/locations/{id}: the Google locations, apps, shops, business units and listings connected to your Prisma workspace. # Locations A location is one thing that receives reviews on one platform: a Google Business Profile location, an app in the App Store or Google Play, a Judge.me shop, a Trustpilot business unit, a Yotpo or REVIEWS.io store, a G2 product, or a Yelp, Tripadvisor or Booking.com listing. Locations are created by Prisma when you [connect](/docs/api-reference/connections) an account, and every [review](/docs/api-reference/reviews) belongs to one. Plans are priced by connected locations: see [Pricing](/pricing). ## The location object ```json { "id": "loc_01KJ8P9W2C4D6F8G0H2J4K6M8N", "platform": "google", "connection_id": "con_01KJ8P7R5S3T1V9W7X5Y3Z1A2B", "name": "Café Ruzafa", "address": "Carrer de Sueca 10, Valencia", "url": "https://maps.google.com/?cid=1234567890", "external_id": "accounts/1234567890/locations/9876543210", "status": "active", "last_synced_at": "2026-10-23T08:00:12.000Z", "next_sync_at": "2026-10-24T08:00:12.000Z" } ``` | Field | Type | Description | | --- | --- | --- | | `id` | string | Prisma location ID (`loc_…`). Use it as `location_id` in `GET /v1/reviews`. | | `platform` | enum | Platform ID, as in the review object. | | `connection_id` | string | The connection that created it. | | `name` | string | Name on the platform. For Yelp, Tripadvisor and Booking.com it is the listing URL you connected. | | `address` | string \| null | Address for places. For App Store apps it holds the bundle ID. | | `url` | string \| null | Public page of the location, app or listing, when known. | | `external_id` | string | The platform's own ID: Google location name, App Store app ID, Android package name, listing URL… | | `status` | enum | `active` or `disconnected`. | | `last_synced_at` | timestamp \| null | Last time Prisma checked it for reviews. `null` until the first sync finishes. | | `next_sync_at` | timestamp \| null | When the next check is scheduled. `null` once disconnected. | How often each platform is checked is on [Platforms](/docs/api-reference/platforms). ## List locations `GET /v1/locations` Returns every connected location of your workspace in the mode of your key, oldest first. Disconnected locations are left out. The list is not paginated: `next_cursor` is always `null`. ```bash curl https://api.prismareviews.com/v1/locations \ -H "Authorization: Bearer $PRISMA_API_KEY" ``` ```javascript const res = await fetch("https://api.prismareviews.com/v1/locations", { headers: { Authorization: "Bearer " + process.env.PRISMA_API_KEY }, }); const { data } = await res.json(); for (const loc of data) console.log(loc.id, loc.platform, loc.name); ``` ```python import os import requests resp = requests.get( "https://api.prismareviews.com/v1/locations", headers={"Authorization": f"Bearer {os.environ['PRISMA_API_KEY']}"}, timeout=30, ) for loc in resp.json()["data"]: print(loc["id"], loc["platform"], loc["name"]) ``` **Response `200`** ```json { "data": [ { "id": "loc_01KJ8P9W2C4D6F8G0H2J4K6M8N", "platform": "google", "connection_id": "con_01KJ8P7R5S3T1V9W7X5Y3Z1A2B", "name": "Café Ruzafa", "address": "Carrer de Sueca 10, Valencia", "url": "https://maps.google.com/?cid=1234567890", "external_id": "accounts/1234567890/locations/9876543210", "status": "active", "last_synced_at": "2026-10-23T08:00:12.000Z", "next_sync_at": "2026-10-24T08:00:12.000Z" } ], "next_cursor": null } ``` There are no query filters: filter by `platform` or `connection_id` on your side. ## Get a location `GET /v1/locations/{id}` ```bash curl https://api.prismareviews.com/v1/locations/loc_01KJ8P9W2C4D6F8G0H2J4K6M8N \ -H "Authorization: Bearer $PRISMA_API_KEY" ``` ```javascript const res = await fetch( "https://api.prismareviews.com/v1/locations/loc_01KJ8P9W2C4D6F8G0H2J4K6M8N", { headers: { Authorization: "Bearer " + process.env.PRISMA_API_KEY } } ); console.log(await res.json()); ``` ```python resp = requests.get( "https://api.prismareviews.com/v1/locations/loc_01KJ8P9W2C4D6F8G0H2J4K6M8N", headers={"Authorization": f"Bearer {os.environ['PRISMA_API_KEY']}"}, timeout=30, ) print(resp.json()) ``` Returns `200` with one location object, including disconnected ones until their data is deleted. `404 resource_not_found` if there is no location with that ID in your workspace and mode. ## Plan limits When a connection would bring more locations than your plan allows (Spark 2, Beam 10, Spectrum 50, Aurora 200), Prisma creates locations up to the limit and reports the rest in `locations_over_plan_limit` on the connection response. Those aren't imported. To add them, upgrade or delete a connection you no longer need, then connect again. Test locations count separately from live ones. `GET /v1/usage` shows how many live locations you use: see [Usage](/docs/api-reference/usage). --- > How each platform connects to the Prisma reviews API (OAuth, credentials or listing URL), the exact credential fields, reply support, reply limits and sync frequency. # Platforms Prisma reads reviews from eleven platforms and returns them in the same JSON. This page lists, for each one, the `platform` ID, how it connects with [`POST /v1/connections`](/docs/api-reference/connections), whether Prisma can publish replies there and how often Prisma checks for new reviews. ## Summary | Platform | `platform` | Connects with | Replies | Reply limit | Delete reply | Plans | | --- | --- | --- | --- | --- | --- | --- | | Google Business Profile | `google` | OAuth | By API | 4,096 bytes | Yes | All | | App Store | `app_store` | Credentials | By API | 5,970 characters | Yes | All | | Google Play | `google_play` | Credentials | By API | 350 characters | No | All | | Judge.me | `judge_me` | Credentials | By API (no edits) | — | No | All | | Trustpilot (beta) | `trustpilot` | OAuth + credentials | By API | — | Yes | All | | Yelp (beta) | `yelp` | Listing URL | Read only | — | — | Beam and up | | Tripadvisor (beta) | `tripadvisor` | Listing URL | Read only | — | — | Beam and up | | Booking.com (beta) | `booking` | Listing URL | Read only | — | — | Beam and up | | Yotpo (beta) | `yotpo` | Credentials | Read only | — | — | Beam and up | | REVIEWS.io (beta) | `reviews_io` | Credentials | Read only | — | — | Beam and up | | G2 (beta) | `g2` | Credentials | Read only | — | — | Beam and up | - **By API:** Prisma publishes the reply through the platform's official API (`202`, `status: "queued"`, then `published`). - **Read only:** the platform has no reply API for third parties. Prisma reads the reviews, and each review's `url` links to where you answer it on the platform (for some platforms, the listing or product page). `POST` and `DELETE /v1/reviews/{id}/reply` return `422 reply_not_supported`. Prisma never logs into a customer's account. - **—** in the limit column means Prisma's own limit of 10,000 characters applies. - Connecting a platform that isn't in your plan returns `403 plan_upgrade_required`. Plans are on [Pricing](/pricing). ## How often Prisma checks for reviews The first sync starts as soon as a location is connected. After that: | Platforms | Spark | Beam | Spectrum | Aurora | | --- | --- | --- | --- | --- | | Google Business Profile | Every 24 hours | Every 24 hours | Every 24 hours | Every 24 hours | | App Store, Google Play, Judge.me, Trustpilot | Every 60 minutes | Every 60 minutes | Every 15 minutes | Every 15 minutes | | Yelp, Tripadvisor, Booking.com, Yotpo, REVIEWS.io, G2 | Not included | Every 6 hours | Every 60 minutes | Every 30 minutes | `last_synced_at` and `next_sync_at` on each [location](/docs/api-reference/locations) show the schedule, and `review.created` [webhooks](/docs/api-reference/webhooks) fire when a new review is found. The first sync imports up to 90 days of history on Spark, 365 days on Beam and everything the platform returns on Spectrum and Aurora. ## Google Business Profile `"platform": "google"` · OAuth · replies by API · [Google Business Profile API](/google-business-profile-api) ```json { "platform": "google", "redirect_url": "https://yourapp.com/prisma/callback" } ``` - The owner or a manager of the profile approves access on Google's consent screen (scope `business.manage`). Prisma imports every location that account can see, one location per Business Profile location. - Prisma uses its own Google Cloud project, so you don't need one. - Replies: up to 4,096 bytes; posting again replaces the reply; deleting is supported. - Google limits edits per profile (see [Google Business Profile API usage limits](https://developers.google.com/my-business/content/limits)). Prisma queues replies and retries when Google answers with 429. ## App Store `"platform": "app_store"` · credentials · replies by API · [App Store reviews API](/app-store-reviews-api) Create an App Store Connect API key with the **Customer Support** role (App Store Connect → Users and Access → Integrations) and send: | Field | Description | | --- | --- | | `issuer_id` | Your issuer ID (a UUID). | | `key_id` | The 10-character key ID. | | `private_key` | The full contents of the `.p8` file, including the `BEGIN PRIVATE KEY` line. Keep the line breaks as `\n` in JSON. | ```json { "platform": "app_store", "credentials": { "issuer_id": "57246542-96fe-1a63-e053-0824d011072a", "key_id": "2X9R4HXF34", "private_key": "-----BEGIN PRIVATE KEY-----\nMIGTAgEAMBMG...\n-----END PRIVATE KEY-----" } } ``` Each app the key can see becomes a location (`address` holds the bundle ID). Replies: up to 5,970 characters; posting again replaces the reply; deleting is supported. Apple applies hourly limits per key (see [App Store Connect API rate limits](https://developer.apple.com/documentation/appstoreconnectapi/identifying-rate-limits)). ## Google Play `"platform": "google_play"` · credentials · replies by API · [Google Play reviews API](/google-play-reviews-api) In Play Console, invite Prisma's service account, `reviews@sopa-reviews-play.iam.gserviceaccount.com`, with the permissions **Reply to reviews** and **View app information**, for the apps you want to connect. Then send their package names: | Field | Description | | --- | --- | | `package_names` | Array of 1 to 200 Android package names, like `com.example.app`. | ```json { "platform": "google_play", "credentials": { "package_names": ["com.example.app"] } } ``` Prisma checks access to every package when you connect, so a typo or a missing invitation returns `422 invalid_credentials` at once. Each package becomes a location. - The Google Play API only returns reviews created or edited in the last 7 days. Older reviews aren't available, so your history is what Prisma has stored since you connected. - Replies: up to 350 characters; posting again replaces the reply; Google Play's API can't delete replies (`422 operation_not_supported`). See [Google Play reply-to-reviews limits](https://developers.google.com/android-publisher/reply-to-reviews). ## Judge.me `"platform": "judge_me"` · credentials · replies by API · [Judge.me reviews API](/judge-me-reviews-api) | Field | Description | | --- | --- | | `shop_domain` | The shop's `myshopify.com` domain, like `example-store.myshopify.com`. | | `api_token` | The private API token from Judge.me (Settings → Integrations → Developers). | One location per shop, with all its product and store reviews. Judge.me's API can create replies but can't edit or delete them: send each reply once (`DELETE` returns `422 operation_not_supported`). Its API doesn't return replies written elsewhere, so `reply` only shows replies sent through Prisma. ## Trustpilot (beta) `"platform": "trustpilot"` · OAuth plus credentials · replies by API · [Trustpilot API](/trustpilot-api) Trustpilot's OAuth doesn't say which profile the user manages, so send the profile's domain with the request: | Field | Required | Description | | --- | --- | --- | | `credentials.domain` | yes | The domain of the Trustpilot profile, like `example.com`. | | `credentials.business_user_id` | no | The Trustpilot business user who signs replies. | | `redirect_url` | yes | Where the user lands after approving. | ```json { "platform": "trustpilot", "redirect_url": "https://yourapp.com/prisma/callback", "credentials": { "domain": "example.com" } } ``` A business user of that profile approves access. The business needs Trustpilot's API module on its account. One location per business unit. Replies can be replaced and deleted. ## Yelp, Tripadvisor and Booking.com (beta) `"platform": "yelp" | "tripadvisor" | "booking"` · listing URL · read only · [Yelp](/yelp-reviews-api), [Tripadvisor](/tripadvisor-reviews-api), [Booking.com](/booking-com-reviews-api) | Platform | `listing_url` must look like | | --- | --- | | Yelp | `https://www.yelp.com/biz/…` | | Tripadvisor | `https://www.tripadvisor.com/…_Review-…` (any Tripadvisor country domain) | | Booking.com | `https://www.booking.com/hotel/…` | ```json { "platform": "tripadvisor", "listing_url": "https://www.tripadvisor.com/Restaurant_Review-g187514-d1234567-Reviews-Example_Cafe-Madrid.html" } ``` One connection is one listing. Prisma reads the public reviews of the listing through a data provider; connect only listings of businesses you own or manage. Each check collects up to 1,000 reviews, and results arrive a few minutes after the check starts. Read only: answer reviews on the platform, with the link in each review's `url`. ## Yotpo, REVIEWS.io and G2 (beta) Credentials · read only · [Yotpo](/yotpo-reviews-api), [REVIEWS.io](/reviews-io-api), [G2](/g2-reviews-api) | Platform | Fields | | --- | --- | | Yotpo (`yotpo`) | `store_id` (the Store ID, also called app key) and `api_secret`. One location per store. Yotpo's own data can take up to an hour to show new reviews. | | REVIEWS.io (`reviews_io`) | `store` (the store key) and `api_key` (the private API key, from Integrations → API). One location per store, with company and product reviews. | | G2 (`g2`) | `api_token` (a G2 API token with access to product reviews) and, optionally, `products`: 1 to 200 product UUIDs or slugs. Without `products`, Prisma imports the products the account owns. One location per product. | ```json { "platform": "reviews_io", "credentials": { "store": "example-store", "api_key": "your-private-api-key" } } ``` These platforms' APIs can read reviews but not reply to them, so they are read only: each review's `url` links to where you answer it. G2 answers `403` when the vendor's G2 plan doesn't include API access to reviews; Prisma returns that as `422 invalid_credentials` at connection time. ## Not supported - **Facebook:** Meta removed the Page ratings API in Graph API v22.0. - **Amazon** and **Glassdoor:** not supported. The full overview, with setup notes, is on [Integrations](/integrations). ## FAQ ### Why can't I reply to Yelp, Tripadvisor and Booking.com reviews through Prisma? Those platforms have no API to reply to reviews for third parties. Prisma reads their reviews, and each review includes a `url` to answer it on the platform. A reply request returns `422 reply_not_supported`. Prisma never logs into a customer's account. ### Is Facebook supported? No. Meta removed the Page ratings API in Graph API v22.0. Amazon and Glassdoor are not supported either. ### Do I need my own Google Cloud project? No. Prisma uses its own Google Cloud project for the Business Profile API. The owner or a manager of the profile only approves access with OAuth. --- > POST and DELETE /v1/reviews/{id}/reply: publish your reply to Google, App Store, Google Play, Judge.me and Trustpilot reviews through their official APIs. # Replies A reply is the business's public answer to a review. You write the text, or your software does; Prisma publishes it through the platform's official API. Each review has at most one reply in Prisma: `POST /v1/reviews/{id}/reply` creates it or replaces it, and `DELETE /v1/reviews/{id}/reply` removes it. What happens next depends on the platform: - **API platforms** (Google Business Profile, App Store, Google Play, Judge.me, Trustpilot): Prisma publishes the reply through the platform's official API. The request returns `202` with `reply.status: "queued"`, and a background job publishes it. - **Read-only platforms** (Yelp, Tripadvisor, Booking.com, Yotpo, REVIEWS.io, G2): these platforms have no reply API for third parties, so Prisma reads their reviews but can't reply. `POST` and `DELETE` return `422 reply_not_supported` with the review's `url`, a link to answer it on the platform. The full table, with per-platform limits, is on [Platforms](/docs/api-reference/platforms). ## Create or replace a reply `POST /v1/reviews/{id}/reply` | Body param | Required | Description | | --- | --- | --- | | `text` | yes | Reply text. Leading and trailing spaces are trimmed; 1 to 10,000 characters, and the platform's own limit applies (see below). | | Header | Required | Description | | --- | --- | --- | | `Idempotency-Key` | no | A unique value per reply. Retrying with the same key and body returns the first response instead of replying twice. | ```bash curl -X POST https://api.prismareviews.com/v1/reviews/rev_01KJ8Q2M4T7HX3VB9CWD6NRZ5E/reply \ -H "Authorization: Bearer $PRISMA_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: 7c1e2a90-4b1d-4e0a-9d55-1f6c0b7f2e31" \ -d '{"text": "Thanks Marta! We have added a second barista at lunch."}' ``` ```javascript import crypto from "node:crypto"; const res = await fetch( "https://api.prismareviews.com/v1/reviews/rev_01KJ8Q2M4T7HX3VB9CWD6NRZ5E/reply", { method: "POST", headers: { Authorization: "Bearer " + process.env.PRISMA_API_KEY, "Content-Type": "application/json", "Idempotency-Key": crypto.randomUUID(), }, body: JSON.stringify({ text: "Thanks Marta! We have added a second barista at lunch." }), } ); const review = await res.json(); console.log(res.status, review.reply.status); // 202 "queued" ``` ```python import os import uuid import requests resp = requests.post( "https://api.prismareviews.com/v1/reviews/rev_01KJ8Q2M4T7HX3VB9CWD6NRZ5E/reply", json={"text": "Thanks Marta! We have added a second barista at lunch."}, headers={ "Authorization": f"Bearer {os.environ['PRISMA_API_KEY']}", "Idempotency-Key": str(uuid.uuid4()), }, timeout=30, ) print(resp.status_code, resp.json()["reply"]["status"]) ``` The response is the full [review object](/docs/api-reference/reviews) with the new `reply`. **Response `202`** (API platforms) ```json { "id": "rev_01KJ8Q2M4T7HX3VB9CWD6NRZ5E", "platform": "google", "location_id": "loc_01KJ8P9W2C4D6F8G0H2J4K6M8N", "rating": 4, "title": null, "text": "Great flat white, a bit slow at lunchtime.", "author": "Marta G.", "language": "en", "url": null, "created_at": "2026-10-19T12:41:07.000Z", "updated_at": "2026-10-19T12:41:07.000Z", "reply": { "text": "Thanks Marta! We have added a second barista at lunch.", "created_at": null, "status": "queued", "error": null } } ``` **Response `422`** (read-only platforms): nothing is stored or sent. Answer the review on the platform with the link in `url`. ```json { "error": { "type": "invalid_request_error", "code": "reply_not_supported", "message": "Yelp has no reply API for third parties, so Prisma only reads its reviews. Answer this one on the platform: https://www.yelp.com/biz/example-cafe-san-francisco", "retryable": false, "request_id": "req_01KJ8T6F8G0H2J4K6M8N0P2Q4R", "docs_url": "https://prismareviews.com/docs/errors#reply_not_supported", "platform": "yelp", "url": "https://www.yelp.com/biz/example-cafe-san-francisco" } } ``` ### What happens to a queued reply 1. A background job sends the reply to the platform. When the queue is busy, jobs from bigger plans run first. 2. When the platform accepts it, `reply.status` becomes `published`, `reply.created_at` is set and you get a `reply.published` [webhook](/docs/api-reference/webhooks). 3. If the platform is rate limiting or down (HTTP 429 or 5xx), Prisma retries with exponential backoff: 30 seconds, then doubling up to one hour between attempts, for up to 8 attempts. 4. If the platform refuses the reply, or every attempt fails, `reply.status` becomes `failed`, `reply.error` says why and you get a `reply.failed` webhook. Fix the cause and `POST` the reply again. 5. If the platform rejects Prisma's access (expired or revoked token or key), the connection becomes `reauth_required` and you get a `connection.updated` webhook. See [Connections](/docs/api-reference/connections). With a test key, queued replies go through the same steps and end as `published`, but nothing reaches any platform. ### Replacing a reply Posting again replaces the stored reply and, on API platforms, publishes the new text: Google Business Profile, App Store, Google Play and Trustpilot update the reply on the platform. Judge.me's API can create replies but not edit them, so send a Judge.me reply once. ### Reply length limits Prisma checks the platform's limit before queueing, so a reply that is too long fails at once instead of in the background: | Platform | Limit | | --- | --- | | Google Business Profile | 4,096 bytes (UTF-8) | | App Store | 5,970 characters | | Google Play | 350 characters | | Others | 10,000 characters (Prisma's own limit) | ```json { "error": { "type": "invalid_request_error", "code": "reply_too_long", "message": "Google Play accepts replies of up to 350 characters.", "retryable": false, "request_id": "req_01KJ8T3C5D7E9F1G3H5J7K9M1N", "docs_url": "https://prismareviews.com/docs/errors#reply_too_long", "platform": "google_play", "max_length": 350 } } ``` ### Errors | Status | `code` | When | | --- | --- | --- | | 404 | `resource_not_found` | No review with that ID in your workspace and mode. | | 409 | `connection_reauth_required` | The connection of the review's location is not `active`. The error includes `connection_id`. Reconnect, then retry. | | 409 | `idempotency_conflict` | The `Idempotency-Key` was already used with a different request. | | 422 | `invalid_request` | `text` is missing or empty, or the body isn't valid JSON. | | 422 | `reply_not_supported` | The platform is read only (Yelp, Tripadvisor, Booking.com, Yotpo, REVIEWS.io, G2). The error includes `platform` and `url`. | | 422 | `reply_too_long` | The text is over the platform's limit. The error includes `platform` and `max_length`. | ## Delete a reply `DELETE /v1/reviews/{id}/reply` ```bash curl -X DELETE https://api.prismareviews.com/v1/reviews/rev_01KJ8Q2M4T7HX3VB9CWD6NRZ5E/reply \ -H "Authorization: Bearer $PRISMA_API_KEY" ``` ```javascript const res = await fetch( "https://api.prismareviews.com/v1/reviews/rev_01KJ8Q2M4T7HX3VB9CWD6NRZ5E/reply", { method: "DELETE", headers: { Authorization: "Bearer " + process.env.PRISMA_API_KEY } } ); console.log(res.status, await res.json()); ``` ```python resp = requests.delete( "https://api.prismareviews.com/v1/reviews/rev_01KJ8Q2M4T7HX3VB9CWD6NRZ5E/reply", headers={"Authorization": f"Bearer {os.environ['PRISMA_API_KEY']}"}, timeout=30, ) print(resp.status_code, resp.json()) ``` **Response `202`** (a reply published through a platform that supports deleting: Google Business Profile, App Store, Trustpilot). A job removes it from the platform; when it's done, `reply` becomes `null`. ```json { "id": "rev_01KJ8Q2M4T7HX3VB9CWD6NRZ5E", "deleted": false, "status": "queued" } ``` | Status | `code` | When | | --- | --- | --- | | 404 | `resource_not_found` | No review with that ID in your workspace and mode. | | 404 | `reply_not_found` | The review has no reply. | | 422 | `operation_not_supported` | The platform's API can't delete replies: Google Play and Judge.me. On Google Play you can replace the reply instead. | | 422 | `reply_not_supported` | The platform is read only. Remove the reply on the platform, with the link in `url`. | All error codes are on [Errors](/docs/errors). --- > GET /v1/reviews and GET /v1/reviews/{id}: filters, cursor pagination and the normalized review object for Google, App Store, Google Play, Trustpilot and more. # Reviews A review is one customer review on one of your connected locations, normalized to the same JSON shape on every platform. Prisma imports reviews when a location is connected and then checks for new and edited ones on a schedule set by the platform and your plan (see [Platforms](/docs/api-reference/platforms)). You read them with two endpoints, and you get a [webhook](/docs/api-reference/webhooks) when one is created or updated. ## The review object ```json { "id": "rev_01KJ8Q2M4T7HX3VB9CWD6NRZ5E", "platform": "google", "location_id": "loc_01KJ8P9W2C4D6F8G0H2J4K6M8N", "rating": 4, "title": null, "text": "Great flat white, a bit slow at lunchtime.", "author": "Marta G.", "language": "en", "url": null, "created_at": "2026-10-19T12:41:07.000Z", "updated_at": "2026-10-19T12:41:07.000Z", "reply": { "text": "Thanks Marta! We have added a second barista at lunch.", "created_at": "2026-10-21T09:02:11.000Z", "status": "published", "error": null } } ``` | Field | Type | Description | | --- | --- | --- | | `id` | string | Prisma review ID (`rev_…`). Stable while the location stays connected. | | `platform` | enum | `google`, `app_store`, `google_play`, `judge_me`, `trustpilot`, `yelp`, `tripadvisor`, `booking`, `yotpo`, `reviews_io`, `g2` | | `location_id` | string | The [location](/docs/api-reference/locations) the review belongs to. | | `rating` | integer \| null | 1 to 5 stars. `null` if the platform gave no rating. | | `title` | string \| null | Review title, on platforms that have one (App Store, Trustpilot, Judge.me…). | | `text` | string \| null | Review body. `null` for rating-only reviews. | | `author` | string \| null | Display name as shown by the platform. | | `language` | string \| null | Language code when the platform provides one. | | `url` | string \| null | Public link to the review when the platform provides one. On read-only platforms (Yelp, Tripadvisor, Booking.com, Yotpo, REVIEWS.io, G2) it is always set: the link to answer the review on the platform, which for some platforms is the listing or product page. | | `created_at` | timestamp | When the review was written on the platform. | | `updated_at` | timestamp | When it was last edited on the platform. | | `reply` | object \| null | The business reply, or `null` if there is none. | **The `reply` object** | Field | Type | Description | | --- | --- | --- | | `text` | string | Reply text. | | `status` | enum | `published` (live on the platform), `queued` (sent through Prisma, waiting to be published), `failed` (the platform refused it; see `error`). | | `created_at` | timestamp \| null | When the reply was published. `null` while it is queued. | | `error` | string \| null | Why publishing failed, when `status` is `failed`. | A reply that appears on the platform, whoever wrote it, comes back as `published`. How to create, replace and delete replies is on [Replies](/docs/api-reference/replies). ## List reviews `GET /v1/reviews` Returns the reviews of your workspace (in the mode of your key), newest first. | Query param | Type | Description | | --- | --- | --- | | `location_id` | string | Only reviews of this location. Omit to list across all locations. | | `platform` | enum | Only reviews from this platform (one of the values above). | | `since` | ISO 8601 | Only reviews written at or after this time, by `created_at`. Either a date (`2026-10-01`, meaning midnight UTC) or a full timestamp with time zone offset (`2026-10-01T00:00:00Z`, `2026-10-01T02:00:00+02:00`). | | `has_reply` | `true` \| `false` | `true`: reviews with a reply in any status. `false`: reviews without one. | | `limit` | integer | Page size, 1 to 100. Default 50. | | `cursor` | string | The `next_cursor` of the previous page. | ```bash curl "https://api.prismareviews.com/v1/reviews?location_id=loc_01KJ8P9W2C4D6F8G0H2J4K6M8N&has_reply=false&limit=20" \ -H "Authorization: Bearer $PRISMA_API_KEY" ``` ```javascript const params = new URLSearchParams({ location_id: "loc_01KJ8P9W2C4D6F8G0H2J4K6M8N", has_reply: "false", limit: "20", }); const res = await fetch("https://api.prismareviews.com/v1/reviews?" + params, { headers: { Authorization: "Bearer " + process.env.PRISMA_API_KEY }, }); const { data, next_cursor } = await res.json(); for (const review of data) console.log(review.rating, review.author, review.text); ``` ```python import os import requests resp = requests.get( "https://api.prismareviews.com/v1/reviews", params={"location_id": "loc_01KJ8P9W2C4D6F8G0H2J4K6M8N", "has_reply": "false", "limit": 20}, headers={"Authorization": f"Bearer {os.environ['PRISMA_API_KEY']}"}, timeout=30, ) resp.raise_for_status() for review in resp.json()["data"]: print(review["rating"], review["author"], review["text"]) ``` **Response `200`** ```json { "data": [ { "id": "rev_01KJ8Q2M4T7HX3VB9CWD6NRZ5E", "platform": "google", "location_id": "loc_01KJ8P9W2C4D6F8G0H2J4K6M8N", "rating": 4, "title": null, "text": "Great flat white, a bit slow at lunchtime.", "author": "Marta G.", "language": "en", "url": null, "created_at": "2026-10-19T12:41:07.000Z", "updated_at": "2026-10-19T12:41:07.000Z", "reply": null } ], "next_cursor": "MjAyNi0xMC0xOVQxMjo0MTowNy4wMDBafHJldl8wMUtKOFEyTTRUN0hYM1ZCOUNXRDZOUlo1RQ" } ``` ### Paging through every review Repeat the call with `cursor` set to the last `next_cursor`, keeping the same filters, until `next_cursor` is `null`. ```javascript let cursor = null; do { const params = new URLSearchParams({ limit: "100" }); if (cursor) params.set("cursor", cursor); const res = await fetch("https://api.prismareviews.com/v1/reviews?" + params, { headers: { Authorization: "Bearer " + process.env.PRISMA_API_KEY }, }); const page = await res.json(); for (const review of page.data) console.log(review.id); cursor = page.next_cursor; } while (cursor); ``` ```python import os import requests cursor = None while True: params = {"limit": 100} if cursor: params["cursor"] = cursor page = requests.get( "https://api.prismareviews.com/v1/reviews", params=params, headers={"Authorization": f"Bearer {os.environ['PRISMA_API_KEY']}"}, timeout=30, ).json() for review in page["data"]: print(review["id"]) cursor = page["next_cursor"] if not cursor: break ``` `since` filters by when the review was written, not when it was edited. To keep a local copy in sync with edits, listen for `review.updated` webhooks and fetch the review again. ## Get a review `GET /v1/reviews/{id}` ```bash curl https://api.prismareviews.com/v1/reviews/rev_01KJ8Q2M4T7HX3VB9CWD6NRZ5E \ -H "Authorization: Bearer $PRISMA_API_KEY" ``` ```javascript const res = await fetch( "https://api.prismareviews.com/v1/reviews/rev_01KJ8Q2M4T7HX3VB9CWD6NRZ5E", { headers: { Authorization: "Bearer " + process.env.PRISMA_API_KEY } } ); console.log(await res.json()); ``` ```python resp = requests.get( "https://api.prismareviews.com/v1/reviews/rev_01KJ8Q2M4T7HX3VB9CWD6NRZ5E", headers={"Authorization": f"Bearer {os.environ['PRISMA_API_KEY']}"}, timeout=30, ) print(resp.json()) ``` Returns `200` with one review object. If no review with that ID exists in your workspace and mode, you get `404 resource_not_found`. ## Good to know - **History.** On the first sync Prisma imports up to 90 days of history on Spark, 365 days on Beam and everything the platform returns on Spectrum and Aurora. Some platforms return less: the Google Play API only returns reviews from the last 7 days, so the history is what Prisma has stored since you connected. - **Disconnected locations.** When you delete a connection, its reviews stay readable until they are deleted 30 days later. - **Errors.** `422 invalid_request` for an unknown `platform`, a `since` that is neither a date nor a timestamp with offset, a `limit` out of range or a bad `cursor`. The `message` names the parameter. All codes are on [Errors](/docs/errors). --- > GET /v1/usage: your Prisma plan, connected locations against the plan limit, requests and replies published this month, and your rate limit. # Usage `GET /v1/usage` returns your workspace's plan and what it has used in the current calendar month (UTC). Use it to show usage in your own app or to warn before you reach the location limit. ## Get usage `GET /v1/usage` ```bash curl https://api.prismareviews.com/v1/usage \ -H "Authorization: Bearer $PRISMA_API_KEY" ``` ```javascript const res = await fetch("https://api.prismareviews.com/v1/usage", { headers: { Authorization: "Bearer " + process.env.PRISMA_API_KEY }, }); const usage = await res.json(); console.log(`${usage.locations.used} of ${usage.locations.limit} locations`); ``` ```python import os import requests usage = requests.get( "https://api.prismareviews.com/v1/usage", headers={"Authorization": f"Bearer {os.environ['PRISMA_API_KEY']}"}, timeout=30, ).json() print(f"{usage['locations']['used']} of {usage['locations']['limit']} locations") ``` **Response `200`** ```json { "plan": "beam", "period": "2026-10", "locations": { "used": 7, "limit": 10 }, "requests": 18234, "requests_per_minute_limit": 120, "replies_published": 412 } ``` | Field | Type | Description | | --- | --- | --- | | `plan` | enum | `spark`, `beam`, `spectrum`, `aurora` or `custom`. | | `period` | string | The current month, `YYYY-MM` in UTC. Counters reset when it changes. | | `locations.used` | integer | Active live locations. Test locations and disconnected ones are not counted. | | `locations.limit` | integer | Locations included in your plan: Spark 2, Beam 10, Spectrum 50, Aurora 200. | | `requests` | integer | API requests this month from all of the workspace's keys, test keys included. Requests rejected by the rate limit are not counted. | | `requests_per_minute_limit` | integer | Your per-key rate limit. | | `replies_published` | integer | Replies published on a platform through Prisma this month, test replies included. | Billing is by connected locations, not by requests: see [Pricing](/pricing). `GET /v1/usage` returns the same numbers with a live key and a test key. --- > Prisma webhooks: register endpoints with /v1/webhooks, get review.created, review.updated, reply.published, reply.failed and connection.updated, and verify the HMAC signature. # Webhooks Webhooks tell your server when something changes, so you don't have to poll. You register an HTTPS endpoint with the API, choose the events, and Prisma sends a signed `POST` for each one. ## Events | Event | When | `data` | | --- | --- | --- | | `review.created` | A new review is found on a connected location. | `id` (review), `location_id`, `platform` | | `review.updated` | A review's content changed on the platform, or a reply appeared or changed there. | `id`, `location_id`, `platform` | | `reply.published` | A reply sent through Prisma is live on the platform. | `id` (review), `location_id`, `platform` | | `reply.failed` | The platform refused a reply, or every retry failed. | `id` (review), `platform`, `error` | | `connection.updated` | An OAuth connection became `active`, or a connection became `reauth_required`. | `id` (connection), `platform`, `status`, plus `locations_created` or `reason` | Events carry IDs, not full objects. Fetch the current state with [`GET /v1/reviews/{id}`](/docs/api-reference/reviews) or [`GET /v1/connections`](/docs/api-reference/connections). ## Register an endpoint `POST /v1/webhooks` | Body param | Required | Description | | --- | --- | --- | | `url` | yes | Your endpoint. Must start with `https://`. | | `events` | no | Array of event types, or `["*"]` for all of them (the default). At least one if you send it. | ```bash curl -X POST https://api.prismareviews.com/v1/webhooks \ -H "Authorization: Bearer $PRISMA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"url": "https://yourapp.com/webhooks/prisma", "events": ["review.created", "reply.failed", "connection.updated"]}' ``` ```javascript const res = await fetch("https://api.prismareviews.com/v1/webhooks", { method: "POST", headers: { Authorization: "Bearer " + process.env.PRISMA_API_KEY, "Content-Type": "application/json", }, body: JSON.stringify({ url: "https://yourapp.com/webhooks/prisma", events: ["review.created", "reply.failed", "connection.updated"], }), }); const endpoint = await res.json(); // Store endpoint.secret now: it is never shown again ``` ```python import os import requests resp = requests.post( "https://api.prismareviews.com/v1/webhooks", json={ "url": "https://yourapp.com/webhooks/prisma", "events": ["review.created", "reply.failed", "connection.updated"], }, headers={"Authorization": f"Bearer {os.environ['PRISMA_API_KEY']}"}, timeout=30, ) endpoint = resp.json() print(endpoint["secret"]) # store it now: it is never shown again ``` **Response `201`** ```json { "id": "we_01KJ8R1A3B5C7D9E1F3G5H7J9K", "url": "https://yourapp.com/webhooks/prisma", "events": ["review.created", "reply.failed", "connection.updated"], "secret": "whsec_3q2Xk9LmP0aB7cD1eF4gH6jK8mN2pQ5r", "created_at": "2026-10-21T09:20:00.000Z" } ``` The signing `secret` is returned only in this response. Prisma stores it encrypted and never shows it again. Webhook endpoints belong to the workspace, not to a key: endpoints created with a test key and with a live key are the same list, and each endpoint receives events from both modes. Fetch the object with a key of the matching mode (a test review returns `404` to a live key). ## List endpoints `GET /v1/webhooks` ```bash curl https://api.prismareviews.com/v1/webhooks \ -H "Authorization: Bearer $PRISMA_API_KEY" ``` ```json { "data": [ { "id": "we_01KJ8R1A3B5C7D9E1F3G5H7J9K", "url": "https://yourapp.com/webhooks/prisma", "events": ["review.created", "reply.failed", "connection.updated"], "active": true, "created_at": "2026-10-21T09:20:00.000Z" } ] } ``` ## Delete an endpoint `DELETE /v1/webhooks/{id}` ```bash curl -X DELETE https://api.prismareviews.com/v1/webhooks/we_01KJ8R1A3B5C7D9E1F3G5H7J9K \ -H "Authorization: Bearer $PRISMA_API_KEY" ``` ```json { "id": "we_01KJ8R1A3B5C7D9E1F3G5H7J9K", "deleted": true } ``` `404 resource_not_found` if there is no endpoint with that ID in your workspace. Deliveries still pending for it are dropped. There is no update endpoint: to change the URL or the events, create a new endpoint and delete the old one. ## The delivery Each event is a `POST` with a JSON body: ```json { "id": "evt_01KJ8S2B4C6D8E0F2G4H6J8K0M", "type": "review.created", "created_at": "2026-10-21T09:00:03.000Z", "data": { "id": "rev_01KJ8Q2M4T7HX3VB9CWD6NRZ5E", "location_id": "loc_01KJ8P9W2C4D6F8G0H2J4K6M8N", "platform": "google" } } ``` More `data` examples: ```json { "id": "rev_01KJ8Q2M4T7HX3VB9CWD6NRZ5E", "platform": "google_play", "error": "Google Play API 400: ..." } ``` ```json { "id": "con_01KJ8P7R5S3T1V9W7X5Y3Z1A2B", "platform": "google", "status": "reauth_required", "reason": "Google rejected the access token" } ``` Headers: | Header | Value | | --- | --- | | `Content-Type` | `application/json` | | `User-Agent` | `Prisma-Webhooks/1` | | `Prisma-Signature` | `t=,v1=` | Answer with any `2xx` status within 10 seconds. Do the slow work after answering, for example in a queue. ### Retries A delivery fails on a non-`2xx` status, a timeout or a connection error. Failed deliveries are retried with exponential backoff: 30 seconds, then doubling up to one hour between attempts. Prisma stops after 30 attempts or when the event is older than your plan's retry window, whichever comes first: | Plan | Retry window | | --- | --- | | Spark | 24 hours | | Beam | 72 hours | | Spectrum, Aurora | 7 days | Each retry is signed again with a fresh timestamp and keeps the same event `id`. De-duplicate by `id`. ## Verify the signature `Prisma-Signature` is `t=,v1=`, where the signature is the hex HMAC-SHA256 of `.` with your endpoint's secret. Compute it over the raw request body, before any JSON parsing, compare in constant time and reject events whose timestamp is more than 5 minutes from your clock. ```javascript import crypto from "node:crypto"; export function verifyPrismaSignature(rawBody, header, secret) { const parts = Object.fromEntries(header.split(",").map((p) => p.split("="))); const t = Number(parts.t); if (!t || !parts.v1 || Math.abs(Date.now() / 1000 - t) > 300) return false; const expected = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex"); const a = Buffer.from(expected); const b = Buffer.from(parts.v1); return a.length === b.length && crypto.timingSafeEqual(a, b); } ``` ```python import hashlib import hmac import time def verify_prisma_signature(raw_body: bytes, header: str, secret: str) -> bool: parts = dict(p.split("=", 1) for p in header.split(",")) t, sig = parts.get("t"), parts.get("v1") if not t or not sig or abs(time.time() - int(t)) > 300: return False expected = hmac.new(secret.encode(), t.encode() + b"." + raw_body, hashlib.sha256).hexdigest() return hmac.compare_digest(expected, sig) ``` With Express, read the raw body with `express.raw({ type: "application/json" })` on the webhook route, verify, then `JSON.parse` it. ## FAQ ### Why doesn't the webhook include the whole review? Events carry IDs only, so a retried or late event never overwrites newer data. Fetch the current object with `GET /v1/reviews/{id}`. ### Can I get the signing secret again? No. It is shown once, when you create the endpoint. If you lose it, delete the endpoint and create a new one. ### Can events arrive twice or out of order? Yes, as with most webhook systems. A delivery that timed out is retried with the same event `id`, so de-duplicate by `id` and use `created_at` to order events. --- > Reviews API errors explained: every Prisma error code with its HTTP status, what it means and what to do, plus how Google, App Store and Trustpilot errors reach you. # Reviews API errors and how to fix them Prisma returns one error format for every endpoint and every platform. Each error has an HTTP status, a stable `code`, a `retryable` flag and a `request_id`. Most errors you'll see in production are one of three things: a connection that needs reauthorization, the per-key rate limit, or a reply the platform won't take. ## Error format ```json { "error": { "type": "conflict_error", "code": "connection_reauth_required", "message": "The connection for this location is not active. Reconnect it and try again.", "retryable": false, "request_id": "req_01KJ8T3C5D7E9F1G3H5J7K9M1N", "docs_url": "https://prismareviews.com/docs/errors#connection_reauth_required", "connection_id": "con_01KJ8P7R5S3T1V9W7X5Y3Z1A2B" } } ``` | Field | Description | | --- | --- | | `type` | The family of the error, from the HTTP status (table below). | | `code` | Stable, machine-readable code. Branch on this, not on `message`. | | `message` | Human-readable explanation. It can change; don't parse it. | | `retryable` | `true` for `429` and `5xx` errors: the same request can succeed later. | | `request_id` | The `req_…` ID, also in the `Request-Id` header. Log it and include it when you contact support. | | `docs_url` | Link to the description of this code on this page: `https://prismareviews.com/docs/errors#`. | Some errors add fields: `max_length` and `platform` on `reply_too_long`, `platform` and `url` on `reply_not_supported`, `platform` on `plan_upgrade_required`, `connection_id` on `connection_reauth_required`. | `type` | HTTP status | | --- | --- | | `authentication_error` | 401 | | `permission_error` | 403 | | `not_found_error` | 404 | | `conflict_error` | 409 | | `invalid_request_error` | 422 | | `rate_limit_error` | 429 | | `api_error` | 500, 501 | ## All error codes | HTTP | `code` | Retryable | Meaning | | --- | --- | --- | --- | | 401 | [`missing_api_key`](#missing_api_key) | No | No API key, or not shaped like one. | | 401 | [`invalid_api_key`](#invalid_api_key) | No | The key doesn't exist or was revoked. | | 403 | [`plan_upgrade_required`](#plan_upgrade_required) | No | The platform isn't in your plan. | | 404 | [`resource_not_found`](#resource_not_found) | No | No object with that ID in your workspace and mode. | | 404 | [`reply_not_found`](#reply_not_found) | No | The review has no reply to delete. | | 404 | [`route_not_found`](#route_not_found) | No | The method and path don't exist. | | 409 | [`connection_reauth_required`](#connection_reauth_required) | No | The location's connection is not active. | | 409 | [`idempotency_conflict`](#idempotency_conflict) | No | The `Idempotency-Key` was used with a different request. | | 422 | [`invalid_request`](#invalid_request) | No | A parameter or the body is missing or invalid. | | 422 | [`invalid_credentials`](#invalid_credentials) | No | Platform credentials are malformed or were rejected. | | 422 | [`operation_not_supported`](#operation_not_supported) | No | The platform's API can't do this. | | 422 | [`reply_not_supported`](#reply_not_supported) | No | The platform is read only: Prisma can't reply there. | | 422 | [`reply_too_long`](#reply_too_long) | No | The reply is over the platform's limit. | | 429 | [`rate_limited`](#rate_limited) | Yes | Too many requests this minute for this key. | | 500 | [`internal_error`](#internal_error) | Yes | Something failed on Prisma's side. | | 501 | [`platform_not_available`](#platform_not_available) | Yes | The platform can't be connected with live keys yet. | ## Error reference

missing_api_key · 401

**Type:** `authentication_error`. The request has no `Authorization` header, uses a scheme other than `Bearer`, or the value isn't shaped like a Prisma key (`pk_live_…` or `pk_test_…`). **What to do:** send `Authorization: Bearer $PRISMA_API_KEY`. Check that the environment variable is set and that no quotes or spaces ended up in it. See [Authentication](/docs/api-reference/authentication).

invalid_api_key · 401

**Type:** `authentication_error`. The key is well formed but doesn't exist or was revoked. **What to do:** use a current key. Keys can't be recovered, only replaced: email hello@prismareviews.com for a new one.

plan_upgrade_required · 403

**Type:** `permission_error`. You tried to connect a platform that isn't included in your plan. Yelp, Tripadvisor, Booking.com, Yotpo, REVIEWS.io and G2 start at Beam. The error includes `platform`. **What to do:** upgrade (see [Pricing](/pricing)) or connect a platform included in Spark: Google Business Profile, App Store, Google Play, Judge.me or Trustpilot.

resource_not_found · 404

**Type:** `not_found_error`. No review, location, connection or webhook endpoint with that ID exists in your workspace, in the mode of your key. A test key can't see live objects and a live key can't see test ones. **What to do:** check the ID and that you use a key of the right mode. Get current IDs from the list endpoints.

reply_not_found · 404

**Type:** `not_found_error`. `DELETE /v1/reviews/{id}/reply` was called on a review that has no reply. **What to do:** nothing to delete. If you track replies locally, refresh the review with `GET /v1/reviews/{id}`.

route_not_found · 404

**Type:** `not_found_error`. The method and path combination doesn't exist, for example `GET /v1/review` or `PUT /v1/reviews/{id}/reply`. **What to do:** check the path and method against the [API reference](/docs/api-reference). Every path starts with `/v1`.

connection_reauth_required · 409

**Type:** `conflict_error`. You tried to reply to a review whose location belongs to a connection that isn't `active`: it is `reauth_required` (the platform rejected the stored token or key), `pending` or `disconnected`. The error includes `connection_id`. **What to do:** don't retry as is. Delete the connection and create a new one, then reply again on the new location's review. You also get a `connection.updated` webhook when a connection becomes `reauth_required`. See [Connections](/docs/api-reference/connections).

idempotency_conflict · 409

**Type:** `conflict_error`. The `Idempotency-Key` you sent was already used in your workspace for a different request (another body or another review). **What to do:** use a new key for every distinct reply. Reuse a key only to retry exactly the same request.

invalid_request · 422

**Type:** `invalid_request_error`. A query parameter or body field is missing or invalid, or the body isn't valid JSON. The `message` names the field, for example `limit: Number must be less than or equal to 100`. **What to do:** fix the request. Retrying it unchanged gives the same error.

invalid_credentials · 422

**Type:** `invalid_request_error`. On `POST /v1/connections`, the `credentials` don't have the fields the platform needs, or the platform rejected them when Prisma tried them. Nothing was stored. **What to do:** read the `message`, which names the field or quotes the platform's answer, and check the fields on [Platforms](/docs/api-reference/platforms). For Google Play, check that Prisma's service account is invited for every package.

operation_not_supported · 422

**Type:** `invalid_request_error`. The platform's API can't do what you asked. Today this is deleting a reply on Google Play or Judge.me. **What to do:** on Google Play, replace the reply with `POST` instead. On Judge.me, change it in Judge.me itself.

reply_not_supported · 422

**Type:** `invalid_request_error`. You sent `POST` or `DELETE /v1/reviews/{id}/reply` for a review on a read-only platform: Yelp, Tripadvisor, Booking.com, Yotpo, REVIEWS.io or G2. These platforms have no reply API for third parties. The error includes `platform` and `url`, the link to answer the review on the platform (for some platforms, the listing or product page). **What to do:** answer the review on the platform with the link in `url`. Don't retry: nothing was stored or sent. To skip these reviews in your reply flow, check `platform` before you reply.

reply_too_long · 422

**Type:** `invalid_request_error`. The reply is longer than the platform accepts: Google Business Profile 4,096 bytes, App Store 5,970 characters, Google Play 350 characters. The error includes `platform` and `max_length`. **What to do:** shorten the text and send it again. Nothing was queued.

rate_limited · 429

**Type:** `rate_limit_error`. Your key made more requests this minute than your plan allows (Spark 60, Beam 120, Spectrum 600, Aurora 1,200 per minute). The `Retry-After` header says how many seconds until the window resets. **What to do:** wait `Retry-After` seconds and retry. Spread bulk jobs over time and use webhooks instead of polling.

internal_error · 500

**Type:** `api_error`. Something failed on Prisma's side. It is logged with the `request_id`. **What to do:** retry with exponential backoff. If it keeps happening, email hello@prismareviews.com with the `request_id`.

platform_not_available · 501

**Type:** `api_error`. The platform can't be connected with live keys yet. Every platform on [Platforms](/docs/api-reference/platforms) is available today, so you should not see it; it exists for platforms added in the future, which can be tried with a test key first. **What to do:** use a test key to build against the platform until it is available with live keys. ## Retrying safely Retry only when `retryable` is `true`, and back off exponentially. With `429`, wait `Retry-After` first. ```javascript async function prisma(path, init = {}) { for (let attempt = 0; attempt < 5; attempt++) { const res = await fetch("https://api.prismareviews.com/v1" + path, { ...init, headers: { Authorization: "Bearer " + process.env.PRISMA_API_KEY, ...init.headers }, }); if (res.ok) return res.json(); const { error } = await res.json(); if (!error.retryable) throw new Error(`${error.code}: ${error.message} (${error.request_id})`); const wait = Number(res.headers.get("Retry-After")) || 2 ** attempt; await new Promise((r) => setTimeout(r, wait * 1000)); } throw new Error("Prisma API: too many retries"); } ``` ```python import os import time import requests def prisma(method, path, **kw): for attempt in range(5): r = requests.request( method, "https://api.prismareviews.com/v1" + path, headers={"Authorization": f"Bearer {os.environ['PRISMA_API_KEY']}"}, timeout=30, **kw, ) if r.ok: return r.json() error = r.json()["error"] if not error["retryable"]: raise RuntimeError(f"{error['code']}: {error['message']} ({error['request_id']})") time.sleep(int(r.headers.get("Retry-After", 2 ** attempt))) raise RuntimeError("Prisma API: too many retries") ``` When you retry `POST /v1/reviews/{id}/reply`, send the same `Idempotency-Key` so a reply is never posted twice. ## Errors from the platforms Prisma never calls Google, Apple, Trustpilot or any other platform inside your request, so their errors don't come back as HTTP errors from Prisma. Syncs and replies run in a background queue, and platform errors reach you like this: | The platform answers | What Prisma does | What you see | | --- | --- | --- | | 401 or 403 on the stored token or key (revoked access, deleted key, expired refresh token) | Marks the connection `reauth_required` and stops using it | `connection.updated` webhook with `status` and `reason`; replies return `409 connection_reauth_required` | | 429 or 5xx (rate limited or down) | Retries with exponential backoff, 30 seconds doubling up to one hour, up to 8 attempts for a reply | Nothing, unless every attempt fails | | Any other refusal of a reply (for example, a review the platform won't let you answer) | Gives up at once | `reply.status: "failed"`, `reply.error` with the platform's answer, and a `reply.failed` webhook | | Rejected credentials when connecting | Stores nothing | `422 invalid_credentials` on `POST /v1/connections` | A few platform specifics: - **Google Business Profile:** Google refresh tokens stop working when the owner revokes access, after six months without use, or when too many tokens exist for the same account and client (see [Google OAuth 2.0](https://developers.google.com/identity/protocols/oauth2)). On Google, a 403 on a single location (for example, the account lost access to it) fails that reply instead of marking the connection. Google also limits edits per profile (see [Google Business Profile API usage limits](https://developers.google.com/my-business/content/limits)); Prisma retries those 429s for you. - **App Store:** a revoked key, or a key without the Customer Support role, makes Apple answer 401 or 403, and the connection becomes `reauth_required`. Status codes are in Apple's [App Store Connect API HTTP status codes](https://developer.apple.com/documentation/appstoreconnectapi/about-the-http-status-code). - **Trustpilot:** a 401 or 403 usually means the business user lost access to the profile or the account doesn't have Trustpilot's API module. See [Trustpilot common error messages](https://developers.trustpilot.com/common-error-messages) and the [Trustpilot API](/trustpilot-api) page. - **Google Play:** a 401 or 403 means Prisma's service account is no longer invited for that app in Play Console. See the [Google Play reviews API](/google-play-reviews-api) page. ## FAQ ### Why does my Google connection stop working after a while? Google refresh tokens stop working if the user revokes access, if the token goes unused for six months, or if too many tokens exist for the same account and client. Prisma marks the connection `reauth_required` and sends a `connection.updated` webhook; delete it and connect again. ### What should I do with a 429 error? Wait the number of seconds in the `Retry-After` header and retry. The limit is per API key and per minute, set by your plan. Platform rate limits never reach you as 429: Prisma queues and retries those itself. ### Why can't I find an old Google Play review? The Google Play API only returns reviews created or edited in the last 7 days. Prisma keeps every review it has seen since you connected, but it cannot import older ones. ### Do I get charged for failed requests? No. Billing is per connected location, not per request, and requests over the rate limit are not even counted in your usage. See also the [API reference](/docs/api-reference), the [Quickstart](/docs/quickstart) and the [Changelog](/changelog). --- > Google reviews API example in 5 minutes: get an API key, connect a location with OAuth, list reviews and post a reply. curl, Node and Python code. # Google reviews API example: your first call in 5 minutes This Google reviews API example takes you from zero to a posted reply in four steps: get a Prisma API key, connect a Google Business Profile location with OAuth, list its reviews, and reply to one. Every step has copy-paste code in curl, Node and Python. The same calls work for the App Store, Google Play, Trustpilot and the other platforms: only the `platform` value and how you connect change. **What you need** - A Google Business Profile you own or manage (or an App Store, Google Play or Trustpilot account). To try the API without one, use your test key: it works right away with sample data. - curl, Node.js 18+ (built-in `fetch`) or Python 3 with `requests`. - 5 minutes. You don't need your own Google Cloud project: Prisma uses its own for the Business Profile API. ## Step 1 — Get your API key [Create a free account](https://app.prismareviews.com/signup) with your email and a password, or with Google, GitHub or Apple, confirm your email and answer four short questions about how you'll use Prisma. You then get two keys, shown in full only once, so copy them: a live key (`pk_live_…`) for your real accounts and a test key (`pk_test_…`) that works right away with sample data and never touches a real account. Lost a key? Roll it in the [dashboard](https://app.prismareviews.com/api-keys) to get a new one. Store the key as an environment variable so it never ends up in your code: ```bash export PRISMA_API_KEY="pk_live_..." ``` Every request uses `https://api.prismareviews.com/v1` and the header `Authorization: Bearer $PRISMA_API_KEY`. Keys are secret: call Prisma from your server, never from a browser. More on [Authentication](/docs/api-reference/authentication). ## Step 2 — Connect a Google Business Profile location Create a connection. Prisma returns an `authorization_url`; open it and approve access with the Google account that owns or manages the profile. ```bash curl -X POST https://api.prismareviews.com/v1/connections \ -H "Authorization: Bearer $PRISMA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"platform": "google", "redirect_url": "https://yourapp.com/prisma/callback"}' ``` ```javascript const res = await fetch("https://api.prismareviews.com/v1/connections", { method: "POST", headers: { Authorization: "Bearer " + process.env.PRISMA_API_KEY, "Content-Type": "application/json", }, body: JSON.stringify({ platform: "google", redirect_url: "https://yourapp.com/prisma/callback" }), }); const connection = await res.json(); console.log(connection.authorization_url); ``` ```python import os import requests resp = requests.post( "https://api.prismareviews.com/v1/connections", json={"platform": "google", "redirect_url": "https://yourapp.com/prisma/callback"}, headers={"Authorization": f"Bearer {os.environ['PRISMA_API_KEY']}"}, timeout=30, ) print(resp.json()["authorization_url"]) ``` **Response `201`** ```json { "id": "con_01KJ8P7R5S3T1V9W7X5Y3Z1A2B", "platform": "google", "status": "pending", "metadata": {}, "created_at": "2026-10-21T08:55:00.000Z", "updated_at": "2026-10-21T08:55:00.000Z", "authorization_url": "https://accounts.google.com/o/oauth2/v2/auth?client_id=...&state=..." } ``` When the owner approves, Prisma sends them to your `redirect_url` with `connection_id`, `status=active` and `locations_created` in the query string. The connection becomes `active` and Prisma imports the locations that account can see. With a test key, the connection is `active` at once, with two sample locations, and there is no `authorization_url`. List the locations to get your `location_id`: ```bash curl https://api.prismareviews.com/v1/locations \ -H "Authorization: Bearer $PRISMA_API_KEY" ``` ```json { "data": [ { "id": "loc_01KJ8P9W2C4D6F8G0H2J4K6M8N", "platform": "google", "connection_id": "con_01KJ8P7R5S3T1V9W7X5Y3Z1A2B", "name": "Café Ruzafa", "address": "Carrer de Sueca 10, Valencia", "url": "https://maps.google.com/?cid=1234567890", "external_id": "accounts/1234567890/locations/9876543210", "status": "active", "last_synced_at": "2026-10-21T08:57:03.000Z", "next_sync_at": "2026-10-22T08:57:03.000Z" } ], "next_cursor": null } ``` Your first 2 connected locations are free. Other ways to connect (credentials for the App Store and Google Play, a listing URL for Yelp) are on [Connections](/docs/api-reference/connections). ## Step 3 — List reviews (Google reviews API example) Fetch the latest reviews for the location. `since` is optional and accepts a date (`2026-10-01`, midnight UTC) or a full ISO 8601 timestamp with offset (`2026-10-01T00:00:00Z`). ```bash curl "https://api.prismareviews.com/v1/reviews?location_id=loc_01KJ8P9W2C4D6F8G0H2J4K6M8N&platform=google&since=2026-10-01" \ -H "Authorization: Bearer $PRISMA_API_KEY" ``` ```javascript const res = await fetch( "https://api.prismareviews.com/v1/reviews?location_id=loc_01KJ8P9W2C4D6F8G0H2J4K6M8N&platform=google", { headers: { Authorization: "Bearer " + process.env.PRISMA_API_KEY } } ); if (!res.ok) throw new Error("Prisma error " + res.status); const { data } = await res.json(); for (const review of data) { console.log(review.rating, review.author, review.text); } ``` ### Google reviews API Python example ```python import os import requests resp = requests.get( "https://api.prismareviews.com/v1/reviews", params={"location_id": "loc_01KJ8P9W2C4D6F8G0H2J4K6M8N", "platform": "google"}, headers={"Authorization": f"Bearer {os.environ['PRISMA_API_KEY']}"}, timeout=30, ) resp.raise_for_status() for review in resp.json()["data"]: print(review["rating"], review["author"], review["text"]) ``` **Response** (same shape for every platform) ```json { "data": [ { "id": "rev_01KJ8Q2M4T7HX3VB9CWD6NRZ5E", "platform": "google", "location_id": "loc_01KJ8P9W2C4D6F8G0H2J4K6M8N", "rating": 4, "title": null, "text": "Great flat white, a bit slow at lunchtime.", "author": "Marta G.", "language": "en", "url": null, "created_at": "2026-10-19T12:41:07.000Z", "updated_at": "2026-10-19T12:41:07.000Z", "reply": null } ], "next_cursor": "MjAyNi0xMC0xOVQxMjo0MTowNy4wMDBafHJldl8wMUtKOFEyTTRUN0hYM1ZCOUNXRDZOUlo1RQ" } ``` Pass `cursor=` to get the next page. When `next_cursor` is `null`, you have everything. All fields and filters are on [Reviews](/docs/api-reference/reviews). ## Step 4 — Reply to a review Replies to Google reviews are published through Google's API. Posting again replaces the existing reply. ```bash curl -X POST https://api.prismareviews.com/v1/reviews/rev_01KJ8Q2M4T7HX3VB9CWD6NRZ5E/reply \ -H "Authorization: Bearer $PRISMA_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: 7c1e2a90-4b1d-4e0a-9d55-1f6c0b7f2e31" \ -d '{"text": "Thanks Marta! We have added a second barista at lunch. See you soon."}' ``` ```javascript import crypto from "node:crypto"; const res = await fetch("https://api.prismareviews.com/v1/reviews/rev_01KJ8Q2M4T7HX3VB9CWD6NRZ5E/reply", { method: "POST", headers: { Authorization: "Bearer " + process.env.PRISMA_API_KEY, "Content-Type": "application/json", "Idempotency-Key": crypto.randomUUID(), }, body: JSON.stringify({ text: "Thanks Marta! We have added a second barista at lunch." }), }); console.log(res.status, await res.json()); ``` ```python import os import uuid import requests resp = requests.post( "https://api.prismareviews.com/v1/reviews/rev_01KJ8Q2M4T7HX3VB9CWD6NRZ5E/reply", json={"text": "Thanks Marta! We have added a second barista at lunch."}, headers={ "Authorization": f"Bearer {os.environ['PRISMA_API_KEY']}", "Idempotency-Key": str(uuid.uuid4()), }, timeout=30, ) print(resp.status_code, resp.json()) ``` **Response `202`**: the full review, with the reply queued for publishing. ```json { "id": "rev_01KJ8Q2M4T7HX3VB9CWD6NRZ5E", "platform": "google", "location_id": "loc_01KJ8P9W2C4D6F8G0H2J4K6M8N", "rating": 4, "title": null, "text": "Great flat white, a bit slow at lunchtime.", "author": "Marta G.", "language": "en", "url": null, "created_at": "2026-10-19T12:41:07.000Z", "updated_at": "2026-10-19T12:41:07.000Z", "reply": { "text": "Thanks Marta! We have added a second barista at lunch.", "created_at": null, "status": "queued", "error": null } } ``` A background job publishes the reply on Google. When it is live, `reply.status` becomes `published` and you get a `reply.published` [webhook](/docs/api-reference/webhooks). If Google refuses it, `reply.status` becomes `failed` with the reason in `reply.error`, and you get `reply.failed`. With a test key the reply goes through the same steps but is never published anywhere. That's it: you listed reviews and sent a reply. If this took more than 15 minutes, tell us at hello@prismareviews.com; it's a bug on our side. ## Next steps - **Get reviews pushed to you.** Register a webhook for `review.created` with `POST /v1/webhooks` instead of polling. See [Webhooks](/docs/api-reference/webhooks). - **Add more platforms.** Repeat step 2 with another `platform`: `trustpilot` uses OAuth too, while `app_store` and `google_play` use credentials. See [Platforms](/docs/api-reference/platforms). - **Handle errors.** A connection that needs reauthorization and the per-key rate limit are the two you will meet first. See [Errors](/docs/errors). ## Common mistakes - **Calling from the browser.** Your API key is a secret. Call Prisma from your server. - **Connecting with the wrong Google account.** The account that approves OAuth must be an owner or manager of the profile, or no locations appear. - **Replying to an unverified Google location.** Google only accepts replies on verified locations. The request is accepted, then the reply ends as `failed` with Google's answer in `reply.error`. - **Bulk-replying in a tight loop.** Google limits edits per Business Profile (see [Google Business Profile API usage limits](https://developers.google.com/my-business/content/limits)). Prisma queues and retries, but space out large batches, and watch your own rate limit (`429 rate_limited`). - **Expecting old Google Play reviews.** The Google Play API only returns reviews created or edited in the last 7 days. Prisma keeps every review it sees from the day you connect, but can't import older ones. ## FAQ ### Is there an official Google reviews API? Yes. Reviews live in the Google Business Profile API (formerly Google My Business API). It requires a Google Cloud project with API access and OAuth on a profile you manage. Prisma uses its own Google Cloud project, so you only authorize your profile with OAuth. ### How many reviews does one call return? Prisma pages results with `next_cursor`; the page size is 50 by default, and you can set it with `limit` up to 100. For reference, Google's own reviews.list returns up to 50 reviews per page. ### Can I use this Google reviews API Python example for Trustpilot or the App Store? Yes. Steps 3 and 4 are identical: the response JSON has the same shape on every platform. Only step 2 changes: Trustpilot also needs the profile's domain, and the App Store connects with an App Store Connect API key instead of OAuth. ### Do I need a Google Cloud project? No. Prisma uses its own Google Cloud project. You only approve access to your profile with OAuth. ### Can I read competitors' reviews? No. Prisma only works on accounts you own or manage. ### What does it cost after the first 2 locations? Plans start at €49 a month for up to 10 locations. See [pricing](/pricing).