Settings API

Last updated: October 9, 2026

Get Settings

GET /api/v1/store/review-settings

{
  "settings": {
    "collection_enabled": false,
    "default_experience": "chat",
    "auto_moderation_preset": "manual",
    "review_request_trigger": "fulfilled",
    "review_request_delay_days": 7,
    "review_request_delivery_fallback_days": 14,
    "request_token_expiry_days": 90,
    "digest_enabled": false,
    "digest_recipient_email": null,
    "ppo_content": {
      "banned_topics": ["price", "promotions", "shipping_delivery", "customer_service", "other_retailers"],
      "banned_terms": [],
      "enabled_widgets": ["features", "top_review", "reviews_summary", "faq", "seo_description"]
    }
  }
}

Update Settings

PUT /api/v1/store/review-settings

Pass only the settings you want to change.

{
  "settings": {
    "default_experience": "chat",
    "auto_moderation_preset": "balanced",
    "digest_enabled": true
  }
}

Six keys are NOT settable via this endpoint

collection_enabled, review_request_trigger, review_request_delay_days, require_marketing_consent, auto_recharge_acknowledged_at, and auto_recharge_acknowledged_by are rejected with HTTP 422 error: "unknown_settings_key". The first four trigger cohort cascades (Shopify pagination, audit, warmup provisioning, consent-gate cancellation) that need the dry-run-confirm flow the embedded admin extension owns. The last two are server-stamped forensic evidence that the merchant saw the auto-recharge disclosure before turning collection on — writing them from a client would let the record be pre-seeded or backdated, so they are write-rejected and read-only.

{
  "error": "unknown_settings_key",
  "key": "collection_enabled",
  "message": "`collection_enabled` cannot be set via this endpoint — use the merchant admin UI (or the admin-ext `/api/admin-ext/collection/*` routes if integrating directly)."
}

Partner-API integrations that need to flip these keys: route the flip through the admin extension on the merchant's behalf. Reading them via the GET above is fine.


GET /api/admin-ext/collection/consent-metrics (admin-ext, JWT-auth required)

Returns the cached daily-cron metric of the merchant's recent-customer marketing-consent distribution — used by the Collect → Request audience UI to render a badge next to the Require marketing consent toggle. Computed by ConsentRateMetricFanOutWorker at 04:30 UTC daily; queries Shopify's customers(first: 250, query: "created_at:>=<30d>") per store and writes the result into review_settings. The endpoint NEVER triggers a fresh fetch on read — it returns whatever the daily worker last cached.

{
  "consent_rate_pct": 23.5,
  "sample_size": 100,
  "subscribed_count": 24,
  "cached_at": "2026-06-03T04:30:00Z"
}

All four fields can be null when the worker has not yet populated the cache for the store (brand-new install, worker disabled, prior days of failures). The UI handles null as "no data yet" rather than rendering 0%.


Setting Reference

