Webhook event catalogue

Every webhook event: when it's sent, how it's delivered, and what the payload looks like.

This page lists every event a webhook subscription can receive, when each one is sent, and a sample of what arrives at your endpoint. To get the same list from the API, call List the events you can subscribe to.

Two ways to receive events

A partner can have one immediate subscription and one batch subscription at the same time, each with its own URL and secret. You choose with delivery_kind when you create a webhook.

ImmediateBatch
What you receiveEach event as it happens, under its own nameOne cache_invalidation per location that had review activity in the window
Events coveredEvery event below except the batch eventsThe batch events
Event filter (events)Optional: pick the immediate events you want (default: all)Not used
Payload modefull (default) or minimalAlways the cache_invalidation shape
Use it toReact to things: a removal, a reply, a failed post, a disconnected siteKnow which locations to refresh: new and changed reviews

Batch events are never sent under their own name. If you subscribe to batch, you'll see cache_invalidation, never review_pulled_from_platform.

Replies posted directly on a review site

📘

response_received is sent when we find a reply to a review that we already had without one: for example, the business replied on Google, Facebook or another review site instead of through us. The reply comes in with the next review pull for that location. If that reply is later edited on the site, you get response_updated.

If a review and its reply are both new to us, they arrive together: you get the review through cache_invalidation (batch) and the reply is already on it, so response_received isn't sent.

Events

Immediate events arrive under their own name: the event field and the X-SAU-Event header carry the names below. Batch events never arrive under their own name; a batch subscription receives cache_invalidation instead.

Batch events

These events are collected into one cache_invalidation per location per window, sent to your batch subscription. They can't be picked on an immediate subscription.

EventWhen it's sent
review_pulled_from_platformA new review is collected from a connected review site.
review_campaign_collectedA review is submitted through one of your review-request campaigns.
review_updatedAn existing review's rating, text, reviewer name or date changes.
review_ingestedNot sent to your subscriptions today. It's reserved for reviews submitted to us directly.

Reviews

EventWhen it's sentExtra fields (metadata)
review_deletedA review is removed: by a partner user, or because a Google location was reconnected to a different listing. The review is kept in its history.reason, and when a user removed it, related_soft_deleted (counts of responses, ai_responses, response_revisions, flags removed with it)
platform_removal_detectedOur weekly check finds that a review is no longer on the review site.detected_via, removal_status, flag_id (if the review was flagged)
profanity_detectedNot sent to your subscriptions today. It's reserved for reviews submitted to us directly.—

Removal requests (flags)

EventWhen it's sentExtra fields (metadata)
flag_submittedSomeone submits or edits a removal request on a review.flag_id, reason, reason_identifier, flag_reason_detail, review_site
flag_submitted_to_platformThe removal request has been submitted to the review site.flag_id, previous_flag_status
flag_deniedThe review site declined the removal request and kept the review.failure_reason, previous_flag_status
flag_approved_removedA flagged review is no longer on the review site: the removal worked.flag_id, detected_via, previous_flag_status

Responses

EventWhen it's sentExtra fields (metadata)
response_receivedA reply posted directly on the review site is found on a review that had none. See above.response_id, platform, source (platform), response_date
response_draftedA response is saved on a review for the first time. Its status says whether it's waiting to post (pending) or already posted (complete).response_id, status, review_site_id, submitted_by; when an AI draft was used, ai_review_response_id, ai_edited, ai_edit_ratio, ai_generation_count
response_updatedAn existing response is changed, either through us or directly on the review site.Through us: as response_drafted. On the site: response_id, platform, source (platform), response_date, previous_response_date
response_deletedA response is deleted through us. The review goes back to having no response.response_id, prior_status, new_status, review_site_id, deleted_response_id
ai_response_generatedAn AI response is drafted for a review.ai_review_response_id, tone, response_chars, source, overage
response_submitted_to_platformA response is queued to post on the review site. For some review sites it's sent again when the site accepts the post.response_id, platform
response_post_succeededThe review site accepted the response.response_id, platform, new_status
response_post_failedPosting the response failed, for example because the review no longer exists or the business already replied.response_id, platform, new_status, reason
response_status_changedThe review site reported a response status other than posted or failed, for example still pending.response_id, platform, new_status
response_resubmittedA response is posted again: because the reviewer edited the review after the reply, or because a pending response is retried.response_id, platform

Review site connections

These events use the connection payload. They're sent when a location's review site changes between connected and disconnected, separately for collecting reviews (axis: pull) and posting responses (axis: post).

EventWhen it's sent
review_pull_connectedWe can collect reviews from the site again.
review_pull_disconnectedWe can no longer collect reviews from the site. error_code and reason say why.
response_post_connectedWe can post responses on the site again.
response_post_disconnectedWe can no longer post responses on the site. error_code and reason say why.

Plan usage

These events use the usage payload. Each is sent at most once per location, feature and threshold in a calendar month.

