Pull the full history once, then keep it current with a cursor. About 30 lines of code.
Most integrations end up wanting reviews in their own store - to join against orders, run reports, or feed a dashboard. The pattern is a full backfill followed by incremental catch-up, and the only part with sharp edges is pagination.
Repuso collects customer reviews from Google, TripAdvisor, Trustpilot, the App Store and 50+ other platforms, and serves them through one REST API in a single JSON schema - with rating history, sentiment, AI summaries and webhooks on new reviews. You point it at a public profile; it keeps that profile current and tells you when something changes.
This guide assumes you already have an API key and at least one profile connected. Both take about two minutes: start with the quickstart, or get a key in the console - the trial is 10 days and needs no card.
curl -u :$REPUSO_KEY https://api.repuso.com/public/v1/profiles/getEach profile has an id, a type (google, tripadvisor, trustpilot…) and its current score and reviews count. Keep the id: it is what you filter reviews by.
The list endpoint is a cursor, not page numbers - reviews arrive continuously, so offsets would duplicate and skip rows. Ask for a page, then send back both cursor values you get:
curl -u :$REPUSO_KEY \
'https://api.repuso.com/public/v1/reviews/get?limit=100&profiles=49331'
curl -u :$REPUSO_KEY \
'https://api.repuso.com/public/v1/reviews/get?limit=100&profiles=49331&before_ts=1788825600&before_id=11109383'Every response carries what you need to decide whether to continue:
{
"count": 1284, // total matching your filters, not this page
"limit": 100,
"has_more": true, // loop while this is true
"items": [ ... ],
"next_before_ts": 1788825600,
"next_before_id": 11109383
}import requests
API = "https://api.repuso.com/public/v1"
AUTH = ("", "YOUR_API_KEY") # key goes in the password field
def fetch_all(profile_id):
params = {"limit": 100, "profiles": profile_id}
while True:
r = requests.get(f"{API}/reviews/get", params=params, auth=AUTH, timeout=30)
r.raise_for_status()
page = r.json()
yield from page["items"]
if not page.get("has_more"):
break
params["before_ts"] = page["next_before_ts"]
params["before_id"] = page["next_before_id"]
for review in fetch_all(49331):
upsert(review) # keyed on review["id"]before_id breaks ties between reviews posted in the same second. With only before_ts, rows sharing a timestamp at a page boundary get skipped silently.profiles= on page two and you start paging through everything.next_before_ts is returned on the last page too, so "loop until the cursor disappears" costs you one extra request every time. has_more is the flag to trust.After the backfill, you only want what is new. Two options, and they compose:
since - ?since=2026-09-01 returns reviews posted on or after that date. Store the newest posted_on you have seen and pass it back. Cheap, and fine on a daily cron.review.created fires as reviews land, so your table updates in seconds rather than on the next poll. See the webhook guide for the setup and signature check.Belt and braces is a webhook for freshness plus a nightly since sweep to catch anything a failed delivery missed.
id bigint primary key -- stable, use it for upserts
profile_id bigint -- which profile it came from
type text -- google, tripadvisor, ...
rating_value numeric -- normalise with rating_scale
rating_scale int -- 5 for most, 10 for some platforms
sentiment text -- positive | neutral | negative
text text
from_name text
posted_on timestamp
media jsonb -- [{type, url, src}]Ratings are not all out of five - rating_scale tells you what the platform used, so store both and divide when you report.
Every response carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset. A backfill at limit=100 rarely gets near the ceiling; if you do hit a 429, Retry-After tells you how long to wait.