SettingTypeDefaultDescription
collection_enabledbooleanfalseRead-only via this PUT (422 on write). When true, the configured Shopify order event automatically creates review requests. Set via the merchant admin UI / admin-ext /collection/{enable,disable} only — the cohort cascade requires the dry-run-confirm flow.
default_experiencestring"chat"chat (AI-guided conversation) or form (simple star + text).
auto_moderation_presetstring"manual"Controls automatic review publishing. "manual" requires merchant approval for all reviews. Other values — "open", "relaxed", "balanced", "careful" — publish reviews that score above a preset threshold. Spam, abuse, and complaint reviews always require manual review regardless of preset. Changing this from "manual" to a non-manual preset triggers retroactive scoring of the BR-collected (platform IN ('direct','public')) pending backlog; imports are excluded.
review_request_triggerstring"fulfilled"Read-only via this PUT (422 on write). Event that starts the send countdown: "created", "paid", "fulfilled", or "delivered". Set via the merchant admin UI / admin-ext /collection/config-change only — the cascade (cancel-in-flight vs in-place reschedule) requires the dry-run-confirm flow.
review_request_delay_daysinteger7Read-only via this PUT (422 on write). Days to wait after the trigger event before sending. Set via the merchant admin UI / admin-ext /collection/config-change only.
review_request_delivery_fallback_daysinteger14Only applies when review_request_trigger = "delivered". If no carrier delivery confirmation arrives within this window, BetterReviews sends the email anyway (measured from fulfillment). Range 3–30. Default 14 balances carrier-signal latency against customer recall. Can be overridden per product — see product_timing_overrides.
show_brandingbooleantrueSet to false to hide the "Powered by BetterReviews" badge on the shopper surfaces BetterReviews renders itself — the AI chat, the step-paced flow, the simple form, the review hub and review-request emails. Only an explicit false hides it. Paid plans only: the badge is a condition of the Free plan, so on a free-plan store the key is stored but has no effect (the entitlement is checked before the setting on every surface). The storefront theme reads a different source — the reviews widget, star-rating badge, Q&A block and quote marquee read show_branding from the betterreviews.widget_settings shop metafield written by the merchant dashboard, gated by the BetterReviews-written betterreviews.plan_flags metafield — so setting this key alone does not change the storefront.
provenance_badge_labelsobject{}Map of imported platform → badge label, rendered next to (never instead of) the verified-buyer badge on reviews from that source. Keys are allowlisted to okendo, judgeme, yotpo, loox, csv, feefo — any other key (including direct/public, the platforms BetterReviews collects itself) is silently dropped, so collected reviews can never carry one. Values are trimmed and clamped to 40 characters. This key is replaced wholesale, not merged — GET the current map, modify it, and PUT the complete map; omitting a platform deletes its label, and sending only a blank value clears every label. This is provenance, not verification: it is independent of is_verified_buyer (which syndicates to Google as is_verified_buyer and Meta as has_verified_purchase), so do not use wording implying BetterReviews verified the purchase. Changing a label re-bakes cached per-product review data — usually visible within ~10 minutes, up to ~90 minutes on catalogues with tens of thousands of products.
reviewer_location_displaystring"state"Reviewer location label next to collected reviews: "off", "state" (e.g. "from Texas"; outside the US and Canada, the country name) or "city" (e.g. "from Austin, Texas" — only for reviews submitted on or after September 25, 2026; older reviews show the state). A missing key means "state"; any other value falls back to "state". The location is the city, state/province and country from the order's shipping address (never street address or postal code), so only reviews collected through BetterReviews from an order carry one — imported reviews never do. The storefront label ships in a follow-up widget release; until then the value is stored but has no visible effect. Changing the value re-bakes cached per-product review data.
product_timing_overridesobject{}Not settable via this PUT (operator-configured only). Per-product override of delay/fallback timing for long-lead-time products (made-to-order, pre-orders, subscriptions). Maps Shopify product_id → { "fallback_days": N } / { "delay_days": N } (each 1–180). When a request covers an overridden product, BetterReviews uses the longest matching value instead of the store-wide setting. Contact support to configure (no self-serve UI yet).
require_marketing_consentbooleanfalseRead-only via this PUT (422 on write). When true, review-request emails only go to customers whose Shopify emailMarketingConsent.marketingState is SUBSCRIBED. When false (default for new stores; transactional mode), BetterReviews sends to every non-suppressed customer per the spec's industry-standard treatment of non-promotional post-purchase requests. The legally explicit-no states (UNSUBSCRIBED / REDACTED / INVALID) are hard-skipped regardless of the flag. Flipping false → true cancels in-flight non-SUBSCRIBED rows with cancelled_reason="consent_gate_tightened". Set via the merchant admin UI / admin-ext /collection/config-change only — the cascade + dual audit (SettingsAuditLog + founder_audit_log require_marketing_consent_flipped) require the dry-run-confirm flow.
ppo_contentobjectall topics banned, no terms, all widgetsProduct-page (AI-optimized content) policy. See Product-page content policy.
request_token_expiry_daysinteger90How long each review-request link stays valid. Range 30–180.
send_window_local_startinteger9Earliest local hour to send review-request emails (in customer's resolved timezone). Range 0–23.
send_window_local_endinteger21Latest local hour (exclusive) to send. Range 1–24.
send_time_strategystring"even_jitter""even_jitter" spreads sends across 9–21 local with deterministic per-row jitter; "off" uses legacy time-of-trigger timing (no local-window adjustment).
digest_enabledbooleanfalseWeekly digest email with review stats.
digest_recipient_emailstringnullOverride email for digest (defaults to store owner).
auto_recharge_enabledbooleanfalseOpt in to Fair auto-recharge. On current plans, once the store passes its included monthly AI-guided reviews (1,000 on Better, 3,000 on Studio), BetterReviews adds blocks of 500 AI-guided reviews at $50 on Better or $25 on Studio; the first 50 over the included amount are free each period. On legacy metered plans, blocks follow the legacy plan: Starter $25 per 100 credits, Growth $50 per 300, Scale $100 per 1,000, with the first 10% of each period's overage free. The block tier follows the plan and cannot be set via this API. Setting to false disables auto-recharge and flips auto_recharge_state to "off"; the store then falls back to the classic review form once the included amount runs out.
monthly_spend_cap_centsinteger20000Maximum cents BetterReviews will spend on auto-recharge per billing period. Range 5000 ($50) to 100000 ($1,000). Freely settable inside this range — no Shopify approval required. The $1,000 ceiling is approved when the merchant subscribes; the merchant cap is enforced locally. The cap is evaluated per billing period and resets when the period rolls — there is no persisted "cap reached" flag to clear. A value written here is the cap the billing engine enforces, and an invalid value is rejected rather than corrected — see the behaviour-change note below before writing this key.
auto_recharge_statestring (read-only)"off"Server-managed. In practice only two values are emitted: "off" and "active". Read-only — flipped by BetterReviews based on settings + billing events. Do not branch on any other value.
auto_recharge_acknowledged_atstring (read-only)absentISO-8601 UTC timestamp, server-stamped when the merchant acknowledged the auto-recharge disclosure while turning review collection on. Absent on stores that never acknowledged. 422 on write. Not auto-recharge state — nothing branches on it; it is an audit record only.
auto_recharge_acknowledged_bystring (read-only)absentThe actor that turned collection on when the acknowledgement was stamped (e.g. "merchant:12345", "merchant:12345:cutover"). Server-derived, never accepted from a request body. 422 on write.

Behaviour change, 2026-08-21 — writes to monthly_spend_cap_cents now take effect, and invalid values are rejected instead of silently clamped. Two changes ship together. Both alter what an existing integration observes; neither is backward-compatible with code that relied on the old behaviour.

  1. The cap you set through this endpoint is now the cap that is actually billed. Previously this PUT wrote only the settings key, while both seams that enforce the cap (block reserve and charge submit) read the store's subscription snapshot and ignored it — so for any store with an active subscription, setting monthly_spend_cap_cents through the API returned 200 and changed nothing about billing. An explicitly submitted, in-range value now propagates to the enforced cap and applies from the next auto-recharge evaluation. If your integration has been writing this key expecting it to bind, it binds now; if it has been writing it as a cosmetic preference, check the value you send before your next call.
  2. Out-of-range or non-integer values return HTTP 422 error: "invalid_cap". Previously any invalid value was silently replaced with the 20000 default and the request returned 200 — so a caller sending 200000 (above the $1,000 ceiling) had the cap quietly set to $200. The value must now be an integer between 5000 ($50) and 100000 ($1,000) inclusive; anything else — a float, a numeric string, null, or an out-of-range integer — is rejected. The rejection is pre-flight, so no part of the settings PUT is applied: every other key in the same request is left unchanged too. Omit the key entirely to leave the cap as it is.
{
  "error": "invalid_cap",
  "message": "monthly_spend_cap_cents must be an integer between 5000 ($50) and 100000 ($1,000)"
}

Note on the deprecated auto_recharge_state values "cap_reached" and "shopify_rejected": both were removed on 2026-08-21 and are no longer emitted by any code path. Each was a cached copy of a negative condition that nothing ever wrote back, so it latched permanently: a store that hit its monthly spend cap once, or had one charge marked failed once, stayed on the non-AI fallback form across billing periods even after the underlying condition cleared. "shopify_rejected" was doubly wrong — it was written by a circuit breaker that matched any failed charge row, including rows created by BetterReviews' own cap-lowering and period-drift guards, so a merchant who merely lowered their own spend cap was locked out and told "Shopify declined a charge" when Shopify had never been called. Whether a store is currently at its cap is now derived live from spend-this-period vs the cap, at the moment it is needed, rather than cached in this field; the Shopify admin surfaces read it as two separate booleans, cap_reached and block_unavailable, on the internal conversation-usage contract. Callers must not treat any auto_recharge_state value as a durable "blocked" state — read "active" as "auto-recharge is currently buying blocks" and anything else as "it is not right now". Stores that latched before the removal may still return a stale string until the field is next written. (The legacy "pending_confirmation" value is likewise unreachable since auto-recharge enable became local-only in v3.)

Note on the legacy pending_overage_tier key: pre-v2, merchants picked a tier via this settings key. It has been removed — any PUT containing pending_overage_tier returns HTTP 422 with error: "pending_overage_tier_removed". Use auto_recharge_enabled instead.

Note on promotional content in email_template.*: any text field inside email_template (subject, heading, body_text, button_text, footer_text) that contains promotional phrases (discounts, percent-off offers, dollar-amount offers, coupons, promo codes, referral incentives, free-shipping mentions, loyalty/rewards framing, gift cards, or cross-promo language) is rejected with HTTP 422 error: "promotional_content_detected". Response carries key (e.g. "email_template.body_text") and phrase (e.g. "percent off") identifying the offending field and pattern. Review-request emails are sent as transactional, post-purchase emails — promotional content would void that classification under CAN-SPAM, PECR, CASL, and GDPR. Use a marketing email platform (Klaviyo, Shopify Email, etc.) for promotional sends. The same gate also fires on POST /api/v1/store/send-test-email if email_template overrides carry promotional content.

Note on cap changes: as of v3 (2026-05-09), monthly_spend_cap_cents is freely settable within $50–$1,000 with no Shopify approval required. The legacy cap_increase_requires_resubscribe error is no longer returned. Values outside that range are rejected with invalid_cap (see the behaviour-change note above), not clamped.


Product-page content policy (ppo_content)

What AI-written product-page copy (see Product Optimizations) may talk about, and which product-page blocks the store publishes.

{
  "settings": {
    "ppo_content": {
      "banned_topics": ["price", "promotions", "shipping_delivery", "customer_service", "other_retailers"],
      "banned_terms": ["bargain"],
      "enabled_widgets": ["features", "top_review", "reviews_summary", "faq"]
    }
  }
}
Sub-keyAllowed valuesDefault
banned_topicsAny of price (price, cost, value for money), promotions (sales, discounts, coupons), shipping_delivery (shipping, delivery, arrival, stock), customer_service (customer service, returns, refunds), other_retailers (Amazon, Etsy, Walmart and similar). [] turns them all off.All five
banned_termsUp to 25 words or short phrases, 2–40 characters each: letters, digits, spaces, apostrophes and hyphens. Stored trimmed and lowercased.[]
enabled_widgetsAt least one of features, top_review, reviews_summary, faq, seo_description.All five
  • Banned topics are on by default for every store. To allow a topic, PUT banned_topics without it.
  • A banned term matches as a whole word, plus simple plurals and verb endings — so order also matches "in order to".
  • A PUT replaces the whole object. A sub-key you leave out goes back to its default; it is not merged with what is stored.
  • GET returns the effective policy, defaults filled in. If you PUT that GET body back unchanged, the values are pinned: a later change to BetterReviews' defaults will not reach your store. Omit ppo_content from the PUT to stay on the defaults.
  • Invalid input is rejected, never corrected — an unknown sub-key, topic or widget, a duplicate, an empty enabled_widgets, more than 25 terms, or a term with markup or other characters returns HTTP 422 error: "invalid_ppo_content", and nothing in the request is saved.
  • Disabled widgets are not charged on regenerate, return 422 widget_disabled when named in blocks_to_optimize, and are left off the live page on publish (omitted_widgets).
  • It applies to the next generation only. Existing drafts and live pages do not change until you regenerate them. Your own edits are never filtered.

Example Configurations

The 6 protected keys (collection_enabled, review_request_trigger, review_request_delay_days, require_marketing_consent, auto_recharge_acknowledged_at, auto_recharge_acknowledged_by) are NOT shown in these examples — the first four are set through the admin UI, the last two are server-stamped. Examples below cover what's settable via this PUT.

Manual-only moderation, no digest

{
  "settings": {
    "auto_moderation_preset": "manual",
    "digest_enabled": false
  }
}

All reviews land in pending for manual approval. No weekly digest email.

Balanced auto-moderation with chat experience + digest

{
  "settings": {
    "default_experience": "chat",
    "auto_moderation_preset": "balanced",
    "digest_enabled": true
  }
}

Reviews scoring above the balanced threshold are published automatically; others require manual review. Weekly digest summarises pending + published activity.

Form experience with relaxed moderation

{
  "settings": {
    "default_experience": "form",
    "auto_moderation_preset": "relaxed",
    "review_request_delivery_fallback_days": 10,
    "digest_enabled": true
  }
}

Star + text form (no chat). Looser auto-publish threshold; tighter delivery fallback (10 instead of the 14 default). The review_request_delivery_fallback_days key IS settable through this PUT — it only affects future fallback scheduling, no cohort cascade.


Email Moderation Settings (admin extension)

Email-driven moderation lets a merchant's CS team approve / reply / archive pending reviews directly from their inbox. These settings are managed through the Shopify admin extension (App Bridge JWT), not the public API. The full endpoint reference lives in the betterreviews-api skill; the keys are summarized here for completeness.

KeyTypeDefaultDescription
email_actions_enabledbooleanfalseMaster toggle for the entire email-driven moderation surface. When false, no notification emails fire and existing email-action links won't work.
pending_notification_modestring"instant""instant" sends a notification email per pending review (deduplicated on (store_id, review_id)). This is the resting default once email_actions_enabled is on. "digest_only" silences per-review sends and is reserved for the upcoming digest worker. "off" is equivalent to flipping email_actions_enabled off.
auto_dm_from_email_actionbooleantrueWhen true, approving a review from an email action fires the merchant's standard auto-thank-you DM via Shopify Flow's review-created trigger. When false, the DM is suppressed for all email-action approvals. The rep can additionally opt out per-review via a checkbox on the email confirmation page.

Recipient management (add / revoke / resend verification / list) and the Invalidate all pending notification links safety button are also admin-extension endpoints — they don't appear on the public API. Recipients are capped at 5 per store (active + pending verification combined), require one-time email-link verification before activation, and have their emails stored encrypted at rest and masked in the admin UI.