Skip to main content

Public API v1 · Pro, Enterprise, Agency

RevioReputation API and webhooks

A 1-star review lands in RevioReputation. Your CRM, your Slack and your team chat know nothing about it until someone opens the inbox. Every day it waits, people who look you up read it without your side of the story.

Connect them. We send a signed POST to your address when we see a new review, and your system can read reviews and post a reply you wrote. One format for reviews from every site your locations are connected to, and the API comes with your plan, not a separate contract.

Included in the Pro, Enterprise and Agency plans. Written for the owner and for the developer who builds the connection.

What you can do

  • Read your reviews and locations, with filters and pages.
  • Post a reply you wrote to a review. It goes out exactly as you send it.
  • Get a webhook when a new review arrives, when a reply is published, and when a new review has 1 or 2 stars.

What version 1 does not do

  • No AI-written replies through the API. AI drafts stay inside RevioReputation.
  • No editing or deleting a reply after it is published.
  • No creating or deleting reviews.
  • No OAuth sign-in for other apps. You connect with a key.
  • No SDK. It is plain HTTPS and JSON, so any language works.

Quick start

  1. In RevioReputation open Integrations > Developers and create a key. Pick its scopes. Copy the key right away: we show it once.
  2. Check the key. Every request goes to https://revioreputation.com/api/v1/public.
Request
curl https://revioreputation.com/api/v1/public/me \
  -H "Authorization: Bearer $REVIO_API_KEY"
Response
{
  "organization": { "id": "6d4b5e20-1a2b-4c3d-8e9f-0a1b2c3d4e5f", "name": "Casa Marisol" },
  "key": {
    "id": "0c9f8e7d-6b5a-4f3e-9d2c-1b0a9f8e7d6c",
    "name": "Zapier",
    "displayPrefix": "rk_live_ab12",
    "scopes": ["reviews:read", "webhooks:manage"],
    "expiresAt": null
  },
  "plan": "PRO"
}
  1. In the same tab add a webhook address and press Send test event. You see the status code your server returned, right there.

Keys and scopes

Send the key in a header on every request: Authorization: Bearer rk_live_.... Only the header works. Don't put the key in a URL.

Server-to-server only. Never put an API key in a browser page or a mobile app: anyone who opens it can copy the key.

ScopeWhat it allows
reviews:readList reviews and read one review.
reviews:replyPost a reply to a review.
locations:readList your locations and the platforms connected to each.
webhooks:manageAdd, list, remove and test webhook addresses made with this key.
any valid keyGET /me
  • You pick the scopes when you create a key, and they never change. Need another scope? Create another key.
  • We keep only a hash of the key, so we can't show it to you again. Lost it? Revoke it and create a new one.
  • A key never expires, or expires after 30, 90 or 365 days. You choose when you create it.
  • Up to 10 active keys per organization.
  • Revoking a key works on the next request: it gets 401 KEY_REVOKED. Webhook addresses made with that key are removed with it.
  • The API follows the plan of the organization owner. If the plan goes below Pro, requests get 403 PLAN_REQUIRED and webhooks stop.

Limits

  • 60 requests a minute per key. Every response that counts against the limit has X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset (Unix seconds when the minute resets). If our counter is briefly unavailable, the headers are missing and nothing is counted.
  • Over the limit you get 429 RATE_LIMITED with Retry-After in seconds. Wait that long and retry.
  • Too many failed key attempts from one IP address also get 429.
  • Lists: up to 100 items a page, 25 if you don't say.
  • Reply text: up to 4,096 characters.
  • Webhook addresses: up to 10 per organization.
  • Test event: one per address every 5 seconds, 20 a minute per organization.

Errors

Every error has the same shape. Branch on code: the message is for people and can change.

Error body
{
  "error": {
    "code": "INSUFFICIENT_SCOPE",
    "message": "This key cannot do this. Create a key with the reviews:reply scope.",
    "requestId": "3f2a1b0c-9d8e-4f7a-8b6c-5d4e3f2a1b0c"
  }
}