EventWhen it's sent
usage.threshold_warningA location has used 80% of a feature's monthly allowance (75% for allowances of 6 to 20). Not sent for allowances of 5 or fewer.
usage.limit_reachedA location has used its full monthly allowance for a feature.

Plans and billing

These events use the billing payload.

EventWhen it's sent
plan.cancelledA location's plan is cancelled. Sent once per location.
brand_plan.cancelledAn account's subscription is cancelled.
plan.changedAn account's plan is changed, or locations are added to a plan mid-cycle. Sent once per account.

Payloads

Every payload has these fields:

FieldTypeDescription
eventstringThe event name, the same as the X-SAU-Event header.
event_idstringUnique id for this event, the same as X-SAU-Delivery. Retries keep the same id, so use it to ignore duplicates.
occurred_atstringWhen it happened (ISO 8601). For cache_invalidation, the end of the window.
delivered_atstringWhen this delivery was sent (ISO 8601).
partner.idintegerYour partner id.

Payloads may carry fields not listed here. Ignore fields you don't recognise.

Review events (full)

The default for an immediate subscription. Used by the review, removal request and response events.

FieldTypeDescription
payload_modestringfull.
actor.typestringWho did it: system, partner_admin, brand_admin, or tradie for any other user (for example a location user).
actor.idinteger or nullThe user's id; null for system.
actor.namestringThe user's name, or system.
review.idintegerThe review's id.
review.external_review_idstringThe review's id on the review site.
review.store_idintegerThe location.
review.review_site_idintegerThe review site.
review.source_platformstringThe review site's name, e.g. Google.
review.rateintegerStar rating.
review.reviewerstringThe reviewer's name.
metadataobjectExtra fields for the event, listed in the tables above.
{
  "event": "response_received",
  "event_id": "01K6HG7B2C9D4E6F8G0H1J2K3M",
  "occurred_at": "2026-10-02T09:20:00+00:00",
  "delivered_at": "2026-10-02T09:20:02+00:00",
  "partner": {
    "id": 4521
  },
  "payload_mode": "full",
  "actor": {
    "type": "system",
    "id": null,
    "name": "system"
  },
  "review": {
    "id": 279309,
    "external_review_id": "AbFvOqm1x7Kc",
    "store_id": 3007,
    "review_site_id": 44,
    "source_platform": "Google",
    "rate": 4,
    "reviewer": "Jordan P."
  },
  "metadata": {
    "response_id": 88412,
    "platform": "google",
    "source": "platform",
    "response_date": "2026-10-02 08:55:13"
  }
}

Review events (minimal)

Sent instead when the subscription's payload_mode is minimal: just enough to know what to refetch.

{
  "event": "response_received",
  "event_id": "01K6HG7B2C9D4E6F8G0H1J2K3M",
  "occurred_at": "2026-10-02T09:20:00+00:00",
  "delivered_at": "2026-10-02T09:20:02+00:00",
  "partner": {
    "id": 4521
  },
  "payload_mode": "minimal",
  "review_history_id": 1184522,
  "review": {
    "id": 279309
  }
}

cache_invalidation

Sent to a batch subscription: one per location that had new or changed reviews in the window. Refetch that location's reviews with Get reviews.

FieldTypeDescription
store.idintegerThe location to refresh.
window.startstringStart of the window (ISO 8601).
window.endstringEnd of the window (ISO 8601).
window.interval_hoursintegerThe subscription's batch_interval_hours.

Windows line up with your interval from midnight UTC. A 6-hour subscription, for example, covers 00:00–06:00, 06:00–12:00 and so on. Locations with no review activity in a window get nothing.

{
  "event": "cache_invalidation",
  "event_id": "3F9A1C0B7E2D4A6F8B1C3D5E7F",
  "occurred_at": "2026-10-02T12:00:00+00:00",
  "delivered_at": "2026-10-02T12:00:41+00:00",
  "partner": {
    "id": 4521
  },
  "store": {
    "id": 3007
  },
  "window": {
    "start": "2026-10-02T06:00:00+00:00",
    "end": "2026-10-02T12:00:00+00:00",
    "interval_hours": 6
  }
}

Connection events

Always sent in full, whatever the payload mode.

