Permission Requests API

Last updated: September 13, 2026

A review on your product page is one thing. Putting a customer's words or photo in a paid ad, a social post, or a marketing email needs that customer's permission. These endpoints ask a specific reviewer for that permission by email and track the answer. When the reviewer says yes, BetterReviews records a dated permission release and the review's rights_status becomes cleared. The merchant-facing walkthrough is in the Requesting permission to feature reviews guide.

The email is a fixed, non-promotional transactional message (not merchant-editable). A reviewer who has unsubscribed, bounced, or whose Shopify marketing state is an explicit no is never emailed — the request is refused at creation and re-checked at send time.

Plan availability: requires the rights_management entitlement. Current plans (Unlimited, Studio) include it. On legacy plans (no longer sold), it is available on Growth and above, and on legacy Pro and Enterprise; stores on the legacy Starter plan do not have it. A store with no active subscription — never subscribed, or cancelled — is in the free state and does not have it either. Without it every endpoint below returns 403 entitlement_required.

Preview a Request

GET /api/v1/reviews/:id/permission-preview

Checks whether the reviewer can be reached, without sending anything. Use it before request-permission to show who will be asked. It checks reachability only — the consent and suppression gates run on request-permission, so a reachable reviewer can still be refused there. Rate limit: 120 per minute per store.

Response (200)

{"reachable": true, "masked_email": "j••@example.com"}

masked_email is always masked — the full address never leaves the server. (An address that cannot be masked comes back as "".) An unreachable review (no email on the imported row and no originating review request) returns {"reachable": false, "masked_email": null}.

Errors

  • 404 review_not_found — Review doesn't exist or belongs to another store

Request Permission

POST /api/v1/reviews/:id/request-permission

Resolves the reviewer's email, applies the consent gate, creates the permission request, marks the review rights_status: "requested", and queues the email. Requires an API key with the write scope.

Request

{"scope": "both"}
FieldTypeRequiredDescription
scopestringNoWhat you want to feature: text, photo, or both (default both)

Response (201)

{"status": "sent", "permission_request_id": 91}

Errors

  • 400 invalid_scopescope is not text, photo, or both
  • 404 review_not_found — Review doesn't exist or belongs to another store
  • 422 already_requested — A request already exists for this review. A reviewer is asked at most once per review; a decline is final
  • 422 recipient_recently_asked — The same reviewer was asked about another of your reviews in the last 24 hours
  • 403 bot_key_not_allowed — Bot-scoped keys cannot use these endpoints
  • 422 no_reachable_email — The review carries no usable email
  • 422 consent_blocked — The reviewer's Shopify marketing state is UNSUBSCRIBED, REDACTED, or INVALID (never overridable)
  • 422 suppressed — The reviewer is on your store's suppression list (unsubscribed, bounced, or complained)
  • 403 — The API key lacks the write scope (body uses detail, not error)
  • 429 — More than 10 requests per minute or 200 per day for the store (body: {"detail": "Rate limit exceeded"})
  • Any other 422 {"error": ...} value means the write was rejected; treat it as not sent

List Permission Requests

GET /api/v1/reviews/permission-requests

Your store's permission requests, newest first. No recipient PII is returned.

Query parameters

ParamDescription
limitPage size (default 50, max 200)
offsetPagination offset (default 0)

Response (200)

{
  "permission_requests": [
    {
      "id": 91,
      "review_id": 4521,
      "scope": "both",
      "status": "sent",
      "recipient_source": "imported",
      "requested_at": "2026-09-13T10:12:00Z",
      "responded_at": null
    }
  ],
  "total": 1,
  "limit": 50,
  "offset": 0,
  "has_more": false
}

Status values:

  • sent — Emailed; waiting on the reviewer.
  • granted — The reviewer said yes. The review's rights_status is cleared.
  • declined — The reviewer said no. Don't feature the review; it cannot be asked again.
  • no_response — Reserved; not yet emitted. An unanswered request stays sent.

recipient_source is imported when the email came with the review (Okendo, Judge.me, Yotpo, CSV) or direct_request when it came from the BetterReviews review request that produced the review.


Example: preview, then ask

REVIEW_ID=4521

curl -s "https://api.betterreviews.app/api/v1/reviews/$REVIEW_ID/permission-preview" \
  -H "X-API-Key: YOUR_API_KEY"
# {"reachable":true,"masked_email":"j••@example.com"}

curl -s -X POST "https://api.betterreviews.app/api/v1/reviews/$REVIEW_ID/request-permission" \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"scope": "both"}'
# {"status":"sent","permission_request_id":91}

The review's rights_status field (on GET /api/v1/reviews/:id) shows none, requested, or cleared; only cleared reviews are licensed for marketing use.