Every response also has an X-Request-Id header with the same id as requestId. If a call keeps failing, send us that id and we can find it in our logs.

Broken requests get the same shape: a body that is not valid JSON is 400 INVALID_JSON, a body over 100 kB is 413 PAYLOAD_TOO_LARGE, and a path that does not exist is 404 NOT_FOUND.

Any route can return 408 TIMEOUT when a request takes too long. The result is then unknown: retry, and for a reply use the same Idempotency-Key so it is never posted twice.

CodeHTTPWhat it means
MISSING_KEY401No "Authorization: Bearer" header.
INVALID_KEY401The key is wrong or was typed with a mistake.
KEY_REVOKED401Someone revoked this key. Create a new one.
KEY_EXPIRED401The key reached its expiry date. Create a new one.
PLAN_REQUIRED403The organization is not on Pro, Enterprise or Agency. The body has requiredTier.
INSUFFICIENT_SCOPE403The key does not have the scope this route needs.
FORBIDDEN403This action is not allowed for this key.
TIMEOUT408The request took too long and the result is unknown. Retry. For a reply, retry with the same Idempotency-Key.
RATE_LIMITED429Too many requests. Wait the Retry-After seconds.
VALIDATION_FAILED400A field or filter is missing, unknown or out of range. The message says which.
INVALID_JSON400The body is not valid JSON.
PAYLOAD_TOO_LARGE413The body is over 100 kB.
INVALID_CURSOR400The cursor is broken or belongs to another organization. Start again without it.
INVALID_URL400The webhook address is not a public https address on an allowed port.
NOT_FOUND404No such review or address in your organization, or no such path.
ALREADY_REPLIED409The review already has a published reply.
MANUAL_POST_REQUIRED409The API does not publish on this platform. The body has platformUrl, where you post it yourself.
INTEGRATION_REQUIRED400The location is not connected to that platform in RevioReputation.
PLATFORM_POST_FAILED422On Google the platform refused the reply and nothing was published, so you can retry. On Facebook and Instagram the reply may have gone out anyway, and the message says: "The platform did not confirm the reply. It may already be visible there: check before posting again."
PLATFORM_REPLY_UNSUPPORTED400Replying on this platform is not available.
IDEMPOTENCY_MISMATCH422This Idempotency-Key was already used with a different request.
REQUEST_IN_PROGRESS409The same reply is being published right now. Retry in a few seconds.
IDEMPOTENCY_UNAVAILABLE503Requests with an Idempotency-Key are briefly unavailable. Nothing was published.
DUPLICATE_URL409This address is already subscribed in RevioReputation or by another key.
TOO_MANY_ENDPOINTS409The organization already has 10 webhook addresses.
CONFLICT409The request conflicts with the current state.
UNPROCESSABLE422The request is well formed but cannot be processed.
ERRORanyAny other refusal. Read the HTTP status and the message.
INTERNAL_ERROR500Something broke on our side. Retry, and send us the requestId if it keeps happening.

Endpoints

All paths start with https://revioreputation.com/api/v1/public.

RouteScopeWhat it does
GET/me any key The organization, key and plan behind your key. Use it to test a connection.
GET/locations locations:read Your locations.
GET/reviews reviews:read Reviews, newest first, with filters and a cursor.
GET/reviews/{id} reviews:read One review.
POST/reviews/{id}/reply reviews:reply Publish your reply on the platform.
GET/webhooks webhooks:manage Webhook addresses made with this key.
POST/webhooks webhooks:manage Subscribe an address to events.
DELETE/webhooks/{id} webhooks:manage Remove an address made with this key.
POST/webhooks/{id}/test webhooks:manage Send a ping to the address now.

Who am I: GET /me

Returns the organization, the key and the plan. Zapier and Make can use it as their connection test. The request is in the quick start above.

Locations: GET /locations

Each location comes with its city, country and the platforms connected to it.

Request
curl https://revioreputation.com/api/v1/public/locations \
  -H "Authorization: Bearer $REVIO_API_KEY"

Reviews: GET /reviews and GET /reviews/{id}

