A webhook, a signature check and a Slack message. No polling.
The most common first integration: a one-star review appears somewhere and the team hears about it in a channel rather than a week later. Repuso pushes the event; you forward it.
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 \
-X POST https://api.repuso.com/public/v1/webhooks/add \
-H 'Content-Type: application/json' \
-d '{"url": "https://yourapp.com/hooks/repuso", "events": ["review.created"]}'The response contains a signing secret. It is shown once - store it now, because you cannot read it back later.
Three events are available: review.created, rating.changed and profile.error. The last one tells you a profile stopped collecting, usually because a connection expired - worth routing somewhere too.
{
"id": "evt_90514",
"event": "review.created",
"occurred_on": "2026-09-16 14:22:00",
"data": {
"id": 11109385,
"profile_id": 49331,
"profile_name": "The Savoy",
"platform": "tripadvisor",
"rating_value": 2,
"rating_scale": 5,
"sentiment": "negative",
"text": "Room was not ready until late afternoon.",
"from_name": "Alex M.",
"posted_on": "2026-09-16 14:20:00"
}
}Every delivery carries a header:
X-Repuso-Signature: t=1789552920,v1=6f1c...c3a9v1 is an HMAC-SHA256 of t + "." + raw_body, keyed with your secret. Compute it over the raw body - parse the JSON afterwards, or a re-serialised copy will never match.
import hmac, hashlib, time
def verify(raw_body: bytes, header: str, secret: str, tolerance=300) -> bool:
parts = dict(p.split("=", 1) for p in header.split(","))
ts, sig = parts.get("t", ""), parts.get("v1", "")
if abs(time.time() - int(ts)) > tolerance: # reject replays
return False
expected = hmac.new(secret.encode(),
f"{ts}.".encode() + raw_body,
hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, sig) # constant timeUse a constant-time comparison, and reject anything whose timestamp is outside a few minutes. A plain == leaks timing information; without the timestamp check, a captured delivery can be replayed forever.
import requests
SLACK = "https://hooks.slack.com/services/T000/B000/xxxx"
STARS = lambda n, scale: "\u2b50" * round(n * 5 / scale)
def handle(event):
d = event["data"]
if d["rating_value"] / d["rating_scale"] > 0.6: # only the ones that need a human
return
requests.post(SLACK, json={"text":
f"{STARS(d['rating_value'], d['rating_scale'])} *{d['profile_name']}* "
f"({d['platform']})\n>{d['text']}\n_{d['from_name']}, {d['posted_on']}_"})Filtering server-side keeps the channel readable: a busy account gets dozens of five-star reviews a day and nobody needs a ping for each. Alert on the ones that need a reply.
curl -u :$REPUSO_KEY -X POST https://api.repuso.com/public/v1/webhooks/test/123
curl -u :$REPUSO_KEY https://api.repuso.com/public/v1/webhooks/deliveries/123The first sends a sample delivery; the second shows recent attempts with their status codes, which is the fastest way to see why a handler is failing.
Deliveries are retried three times per cycle across three cycles. After that the event is dropped, and an endpoint that keeps failing is disabled automatically - so a handler returning 500 for a week stops being called rather than being retried forever.
Two habits that avoid this: return 200 as soon as you have the payload and do the work asynchronously, and treat id as an idempotency key, since a retry after a timeout can deliver the same event twice.