Webhooks
Send every matching review to your own endpoint as signed JSON — Zapier, Make, or a service you wrote. Add a webhook channel from the Sources page; it carries the same star, topic and translation filters as every other channel. Available on all plans.
The request
Each delivery is a single POST with a JSON body:
POST https://your-endpoint.example.com/reviews Content-Type: application/json X-Reviewcast-Event: review.created X-Reviewcast-Signature: sha256=9f86d081884c7d659a2feaa0c55ad015…
Reviewcast sends its own stable schema rather than a provider-specific format, so you can parse it predictably. Field names here are public API: internal renames will not change them.
Events
review.created— a new review matched this channel’s filters.review.updated— a review already delivered here later changed its rating or text.review.rating_drop— the app’s overall rating fell past your configured threshold.
The event name appears both in the X-Reviewcast-Event header and as event in the body. Only the events you enabled on the channel are sent.
Payload
review.created:
{
"event": "review.created",
"channelId": "cmt69i95v00002dm8dlglnknj",
"channelName": "Zapier hook",
"app": { "id": "cmst7ubgd0000…", "name": "Pocket Peaks", "store": "googleplay" },
"review": {
"id": "gp-1234567890",
"store": "googleplay",
"rating": 3,
"title": null,
"text": "地図は見やすいですが、動作が少し重いです。",
"translatedText": "The map is easy to read, but the app runs a little slow.",
"translatedFrom": "JA",
"author": "fuji_hiker",
"version": "4.2.0",
"country": "ja",
"date": "2026-08-16T09:14:00.000Z"
}
}Notes on the review object:
ratingis 1–5. It is calledrating, notscore.titleisnullon stores that have no review titles (Google Play).translatedTextandtranslatedFromare populated only when translation is on for that channel and the review was not already English — otherwisenull. The originaltextis always the untranslated text.countryis store-dependent: a storefront region for the App Store, Amazon and Microsoft Store; the detected language for Google Play.dateis ISO 8601 UTC.
review.updated carries the same review object plus what changed:
"change": {
"oldScore": 1,
"newScore": 4,
"ratingChanged": true,
"textChanged": true,
"headline": "1★ → 4★, text edited"
}review.rating_drop replaces review with the drop detail:
"ratingDrop": {
"currentAvg": 3.30,
"baselineAvg": 4.65,
"drop": 1.35,
"sampleSize": 40,
"baselineSampleSize": 40,
"periodDays": 7
}Verifying the signature
Your endpoint is a public URL — anyone who learns it can POST to it. Every request carries X-Reviewcast-Signature, an HMAC-SHA256 of the exact request body keyed by the channel’s signing secret, so you can reject anything you didn’t send. The secret (whsec_…) is shown when you create the channel and stays viewable under the channel’s Edit.
import crypto from 'node:crypto';
// rawBody MUST be the unparsed body string. Re-serializing the parsed
// object produces different bytes (key order, whitespace) and the HMAC
// will never match.
function verify(rawBody, header, secret) {
const expected = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(rawBody, 'utf8')
.digest('hex');
const a = Buffer.from(expected, 'utf8');
const b = Buffer.from(header ?? '', 'utf8');
// Constant-time: a plain === leaks how many characters matched.
return a.length === b.length && crypto.timingSafeEqual(a, b);
}Two things that catch people out:
- Sign the raw bytes. Frameworks that auto-parse JSON (Express’s
json()middleware, Next’srequest.json()) discard the original body. Capture the raw string first — in Next.js useawait request.text()and parse it yourself. - Compare in constant time.
===returns early on the first differing character, which leaks enough timing information to forge a signature byte by byte.
Responding
Return 2xx as soon as you have the payload and do your real work afterwards — slow endpoints get retried. Your status code decides what happens next:
2xx— delivered, nothing further.410 Gone— the endpoint is permanently retired. Reviewcast drops the channel. Use this deliberately; it is the only response that deletes anything.- Any other status, or a network failure — treated as transient and retried on the next cycle. A one-off 404 or a deploy-window 502 will not remove your channel.
Retries and duplicates
Delivery is at-least-once. A network failure after your server has already processed a request means you will occasionally see the same event twice, so make your handler idempotent — key off review.id, which is stable for the life of the review, and ignore one you have already handled. Ordering is not guaranteed either: under retry, a review.updated can arrive before the review.created it refers to.
Testing your endpoint
Point a webhook channel at a request-inspection service (RequestBin, Beeceptor, webhook.site) to see real deliveries before you write any code. During local development your machine isn’t reachable from the internet — use a tunnel such as ngrok and register the tunnel URL.
To check that your verification actually rejects bad requests, send yourself one with a deliberately wrong signature — it should fail:
curl -X POST https://your-endpoint.example.com/reviews \
-H 'Content-Type: application/json' \
-H 'X-Reviewcast-Event: review.created' \
-H 'X-Reviewcast-Signature: sha256=deadbeef' \
-d '{"event":"review.created","forged":true}'Note that you cannot produce a mismatch by changing the secret in Reviewcast: deliveries are signed with whatever secret the channel currently holds, so both sides always agree. Forging a request is the only way to exercise the failure path.