Newest first, by the time RevioReputation received them. To get the next page, send nextCursor back as cursor with the same filters. Stop when nextCursor is null.

FilterWhat it does
locationIdOne location.
platformOne platform, for example GOOGLE. All values are in the OpenAPI file.
statusnew, pending, replied, ignored or archived.
minRating, maxRating1 to 5. With either of them you only get reviews with stars, not comments.
createdAfterReceived by RevioReputation after this moment (ISO 8601).
reviewedAfterWritten by the customer after this moment (ISO 8601).
limit1 to 100, 25 if you leave it out.
cursorThe nextCursor of the previous page.
Request
# First page: the 2 newest reviews with 1 or 2 stars
curl "https://revioreputation.com/api/v1/public/reviews?maxRating=2&limit=2" \
  -H "Authorization: Bearer $REVIO_API_KEY"

# Next page: send nextCursor back as cursor, with the same filters
curl "https://revioreputation.com/api/v1/public/reviews?maxRating=2&limit=2&cursor=eyJvIjoiNmQ0YjVlMjAi..." \
  -H "Authorization: Bearer $REVIO_API_KEY"
Response
{
  "data": [
    {
      "id": "2f1c9a4e-8b7d-4c3a-9e21-5d6f7a8b9c0d",
      "locationId": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d",
      "platform": "GOOGLE",
      "contentType": "REVIEW",
      "rating": 2,
      "text": "Good paella, but we waited 40 minutes for a table on Saturday.",
      "language": "en",
      "author": { "name": "Maria G." },
      "reviewedAt": "2026-11-07T19:12:00.000Z",
      "createdAt": "2026-11-07T19:40:03.000Z",
      "status": "new",
      "reply": null,
      "url": "https://maps.google.com/?cid=1234567890"
    }
  ],
  "nextCursor": "eyJvIjoiNmQ0YjVlMjAi..."
}

reviewedAt is when the customer wrote it, createdAt is when we received it. rating is null for comments and mentions without stars. reply stays null until a reply is published, and then reply.source tells you who published it.

Polling without missing a review

If you can't receive webhooks, poll with createdAfter. A review can show up in the list a little after the time in its createdAt, so ask from a few minutes before your last check and skip the ids you already have.

Node.js 18 or newer
// Poll for new reviews. Ask from a few minutes before your last check (the overlap)
// and skip the ids you already have: a review can appear in the list shortly after its createdAt.
// Keep `seen` and `lastCheck` in your database; set lastCheck to the moment this run started.
const API = 'https://revioreputation.com/api/v1/public';
const OVERLAP_MS = 10 * 60 * 1000;

async function fetchNewReviews(lastCheck, seen) {
  const createdAfter = new Date(lastCheck.getTime() - OVERLAP_MS).toISOString();
  const fresh = [];
  let cursor = null;
  for (;;) {
    const url = new URL(`${API}/reviews`);
    url.searchParams.set('createdAfter', createdAfter);
    url.searchParams.set('limit', '100');
    if (cursor) url.searchParams.set('cursor', cursor);
    const res = await fetch(url, { headers: { Authorization: `Bearer ${process.env.REVIO_API_KEY}` } });
    if (res.status === 429) {
      const wait = Number(res.headers.get('Retry-After') || 60);
      await new Promise((resolve) => setTimeout(resolve, wait * 1000));
      continue;
    }
    if (!res.ok) {
      // A proxy in between can return an HTML page instead of our JSON error.
      const body = await res.json().catch(() => null);
      throw new Error(`RevioReputation API ${res.status}: ${body?.error?.code ?? 'no JSON error body'}`);
    }
    const page = await res.json();
    for (const review of page.data) {
      if (seen.has(review.id)) continue;
      seen.add(review.id);
      fresh.push(review);
    }
    if (!page.nextCursor) return fresh;
    cursor = page.nextCursor;
  }
}

Posting a reply

POST /reviews/{id}/reply with {"text": "..."}, up to 4,096 characters. We trim spaces at the ends and publish the rest exactly as you sent it. We don't rewrite or check your words. The response is the review with its reply, and a review.replied webhook goes out.

