# Versions and release notes

> The versions of the API and of the page component contract, and how changes are announced.

Source: /en/reference/versions/

## Versioned components

| Component | Current version | Where it appears |
|---|---|---|
| API | `v1` | At the start of the endpoint paths: `/v1/…`, `/admin/v1/…` |
| Page component contract (`data-review-*` attributes, `window.Sayproof`) | `3` | `data-review-version` on the root element and `window.Sayproof.version` |

## Versioning rules

- **Page component contract:** Adding a new attribute, method or event is a minor version change and does not affect your existing CSS rules or code. Removing an attribute or changing its meaning is a major version change; the previous version remains supported for at least one year.
- **API:** Within the current version, new fields may be added to responses, and new endpoints and new error codes may be added. Your code should ignore fields it does not recognise. Removing existing fields or changing their meaning is done in a new API version; the previous version remains supported for at least one year.
- All changes are announced on this page.

## Release notes

### 15 October 2026

- The AI package (review checks, reply drafts, topics and sentiment, translations, review summaries) and invitations by WhatsApp moved to v2; they are switched off in v1 installations. Details: [AI package](/en/features/ai.md), [Invitations on WhatsApp](/en/features/invitations.md#invitations-on-whatsapp).
- `GET /admin/v1/ai` and `POST /admin/v1/ai/requests` return `404`. `POST /admin/v1/reviews/<id>/seller-response/draft` returns `409 seller_response_refused` (`ai_package_off` or `ai_feature_closed`). In the `GET /admin/v1/insights` response, `ai` is `null`.
- The automatic checks run no AI check; `ai_review` is not in the `checks` list of the `GET /admin/v1/settings` response. `reply_tone`, `ai_translation_enabled` and `ai_summary_enabled` are saved with no effect.
- In the `GET /v1/reviews/list` response, `ai_summary` is `null` and `translations` on review rows is `{}`; reviews are not translated and no summary is prepared. The script and the server-rendered HTML do not draw the `data-review-ai-*`, `data-review-translated` and `data-review-show-original` elements.
- `deliveries` in the `GET /admin/v1/settings` response does not contain `whatsapp`; sending `whatsapp` for `invite_delivery` or `reminder_delivery` returns `400 invalid / invite_delivery_unavailable` or `reminder_delivery_unavailable`. No WhatsApp message is sent. `customer_phone` in order data is accepted; until invitations by WhatsApp arrive with v2, the phone number is not stored.
- The **Activity history** now has two tabs: **Panel actions** and **Personal data access**. Reads that show personal data (review details, review history, invitations, the message log, questions, personal data requests, users, searching reviews by email address, `POST /admin/v1/opt-outs/check`) are recorded for panel sessions, server keys and the Sayproof team; the list is read with `GET /admin/v1/access-log` (`export`). Entries cannot be changed and are deleted after 365 days. The data export also carries `personal_data_access.json`. Activity history entries carry the request ID, a keyed hash of the IP address and browser details.
- Two-step sign-in in the panel: users can turn it on for their own account. While it is on, `POST /admin/v1/session` with the right password returns `{"authenticated": false, "step": "second_step"}`; sign-in finishes with `POST /admin/v1/session/passcode` (`passcode`). A wrong code returns `400 invalid_passcode`, an expired step `400 second_step_expired`. Turning it on, confirming and turning it off: `GET/POST /admin/v1/session/second-step`, `POST …/second-step/confirm`, `POST …/second-step/off`. The session answer has a new `second_step` field. Signing in to the Sayproof team's management screen always takes two steps.
- The `POST /admin/v1/users` answer no longer has `set_password_link` or `shown_once`; it has `mailed: true`, and the email address is required (`400 invalid / email_required`). `POST /admin/v1/users/<id>/password-link` returns `202 {"mailed": true}`. Password links go only to the account owner's email. A reset link is valid for 1 hour, a new-account link for 3 days. A notice is sent when a password is set or changed and when the email address changes. New password codes: `invalid / password_guessable`, `password_names_account`. The password reset limit is counted per IP address; past the limit the answer stays `202` and no email is sent.
- A personal data deletion replaces the name and contact details in review, question and answer texts with marks such as `[email]`, `[phone]` and `[name]`; for an erased review that is not live, `available_events` no longer offers `approve`/`unhide`, the transition gets `409 transition_refused / review_anonymized`, and `POST /admin/v1/moderation/bulk` counts such reviews as `skipped`. After the deletion, `proof.order_reference` in the review detail returns `erased-order:<32 hex>`; `GET /admin/v1/brand-export` no longer lists ready exports.
- `POST /v1/ingest/orders` returns the order number you sent in `orders[].external_id`; an erased order sent again is written to the same order. `POST /v1/ingest/deliveries` accepts `detail` but does not store it. Order contact details are turned into pseudonyms when the review writing period ends, and 30 days after the order for orders that will never get an invitation; for those orders' invitations, `GET /admin/v1/send-logs` and `GET /admin/v1/invites` return an empty `recipient`.
- Access copy `export` rows gain `author_name`, `aspect_scores`, `topics` and `seller_responses`; `export_details` gains `deliveries`, `mail_events`, `questions`, `answers` and `consents`; `emails` rows carry `delivered_at` and `opened_at`. The data export package gains `questions.json`, `answers.json`, `question_events.json` and `delivery_signals.json`.
- Channels have a privacy notice link: in a panel session `PATCH /admin/v1/channels/<id>` takes a `privacy_url` field (`https`; otherwise `400 invalid / privacy_url_not_https`, `privacy_url_too_long` above 500 characters) and channel rows carry `privacy_url`. `GET /v1/reviews/list`, `GET /v1/ssr` (`state`) and `GET /v1/viewer/order` answers gain `privacy_url`, and `words` gains `privacy_notice`; the script shows the link below the forms with `data-review-privacy`. The invitation email design has a new optional field: `{{ privacy_url }}`.
- A review's photos and videos are at a public address only while the review is published. In the Admin API review details, `media[].url`, `poster_url` and `srcset` now point to `GET /admin/v1/reviews/<id>/media/<file id>/<file name>`. New refusal codes for `POST /v1/media`: `video_frame_too_large: 3840x2160`, `video_frame_rate_too_high: 60`, `video_codec_not_allowed`, `video_unreadable`, `video_stream_missing`, `image_unreadable`, `image_too_large`; `scan_failed` and `processing_failed` no longer carry extra detail.
- The body of `POST /admin/v1/audit-log/export` is now `{"format", "title", "filters", "columns", "labels", "changes"}`; `rows` is not read. The file is built on the server from the filters; the record carries the file's SHA-256 hash (`fingerprint`).
- Requests refused because of an invalid key, password or signature, an unreadable body or an unknown email service are limited to 60 per 5 minutes per IP address; above that `429 rate_limited` is returned ([Limits](/en/reference/limits.md)). Very deeply nested JSON returns `400 invalid_json` (`400 invalid / body_invalid` for transfer and email notifications) instead of `500`. A character that cannot be stored is refused with `400 invalid / field_invalid: <field>` in transfer and `400 invalid / field_invalid` in email delivery notifications.
- In email delivery notifications only an event that matches a sending adds an address to the opt-out list; email addresses in `detail` are replaced with the word `address`. Outbound requests from Sayproof (product sources, order connections, notification address) use port 443 only; an address with a port number is refused with `port_refused`.
- The review form prepares photos and videos in the customer's browser before uploading them: a photo is re-encoded as a JPEG with a long side of at most 2560 pixels (location and device information never leaves the device), and a video is converted to a silent H.264 MP4 with a short side of at most 1080 pixels and at most 30 frames per second. Each file shows "Preparing N%", then "Uploading N%" and a cancel button (`data-review-form-media-progress`, `data-review-form-media-cancel`). For this the script may load two new lazy chunks: `sayproof-media-*` (when a file is picked) and `sayproof-video-*` (only when a video is picked). If the browser cannot convert, the file is uploaded as it is when it is within the server limit; the checks on the server are unchanged. A video conversion that makes no progress for 20 seconds, or takes more than 4 minutes in total, is stopped and the file is uploaded as it is (when it is within the limit).
- Marketplace reviews: the **Integrations › Marketplace reviews** tab and the `/admin/v1/marketplace/...` endpoints (`sources`, `runs`, `check`, `imported-reviews`, `removals`; panel session only). In the storefront list every review whose source is not `own` carries a ready `origin_label`; a review imported from a marketplace has `origin: "imported"`, `origin_url`, `verified: false` and an empty `verification_method`. `aggregate` gains `imported_count`, `imported_label`, `structured_count` and `structured_average`; `verified_count` and the `verified=1` filter do not count imported reviews. The structured data and the Google product reviews file leave out imported reviews. DOM: `data-review-imported-count`; `data-review-origin` can be a link (`<a>`). The method statement gains an `imported` section. Admin API: an `imported` object in the review detail, `proof.method: source_platform`, `entry_point: import`, the removal reason `source_removed`; `review.imported` and `review.source_updated` in the history. Rows of `GET /admin/v1/marketplace/sources` carry an `attention` field (`held` or `failed` with `count`, `error` and `at`, or `null`); the attention list of `GET /admin/v1/overview` can include `marketplace_held` and `marketplace_failed`. The data export carries `marketplace_sources`, `marketplace_runs`, `marketplace_imported_reviews` and `marketplace_suppressed_reviews` (no access passwords, no temporary upload rows). Brand administrators receive an email when removals are held back and when reads fail as many times in a row as the limit in **Settings › Warning limits**; 0 turns both off.
- A single photo or video of a review can be removed without removing the review: `DELETE /admin/v1/reviews/<id>/media/<file id>` (`moderate` scope), body `{"reason"}` (`personal_data`, `profanity`, `data_subject_request`, `legal_order`). The list is `review_media_removal` in the `GET /admin/v1/reasons` response and `media_removal_reasons` in the review details. The file is deleted permanently and the response is `204`; the removal is written to the review's history and to the **Activity history**. In the panel, each file in the **Photos and videos** card has a **Remove…** button.
- An administrator can no longer change any member's email: the `email` field of `PATCH /admin/v1/users/<id>` returns `403 email_change_own_only`. Members change their own address with `POST /admin/v1/session/email-change` (`{current, email}`) after entering their current password; a wrong password returns `400 invalid / current_password`. `imported_reviews.listed` was added to the personal data access log. The `origin_url` of a marketplace review is filled only when it is on the platform's own domain. For the Sayproof team's sign-in, the setup key is renewed at every password sign-in until setup is finished; if the password changes between the password step and the code step, sign-in starts again (`second_step_expired`). The published photos and videos of a closed brand leave the public address at once and come back if the brand reopens.
- Rows of the review list (`GET /admin/v1/reviews`) and the approval queue (`GET /admin/v1/moderation/queue`) now carry `source`: `{"platform", "name"}` for a review imported from a marketplace (for example `{"platform": "trendyol", "name": "Trendyol"}`) and `null` for the others. In the panel, the marketplace name appears as a badge on the rows of **Awaiting approval** and **All reviews**; the **Came from** filter of **All reviews** moved from the **Filters** panel to the filter bar.
- The review, question and answer forms gained an optional name field and a **Hide my name on the site** box; when ticked, the name shows on the site, in structured data and in the Google product reviews file as the first letter of every word followed by `***` (`A*** K***`), and the full name stays in the panel. `POST /v1/reviews`, `POST /v1/reviews/revise`, `POST /v1/questions` and the answer endpoint accept `hide_name`; review and question details return `name_hidden`. New attributes: `data-review-form-who`, `data-review-hide-name`.

### 13 October 2026

- Text containing NUL (`\u0000`) or an unpaired surrogate now gets `400` instead of `500`: `invalid_json` in a JSON body, `invalid / invalid` in a query parameter, `invalid / <field>` in a single field.
- Phone numbers on the opt-out list are stored as a keyed hash specific to Sayproof; `GET /admin/v1/opt-outs/export` gives this value on phone rows, and it does not match SHA-256. Match your phone list with `POST /admin/v1/opt-outs/check`; it also accepts numbers written the local way (`0532 111 22 33`, `905321112233`) and returns them as you sent them.
- Rows of `GET /admin/v1/invites` and `GET /admin/v1/send-logs` carry `recipient_erased`; if the customer's personal data was erased, `recipient` is empty. The invitations' `counters.conversion` now counts sent invitations with at least one product reviewed, the same as the conversion in **Statistics**.
- `GET /admin/v1/moderation/queue` carries `counters.elsewhere` (`reviews`, `responses`): with `channel`, the number waiting for approval in the other channels.
- Questions and answers: a customer answer is accepted only with a review invitation token (a process invitation token gets `403 token_refused / token_wrong_kind`), and a cancelled or returned line gets `not_a_buyer`; questions and answers containing a link are flagged `link` and wait for approval. The answer email carries neither the asker's nor the buyer's text, and at most 5 such emails go to one address in 24 hours; the rest show in the message log with the `refused_rate_limit` result.
- All AI text (translations included) goes through the same checks; text containing a link is refused too. A failing translation is not kept, a failing summary is not published and a reply draft gets `409 seller_response_refused / ai_draft_unusable`. Process experience reviews (`experience: process`) do not go into the Google product review feed.
- When a user is added (panel session only), an existing account's old password stops working and the set-password link is emailed to the account's address (since 15 October 2026 the response does not carry the link); an account whose last membership is removed loses its password too.
- In the Admin API, on a `5xx` or unexpected error the change and its **Activity history** entry are rolled back together. `POST /admin/v1/settings` no longer overwrites a save made at the same time. `POST /admin/v1/order-sources/<id>/run` returns `202` instead of `500` while the job queue is down.
- The script closes the component without throwing on the page when a `200` response of `GET /v1/reviews/list` has no `core` or `reviews`. The `soft` design's `--review-text-muted` default is now `#7E6874`.

### 12 October 2026

- All brands sign in to the panel at one address; per-brand panel addresses were removed. The signed-in account's membership decides which brand opens; password, **Open in the panel** and support links open this address. Admin API requests sent with a key did not change. A password reset looks up the username or email across all brands and always answers `202 {"requested": true}`.
- Brand data is also kept apart at row level in the database (RLS); the behaviour seen from outside did not change. Responses carry `Content-Security-Policy` and `Permissions-Policy` headers; the API returns only JSON.
- Order verification has a new limit of 10 failed attempts per hour for the same email or order number (`429 rate_limited`). At most 18 files not yet attached to a review can be uploaded per order line (`400 refused / too_many_items: 18`); an image over 40 megapixels or not JPEG, PNG or WebP gets `processing_failed`.
- `POST /admin/v1/invite-templates` checks more of the HTML: `href`, `background`, `poster`, `srcset` and `url()` in styles, and Outlook conditional sections; a tracking image and `@import` in styles get `400 invalid / image_tracking`, `style_import`. `POST /admin/v1/terms` accepts at most 50 regular expressions per brand (`400 invalid / regex_limit: 50`).
- Questions and answers: `GET|POST /v1/questions` and `POST /v1/questions/<id>/answers` (`403 refused / questions_disabled` while off; `questions_enabled` in the list response). An `asker` token in the `GET /v1/viewer` response for signed-in customers. New `data-review-questions` elements; the SSR HTML includes this section too, and the contract version stays `3`. Panel and Admin API: `GET /admin/v1/questions`, `GET /admin/v1/questions/<id>`, `POST /admin/v1/questions/<id>/transition`, `POST /admin/v1/answers/<id>/transition`, `POST /admin/v1/questions/<id>/answer`; details in [Questions and answers](/en/features/questions.md).
- AI package: `ai_summary` in the list response, and `translations` on review rows filled with translations; the script draws the summary (`data-review-ai-summary*`) and the translation (`data-review-translated`, `data-review-show-original`). `GET /admin/v1/ai`, `POST /admin/v1/reviews/<id>/seller-response/draft`; new `GET|POST /admin/v1/settings` fields: `reply_tone`, `ai_translation_enabled`, `ai_summary_enabled`, `syndication_enabled`, `questions_enabled`, `reminder_consent`, `reminder_delivery`, `deliveries`. `GET /admin/v1/insights` carries the `ai` section and `products.fit_returns`.
- With cross-market sharing on, the list also carries proof-of-purchase reviews from your other markets (`origin: "syndicated"`, `origin_market`, `shared`); they do not count in the average or the structured data. Turning summaries or sharing on adds sections to the method statement, so `current_fingerprint` changes.
- WhatsApp: `customer_phone` was added to order data (stored in international form); a new sending route `invite_delivery: "whatsapp"`, `channel_kind: "whatsapp"` in the message log and on the opt-out list, and a new stop reason `no_phone`. `GET /admin/v1/send-logs` and `GET /admin/v1/invites` also search phone numbers.
- Google product review feed: `GET|POST /admin/v1/review-feeds` and `DELETE /admin/v1/review-feeds/<id>` (panel session only), the feed without a session at `GET /admin/v1/shared-review-feeds/<id>/<key>.xml`; details in [Google product reviews](/en/integrate/google-reviews.md). Product mapping gained the `gtin`, `mpn` and `brand` targets.
- A data protection request can also be opened with a phone number; the access copy carries the phone and the orders' phone numbers, and erasure also removes the asker's and the buyer answer's personal data. Review rows carry `flag_open`; `GET /admin/v1/policies?channel=` returns only that channel's market.

### 9 October 2026

- Each brand's panel opens only at an address that belongs to the brand and is made of random letters and digits; there is no shared sign-in address. The panel session resolves the brand from the address and does not read the `X-Sayproof-Tenant` header. Admin API requests made with a key are unchanged; the brand is resolved from the key.
- A user can belong to only one brand: a username used by another brand gets `400 invalid / username_taken`. The `account_shared` answer and the `operator` and `brand_only` fields on user rows were removed.
- Channel rows carry the identity customers see: `brand_name`, `sender_address`, `support_url`. When the channel allowance is full, creating a channel and reopening a closed one return `409 channel_quota`; the channel list carries `quota` (`limit`, `open`, `request`); a request for another channel is sent with `POST /admin/v1/channel-requests`, only in a panel session (`409 channel_request_open` while one is waiting).
- Requests for a brand whose service is frozen or closed get `403 forbidden / brand_inactive`. The data export is prepared and downloaded with `GET|POST /admin/v1/brand-export` and `GET /admin/v1/brand-export/<id>/download`; `404 not_found / export_not_ready` while it is not ready.
- `GET /admin/v1/overview`, `GET /admin/v1/metrics` and `GET /admin/v1/insights` take a `channel` parameter; the numbers then cover only that channel. `GET /admin/v1/overview` carries `sites` rows for each channel (`code`, `label`, `market`, `verdict`, `calls`, `refused`, `last_at`).
- `POST /admin/v1/audit-file` takes `channel`; the stored report and its file name carry the channel code, and the list of earlier reports can be filtered with `channel`. A `market` that does not match the channel's market gets `400 invalid / channel_market_mismatch`.
- Brands can open their own version of the market rules: `POST /admin/v1/policies` (only in a panel session, with the "Can manage market rules" permission) takes `market` or `channel`, `verification_required`, `review_window_days` (7–365), `allowed_sort_options`, `default_sort_option` and an optional `valid_from` in the body; the version affects only that brand. `GET /admin/v1/policies` rows carry `own` (`false` for a Sayproof rule) and the answer returns the limits per market in `limits`. If the Sayproof rule requires proof of purchase, the requirement cannot be turned off (`400 invalid / verification_locked`); a sort option missing from the Sayproof rule gets `400 invalid / sort_not_offered: <option>`.
- Rating topic lists belong to the brand: `GET /admin/v1/aspect-templates` rows carry `own` (`false` for a Sayproof preset list); `POST /admin/v1/aspect-templates` publishes the brand's list or a new version of it; `POST /admin/v1/aspect-templates/retire` retires only the brand's own lists (`409 refused / template_preset`).
- `POST /admin/v1/invites/retry` accepts `channel`; when given, only that channel's failed invitations are queued again.
- The response and file of `POST /admin/v1/subject-requests/<id>/copy` no longer carry the note written on the request (`note`) or the user who handled it (`handler_id`).
- With `channel`, `GET /admin/v1/overview` returns in `mode` the publishing method in force on that channel and `0` in `mode_overrides`; the method statement alert also covers only that channel. The `compliance` section of `GET /admin/v1/insights` follows `channel`; `reviews.product_bands` carries `scope` (`group` or `variant`).
- The `state` of personal data requests is `received`, `completed` or `refused`; the unused `in_progress` value was removed.

### 8 October 2026

- Five new built-in designs were added: `minimal` "Minimal", `soft` "Blush", `bold` "Sport", `natural` "Natural" and `night` "Night". They are chosen in the panel on the **Store appearance** screen; the `data-review-theme` attribute, the `theme` option, the `theme` parameter of the `GET /v1/ssr` request and the `theme` field of the list response can carry these values too. The **Night** design gives the root element its own dark background, padding and corner radius. The look of the existing three designs and the version of the page component contract (`3`) are unchanged.
- The rows of `GET /admin/v1/reviews`, `GET /admin/v1/moderation/queue`, `GET /admin/v1/reviews/<id>` and `GET /admin/v1/diagnostics` carry `product_name`.
- In the review list and the approval queue, `q` also searches the product name, the author's email address and the full review ID.
- In the `filters` declaration of list endpoints, on/off filters (`reported`, `flagged`, `refused`, `sessions`) return `advanced: true`.
- The `publication` field of `GET /admin/v1/metrics` and the `moderation` section of `GET /admin/v1/insights` also carry the `rejected` count; `auto_published` and `by_hand` now count approval decisions.
- The CSV and Excel files of `GET /admin/v1/reviews/<id>/trace` and `GET /admin/v1/audit-file/<id>` carry the **Open in the panel** address; the JSON files are unchanged.
- The order and delivery endpoints (`/v1/ingest/orders`, `/v1/ingest/deliveries`) also accept requests with an `Authorization: Bearer <server key>` header; the key must have the new `orders` (Send orders) permission. An unknown key returns `401 refused / unknown_key`, a cancelled key `401 refused / revoked_key` and a missing permission `403 refused / scope_missing`. Signed sending is unchanged.
- A server key created without `scopes` gets the `orders` and `ssr` permissions; existing keys with the `ssr` permission were given the `orders` permission.
- The `shopify` and `woocommerce` platforms were added to order connections. New `platform_refused` details: `shop_address_invalid`, `api_credentials_missing`, `api_credentials_refused`, `api_scope_missing`; new reason for an unreadable order: `lines_truncated`.
- A domain can also be verified with a `TXT` record named `_sayproof-verification.<domain>`. Domain rows and the `proof_refused` response carry the `dns_name` field; new detail `dns_value_mismatch`.

### 6 October 2026

- Exports come as CSV, Excel and JSON: `GET /admin/v1/reviews` accepts `?export=xlsx` and `?export=json`; `GET /admin/v1/opt-outs/export` accepts `?format=csv|xlsx|json`.
- `POST /admin/v1/subject-requests/<id>/copy` returns the copy as a file when the body carries `format` (`csv`, `xlsx`, `json`). `GET /admin/v1/subject-requests/<id>` returns the request record; `?format=csv|xlsx|json` downloads it as a file.
- The review file: `GET /admin/v1/reviews/<id>/trace` (a file with `?format=csv|xlsx|json`).
- The activity history does not list panel sign-ins by default; `GET /admin/v1/audit-log?sessions=1` returns them too.
- `GET /admin/v1/insights` carries a `deliveries_known` field in the invitation funnel; if no delivery information has ever come from the email service, `delivered` and its rate are `null`. The attention list of `GET /admin/v1/overview` can include the `moderation_slow` kind.

### 29 September 2026: page component contract version 3

A major version change: the default review section was redesigned. The `version` value in the SSR response also became `"3"`.

Changed:

- The content of the root element is split under `data-review-frame` into two parts: `data-review-aside` (the summary and the review writing area) and `data-review-main` (the photo strip, filters, search and sorting, the list). The review writing area is no longer shown before the sorting bar but inside `data-review-aside`, below the summary.
- Each row of the rating breakdown (`data-review-star`) carries a button with the star label, a bar and the count (`data-review-star-filter`); the old "5★ 8" text was removed.
- The "Helpful (n)" button became the "Helpful?" vote group (`data-review-vote`, `data-review-helpful`, `data-review-unhelpful`). The buttons show a thumb icon and only the number (`data-review-vote-count`); the full text ("Yes (n)", "No (m)") is in `aria-label`.
- The top row of a review became `data-review-head`: the initials (`data-review-avatar`) and the author part (`data-review-byline`). `data-review-meta` is now the line under the author name and carries the badges (`data-review-badges`) and the date (`data-review-date`); the author name is no longer in this line.
- The stars are always five characters (`★★★★★`); the filled part is shown with `data-review-fill`, and in the summary it fills to the average.
- The default values of the CSS variables became those of the **Warm** design; the values you define yourself are not affected. The default component's styles load as a separate chunk in parallel with the first list request; they are not loaded in headless use.

Added:

- Three built-in designs (`warm` "Warm", `trust` "Trust", `editorial` "Editorial") and two layouts (`cards` "Cards", `sidebar` "Side panel"). The design and layout are chosen per channel in the panel on the **Store appearance** screen; new channels use **Warm** and **Cards**. The `data-review-theme` and `data-review-style` attributes, the `theme` and `style` options and the `theme` and `style` parameters of the `GET /v1/ssr` request override this choice. The root element and the review writing area carry `data-review-theme`.
- Filtering by star, filter buttons (`data-review-filter`), search (`data-review-search`) and the results line (`data-review-results`). These use the existing `rating`, `verified`, `media`, `q` and `topic` parameters of the list endpoint.
- The photo strip (`data-review-gallery`) and the photo viewer, loaded as a separate chunk (`data-review-lightbox`, `#sayproof-style-lightbox`).
- "Read more" for long texts (`data-review-expand`).
- A colour for the initials circle chosen by the author's name (`data-review-avatar-tone`).
- The `gallery`, `media_reviews`, `style` and `theme` fields in the list response.
- The `--review-track`, `--review-verified`, `--review-summary`, `--review-summary-text`, `--review-avatar` and `--review-avatar-text` CSS variables; a layout that adapts to the component's width (container `sayproof-reviews`).
- Reviews on the order page: `GET /v1/viewer/order`, the `data-review-order` element and `window.Sayproof.mountOrder()`. On the order page, the `sayproof:submitted` event carries `detail: { order, line }`.

### 28 September 2026

- The documentation was published in Turkish and English.
- The invitation link carries the token after the `#` of the address (`#review_token=…`); the `?review_token=…` form is still read.
- On storefront endpoints, the language is determined only from `?lang=`, the `language` field of the body or the channel's language; `Accept-Language` is not used. The script's fixed `tr` fallback was removed.
- The `limits` field was added to the review list response and the `experience` field to the review row; `POST /v1/reviews/revise` accepts the `kept_media` field.
- New attributes: `data-review-experience`, `data-review-aspects`, `data-review-item-aspects`, `data-review-aspect`, `data-review-list-message`, `data-review-form-media-remove`; the `pending` value for `data-review-gate-state` and the `kept` value for `data-review-form-media-state`.
- New Admin API endpoints: `POST /admin/v1/reviews/<id>/reports/resolve`, `POST /admin/v1/opt-outs/check`, `GET /admin/v1/opt-outs/export`.

### 25 September 2026: page component contract version 2

A major version change. Version 1 was never used live in any store; every store started with this version.

Changed:

- `data-review-gate` became the CSS selector of the element in which the review writing area is shown.
- The product code and scope (`data-review-product`, `data-review-scope`) moved from the script tag to the container; the values on the script tag continue to be read as a fallback.
- The SSR root element is rendered inside the container.

Added:

- Headless use: `data-review-mode="headless"`.
- The `data-review-ready` attribute and the `window.Sayproof.write(container)` method.
- The `sayproof:submitted` event.
- Opening the order verification in the same dialog as the review form (`data-review-form-verify`).
- Rating topics with a stepped scale (`data-review-form-steps`).
- Support for signed-in customers (`data-review-viewer`, `GET /v1/viewer`).
- Withdrawing a "Helpful" vote (`vote: "none"`).
- Anonymous product view counting (`POST /v1/views`).
