Developers

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:

  • rating is 1–5. It is called rating, not score.
  • title is null on stores that have no review titles (Google Play).
  • translatedText and translatedFrom are populated only when translation is on for that channel and the review was not already English — otherwise null. The original text is always the untranslated text.
  • country is store-dependent: a storefront region for the App Store, Amazon and Microsoft Store; the detected language for Google Play.
  • date is 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’s request.json()) discard the original body. Capture the raw string first — in Next.js use await 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.

← Back to docs