Request
curl -X POST https://revioreputation.com/api/v1/public/reviews/2f1c9a4e-8b7d-4c3a-9e21-5d6f7a8b9c0d/reply \
  -H "Authorization: Bearer $REVIO_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: crm-ticket-48213" \
  -d '{"text": "Thank you, Maria. You are right, the wait on Saturday was too long. We are sorry."}'

Where it can post

The API publishes on Google, Facebook and Instagram. On Facebook and Instagram that means pages already connected in RevioReputation: new Facebook and Instagram connections are not open yet, they wait for Meta to approve our app.

On TripAdvisor, Trustpilot, Yelp, Booking.com and Airbnb you paste the reply yourself. For those the API returns 409 MANUAL_POST_REQUIRED with platformUrl, the page where you paste it.

LinkedIn gets the same 409 with a link to the post, or to your LinkedIn feed when the post link is unknown. A person publishes every reply there, in RevioReputation or on LinkedIn itself.

When it refuses

  • 409 ALREADY_REPLIED: the review already has a published reply. Changing a published reply is not possible in version 1.
  • 400 INTEGRATION_REQUIRED: the location is not connected to that platform in RevioReputation.
  • 422 PLATFORM_POST_FAILED: on Google, the platform said no and nothing was published, so you can retry. On Facebook and Instagram a reply is a new comment, and a slow platform looks the same as a no. There the message is "The platform did not confirm the reply. It may already be visible there: check before posting again." A retry with the same Idempotency-Key returns this same response. Look at the post first, then send a new key only if the reply is not there.
  • 408 TIMEOUT: the platform took too long, so we don't know yet if the reply went out. Retry with the same Idempotency-Key: you get the response of the first request, never a second post.

Retrying safely: Idempotency-Key

Add an Idempotency-Key header (1 to 100 characters, for example your ticket id). We remember it for 24 hours per API key. A retry with the same key and the same body gets the first response back and never posts twice.

  • Same key, different review or text: 422 IDEMPOTENCY_MISMATCH. Use a new key for a new reply.
  • The first request is still running: 409 REQUEST_IN_PROGRESS. Retry in a few seconds.

Webhooks

Add an address in Integrations > Developers, or with the API below, and pick the events. We send each event to it as a JSON POST.

An event goes out when RevioReputation sees the review during its regular sync with the platform. That is not the moment the customer wrote it: use reviewedAt for that.

Only new reviews send review.created: written in the last 7 days, and not earlier than 24 hours before the platform was connected. Older reviews, like the ones the first import brings in, send no events. Read them with GET /reviews.

EventWhen
review.createdRevioReputation got a new review.
review.repliedA reply was published: in RevioReputation, through the API, by autopilot or on the platform itself.
review.negativeA new review with 1 or 2 stars. It comes together with review.created.
pingOnly from the "Send test event" button and POST /webhooks/{id}/test.
  • The address must be public https, on port 443, or on port 1024 or higher that is not a database port.
  • Up to 10 addresses per organization.
  • Each address gets its own signing secret (whsec_...). We show it once.

What we send

