A promotion ends at 07:00 UTC. Your product data returns to £100, while the page's Product JSON-LD still says £80. A comparison at that boundary can flag the stale structured price for review.
Merchant Sale Transition Audit compares the price expected from your declared feed terms with a static HTML snapshot's JSON-LD. It preserves the evaluation time, HTML hash and JSON-LD location, then compares the result with an earlier baseline. This guide uses an invented product so you can reproduce the behavior without fetching a store.
Disclosure: this is a tutorial for our own Actor, produced with AI assistance and checked against its implementation. The example is synthetic; it is not a customer case study.
What this check covers
Google asks merchants to submit a sale price that agrees with the landing page and checkout, and supports a sale-price date range. See the sale_price requirements for the full rules.
This Actor checks a narrower part of that workflow: declared feed prices against static Product/Offer JSON-LD. You supply the SKU, currency, tax basis and promotion timestamps. It does not import a Merchant Center account or infer your tax treatment.
Its interval convention is explicit: the sale starts inclusively and ends exclusively. At the end timestamp, the expected price returns to the regular price. Both timestamps must include a timezone offset. This is the Actor's comparison rule, not a claim to reproduce every Merchant Center timing behavior.
Run a small example in Apify
Open the Actor page and choose its run option. In the input editor, switch to JSON and paste this input. Check the current platform and Actor pricing shown before starting a run.
{
"feed": [
{
"sku": "DEMO-1",
"price": "100",
"currency": "GBP",
"price_basis": "tax_inclusive",
"sale_price": "80",
"sale_start": "2026-10-06T06:00:00+00:00",
"sale_end": "2026-10-06T07:00:00+00:00",
"url": "https://shop.example/product/demo-1",
"html": "\u003cscript type=\"application/ld+json\"\u003e{\"@type\":\"Product\",\"sku\":\"DEMO-1\",\"offers\":{\"@type\":\"Offer\",\"price\":\"80\",\"priceCurrency\":\"GBP\"}}\u003c/script\u003e",
"evidence_label": "synthetic_tutorial"
}
],
"mode": "simulation",
"checkedAt": "2026-10-06T07:00:00Z",
"fetchPublicUrls": false
}The \u003c and \u003e sequences are JSON escapes for angle brackets. After JSON parsing, the html field contains an ordinary JSON-LD script element.
Start the run and open its dataset results. With this input, the expected row is:
| Field | Value | Meaning |
|---|---|---|
sku | DEMO-1 | The exact Product SKU |
expected | 100 | Regular price at the exclusive sale end |
observed | 80 | Price still present in supplied JSON-LD |
status | mismatch | The declared static prices differ |
classification | unbaselined_issue | A difference with no earlier baseline |
evaluation_mode | simulation | A hypothetical evaluation, not a live observation |
pointer | 0:/offers | Script index and JSON location |
Change checkedAt to 2026-10-06T06:59:59Z and run again: the expected price is £80, so the row becomes clear. Both runs evaluate the same invented markup. They do not prove that a real page changed over time.
Apify explains how Actor runs and results work. This Actor also writes SUMMARY, BASELINE and audit.csv to its run's key-value store.
Tell a new issue from an old one
To reproduce a transition comparison, run the example at 06:59:59 UTC first. Copy the complete array from its BASELINE record. Return to the input, add that array as baseline, and restore checkedAt to 07:00:00 UTC. The stale £80 offer is now classified as new_issue relative to that earlier compatible simulation baseline.
Use the complete output records; a hand-written list of SKU and price is not a valid baseline. When the same difference persists, a compatible earlier baseline can produce existing_issue; when an earlier difference now matches, it can produce resolved. Changed product identity, feed terms or non-earlier baseline times can make a comparison incomparable.
For a real promotion release, capture fresh, authorized HTML before and after the boundary. Use mode: "observed", provide each snapshot's timezone-aware captured_at, and carry the earlier run's complete BASELINE into the later run. Omit checkedAt, or make it exactly equal the capture time. Processing an old snapshot later does not turn it into a fresh observation.
Handle incomplete evidence
Treat needs_review as an unresolved question. Missing SKU, multiple matching Offers, malformed JSON-LD, different currency and ambiguous tax evidence can prevent a price verdict. A declared variant currently matches the exact Product size value; unsupported variant identity needs review.
For owned pages with suitable public static HTML, you can enable fetchPublicUrls. Fetching is bounded to three page attempts and six HTTP requests in total, including robots checks. Redirects, browser rendering, cookies and private-network URLs are unsupported. Supplied snapshots let you inspect authorized HTML without depending on live fetch availability.
A clear row means the declared static fields agree in the stated evidence mode. Visible prices, checkout, shipping, geography, stock, customer discounts, JavaScript rendering and Google approval remain outside this check. Keep those checks in your release process, and investigate the Actor's evidence before changing product data.
Open Merchant Sale Transition Audit on Apify to try the synthetic input, then replace it with a small sample you are authorized to audit.