FieldTypeDescription
store.idintegerThe location.
review_site.store_review_site_idintegerThe location's connection to the site.
review_site.review_site_idintegerThe review site.
review_site.namestringThe review site's name.
review_site.connection_statusstringThe connection's status when this was sent.
review_site.disconnect_reasonstring or nullWhy it's disconnected, in plain words.
axisstringpull (collecting reviews) or post (posting responses).
error_codestring or nullA stable code on the disconnected events, e.g. E-001 (no URL set up), E-002 (sign-in expired).
reasonstring or nullWhy it's disconnected: a short code such as no_url, or a sentence you can show to the business. null on the connected events.
{
  "event": "review_pull_disconnected",
  "event_id": "01K6HH0A1B2C3D4E5F6G7H8J9K",
  "occurred_at": "2026-10-02T10:02:11+00:00",
  "delivered_at": "2026-10-02T10:02:13+00:00",
  "partner": {
    "id": 4521
  },
  "store": {
    "id": 3007
  },
  "review_site": {
    "store_review_site_id": 55102,
    "review_site_id": 44,
    "name": "Google",
    "connection_status": "Disconnected",
    "disconnect_reason": "Google connection has expired. Please reconnect this location."
  },
  "axis": "pull",
  "error_code": "E-002",
  "reason": "Google connection has expired. Please reconnect this location."
}

Usage events

FieldTypeDescription
data.company_idintegerYour partner id.
data.store_idintegerThe location.
data.featurestringThe plan feature, e.g. response_posting, ai_response.
data.threshold_pctinteger80 or 75 for a warning, 100 when the limit is reached.
data.period.year, data.period.monthintegerThe month the allowance belongs to.
data.units_consumedintegerWarnings at 80%: units used so far.
data.units_remainingintegerWarnings: units left this month.
data.projected_days_remaininginteger or nullWarnings at 80%: estimated days until the allowance runs out.
data.messagestringLimit reached: a short message.

In minimal mode, only the common fields and payload_mode are sent.

{
  "event": "usage.threshold_warning",
  "event_id": "01K6HH4Q8R2S6T0V4W8X2Y6Z0A",
  "occurred_at": "2026-10-02T11:30:00+00:00",
  "delivered_at": "2026-10-02T11:30:02+00:00",
  "partner": {
    "id": 4521
  },
  "payload_mode": "full",
  "data": {
    "company_id": 4521,
    "store_id": 3007,
    "feature": "response_posting",
    "threshold_pct": 80,
    "period": {
      "year": 2026,
      "month": 10
    },
    "units_consumed": 40,
    "units_remaining": 10,
    "projected_days_remaining": 7
  }
}

Billing events

data carries names and dates, not ids.

Eventdata fields
plan.cancelledstore_name, brand_name, partner_name, plan_name, plan_start_date, cancelled_date, cancelling_reason, cancelling_reason_other
brand_plan.cancelledcompany_type (Brand or Partner), company_name, partner_name, total_locations, cancelled_date, cancelling_reason, cancelling_reason_other
plan.changedbrand_name, partner_name, total_locations, plan_name, plan_start_date, plan_end_date

In minimal mode, data is left out.

{
  "event": "plan.cancelled",
  "event_id": "01K6HJ1M3N5P7Q9R1S3T5V7W9X",
  "occurred_at": "2026-10-02T13:05:44+00:00",
  "delivered_at": "2026-10-02T13:05:46+00:00",
  "partner": {
    "id": 4521
  },
  "payload_mode": "full",
  "data": {
    "store_name": "Acme Coffee - Circular Quay",
    "brand_name": "Acme Coffee",
    "partner_name": "Acme Partners",
    "plan_name": "Growth",
    "plan_start_date": "2026-01-15",
    "cancelled_date": "2026-10-02",
    "cancelling_reason": "too_expensive",
    "cancelling_reason_other": null
  }
}

Test event

Send a test event delivers webhook.test with no review attached, so you can check your endpoint and signature.

{
  "event": "webhook.test",
  "event_id": "01K6HJ9Z7Y5X3W1V9T7S5R3Q1P",
  "occurred_at": "2026-10-02T14:00:00+00:00",
  "delivered_at": "2026-10-02T14:00:00+00:00",
  "partner": {
    "id": 4521
  },
  "payload_mode": "full",
  "actor": null,
  "review": null,
  "metadata": []
}

Receiving a delivery

Every delivery is a POST with a JSON body and these headers:

HeaderValue
Content-Typeapplication/json
X-SAU-EventThe event name.
X-SAU-DeliveryThe event_id. The same on every retry.
X-SAU-Signaturesha256= followed by the HMAC-SHA256 of the raw request body, in lowercase hex, using your subscription's secret.
X-SAU-Attempt1, 2 or 3.

Verify the signature against the raw body, before parsing it:

import hmac, hashlib

expected = "sha256=" + hmac.new(SECRET.encode(), raw_body, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, request.headers["X-SAU-Signature"]):
    reject()

Reply with any 2xx status within 10 seconds. Anything else, or no answer, counts as a failure.

Retries

  • A failed delivery is retried twice: 5 minutes after the first attempt, then 30 minutes after the second.
  • Retries carry the same event_id. Use it to process each event only once.
  • After 20 failed deliveries in a row, the subscription is switched off. Fix your endpoint, then turn it back on with Update a webhook (is_active: true).
  • Events aren't guaranteed to arrive in order. Use occurred_at to order them.
  • See recent deliveries shows each attempt and the response your endpoint gave.

Did this page help you?