Body of a review.negative event
{
  "id": "evt_8QmZ3kLp0aBcDeFgHiJkLmNo",
  "type": "review.negative",
  "apiVersion": "2026-11-01",
  "createdAt": "2026-11-07T19:40:03.000Z",
  "organizationId": "6d4b5e20-1a2b-4c3d-8e9f-0a1b2c3d4e5f",
  "data": {
    "review": {
      "id": "2f1c9a4e-8b7d-4c3a-9e21-5d6f7a8b9c0d",
      "locationId": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d",
      "platform": "GOOGLE",
      "contentType": "REVIEW",
      "rating": 2,
      "text": "Good paella, but we waited 40 minutes for a table on Saturday.",
      "language": "en",
      "author": { "name": "Maria G." },
      "reviewedAt": "2026-11-07T19:12:00.000Z",
      "createdAt": "2026-11-07T19:40:03.000Z",
      "status": "new",
      "reply": null,
      "url": "https://maps.google.com/?cid=1234567890"
    },
    "location": { "id": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d", "name": "Casa Marisol" }
  }
}

data.review has the same fields as GET /reviews/{id}, frozen at the moment of the event. A retry sends exactly the same body.

HeaderValue
Content-Typeapplication/json
User-AgentRevioReputation-Webhooks/1.0
Revio-Event-IdThe id of the event, same as id in the body. Use it to skip repeats.
Revio-Event-TypeSame as type in the body.
Revio-Delivery-IdOne event going to one address, across its retries.
Revio-Signaturet=<unix seconds>,v1=<hex HMAC-SHA256>. How to check it is below.

How your server should respond

  • Any 2xx within 10 seconds and the delivery counts. We don't read your body and don't follow redirects.
  • 410 Gone switches the address off right away. Use it when you remove your endpoint.
  • Any other status, or no response, is retried after 1 min, 5 min, 30 min, 2 h, 6 h, 24 h. That is 7 attempts over about 33 hours. Then the delivery is marked failed.
  • An address is switched off after 20 failed attempts in a row, or when it has been failing for 3 days without a single success. The organization owner gets an email, and you can turn it back on in Integrations > Developers.

The same event can arrive more than once, so skip any Revio-Event-Id you have already handled. Events are not sent in a fixed order. If order matters to you, compare createdAt or read the review again.

Changing the secret in Integrations > Developers has no overlap: the old secret stops matching at once. Until your server has the new secret, its checks fail and the deliveries go into retries, so update both sides together.

Test it

Send test event in the tab, or POST /webhooks/{id}/test, sends a ping right away and shows the status code your server returned. A ping is not retried. One test per address every 5 seconds, 20 a minute per organization.

Managing addresses with the API

POST /webhooks subscribes an address and returns its secret, only in this response. The address belongs to the key that made it: GET, DELETE and test only see addresses made with the same key, and revoking the key removes them. Addresses added in RevioReputation are not visible to keys. Subscribing the same address again with the same key turns it back on with a new secret.

Subscribe
curl -X POST https://revioreputation.com/api/v1/public/webhooks \
  -H "Authorization: Bearer $REVIO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://example.com/revio-hook", "events": ["review.negative"], "description": "Bad reviews to the CRM"}'
Response (201)
{
  "id": "7b3e1f20-4c5d-4e6f-8a9b-0c1d2e3f4a5b",
  "url": "https://example.com/revio-hook",
  "description": "Bad reviews to the CRM",
  "events": ["review.negative"],
  "source": "api",
  "status": "ACTIVE",
  "disabledReason": null,
  "consecutiveFailures": 0,
  "lastSuccessAt": null,
  "lastFailureAt": null,
  "lastError": null,
  "createdAt": "2026-11-09T10:00:00.000Z",
  "secret": "whsec_..."
}
Send a test event
curl -X POST https://revioreputation.com/api/v1/public/webhooks/7b3e1f20-4c5d-4e6f-8a9b-0c1d2e3f4a5b/test \
  -H "Authorization: Bearer $REVIO_API_KEY"

Checking the signature

  1. Take the raw body exactly as it arrived, before any JSON parsing.
  2. Read t and v1 from the Revio-Signature header.
  3. Compute HMAC-SHA256 with your secret over t, a dot and the raw body, as hex.
  4. Accept the request if it equals a v1 value and t is within 300 seconds of your clock. The time check stops someone from replaying an old request.

Each request carries exactly one v1.

Node.js
// Node.js: check Revio-Signature against the RAW body, not the parsed JSON
const crypto = require('crypto');

function verifyRevio(rawBody, header, secret, now = Date.now() / 1000) {
  const pairs = header.split(',').map((part) => part.split('='));
  const t = Number(pairs.find(([k]) => k === 't')?.[1]);
  const signatures = pairs.filter(([k, v]) => k === 'v1' && /^[0-9a-f]{64}$/.test(v)).map(([, v]) => v);
  if (!Number.isInteger(t) || Math.abs(now - t) > 300) return false;
  const expected = crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest();
  return signatures.some((v) => crypto.timingSafeEqual(Buffer.from(v, 'hex'), expected));
}

// Express:
// app.post('/revio', express.raw({ type: 'application/json' }), (req, res) => {
//   const ok = verifyRevio(req.body.toString('utf8'), req.get('Revio-Signature') || '', process.env.REVIO_WEBHOOK_SECRET);
//   if (!ok) return res.sendStatus(400);
//   const event = JSON.parse(req.body.toString('utf8'));
//   // skip event.id if you have already handled it, then do your work
//   res.sendStatus(200);
// });
Python 3
# Python: check Revio-Signature against the RAW body, not the parsed JSON
import hashlib, hmac, re, time

def verify_revio(raw_body, header, secret, now=None):
    pairs = [part.partition("=") for part in header.split(",")]
    t = next((int(v) for k, _, v in pairs if k == "t" and v.isascii() and v.isdigit()), None)
    signatures = [v for k, _, v in pairs if k == "v1" and re.fullmatch("[0-9a-f]{64}", v)]
    now = time.time() if now is None else now
    if t is None or abs(now - t) > 300:
        return False
    expected = hmac.new(secret.encode(), f"{t}.{raw_body}".encode(), hashlib.sha256).hexdigest()
    return any(hmac.compare_digest(expected, s) for s in signatures)

# Flask:
# @app.post("/revio")
# def revio():
#     raw = request.get_data(as_text=True)  # not request.json
#     if not verify_revio(raw, request.headers.get("Revio-Signature", ""), os.environ["REVIO_WEBHOOK_SECRET"]):
#         return "", 400
#     event = json.loads(raw)
#     # skip event["id"] if you have already handled it, then do your work
#     return "", 200

Test your code with this vector

With now set to the timestamp below, your function must return true. Change one character of the body and it must return false. Our own tests run both samples above on these exact values.

Test vector
secret:    whsec_RevioTestVector0000000000000000000000
timestamp: 1762596000
header:    t=1762596000,v1=6a6659438f766fec367bfd8f2db0734d95de679cf7ca1649b2341aa571ac6814
body:      {"id":"evt_TestVector000000000000","type":"ping","apiVersion":"2026-11-01","createdAt":"2025-11-08T10:00:00.000Z","organizationId":"00000000-0000-4000-8000-000000000000","data":{"message":"Test event from RevioReputation"}}

Zapier and Make

There is no RevioReputation app in Zapier or Make yet. You can still connect both today with their own webhook tools.

Get new reviews into a Zap or a scenario

  1. Zapier: start a Zap with Webhooks by Zapier and the Catch Hook trigger. Make: start a scenario with Webhooks and Custom webhook.
  2. Copy the URL they give you and add it as an address in Integrations > Developers. Pick the events.
  3. Press Send test event to check the connection. The test is a ping: its data has only a message and no review fields.
  4. To map review fields, wait for the first real event, or send the sample body from the Webhooks section to the hook URL yourself with curl.

These steps don't check the Revio-Signature header. Treat the hook URL like a password and keep it private.

Post a reply from a Zap or a scenario

Use Webhooks by Zapier with Custom Request, or the HTTP module with Make a request in Make. Send a POST to /reviews/{id}/reply with the Authorization header and a JSON body with text, as in the curl example above.

OpenAPI file

Every route, field and error code is in one OpenAPI 3 file: /openapi/public-v1.json. We generate it from the code that serves the API. Import it into Postman, Insomnia or any OpenAPI tool.

Changelog

Every webhook body has apiVersion. If we ever change the payload in a way that breaks existing code, it gets a new date here.

DateChange
2026-11-01Version 1: reviews, locations, replies and webhooks.

Questions

Something you need that version 1 doesn't do? Write to info@revioreputation.com and tell us what you are building. If a call failed, add its requestId.

Create Your Account

7-day PRO trial, then the free plan — no credit card

or
or also

(read-only mode · changes are not saved)

Already have an account?