Skip to Content
REST APIMatched data

Matched data

When a group has a matching setup and a matching run has finished, every scraped product is compared with your master list (your own catalogue). The result is the group’s matched results: one row per compared product, with the decision, the score and the master product it belongs to.

These routes read those results. All of them read only your own groups, and always the group’s newest finished matching run (the “active run”).

Read matched rows — GET /api/scraper/group/{id}/match/data

curl -H "Authorization: Bearer $KEY" \ "https://portal.scrapewise.ai/api/scraper-api/api/scraper/group/grp_xyz/match/data?bucket=matched&page=0&size=1000"

Query params

ParamTypeDescription
pageintZero-based page number. Default 0
sizeintRows per page. Default 20, maximum 10,000
sortstringfield,asc or field,desc, e.g. matchScore,desc
filtersstringURL-encoded JSON filter, same syntax as the scraped data read, e.g. {"sourceName":"shop-a"}
viewstringlong (default): one row per compared product. wide: one row per master product, with the sources side by side
bucketstringall (default), matched, review or no. Long view only. Any other value is refused with 400
loadAllbooleantrue: return the whole bucket in one response, up to 5,000 rows. Long view only

A filters value that cannot be read is ignored on this route, and you get the unfiltered page. Check your JSON when the result looks too big.

Buckets

BucketRows
matchedProducts linked to a master product (matchDecision = MATCH), including pairs a person approved in review
reviewProducts with a likely master product that need a person to decide (matchDecision = REVIEW)
noProducts with no fitting master product (matchDecision = NO)
allEverything above, plus the master rows of the run (isMaster = true)

Response — long view

A page envelope: content, totalElements, totalPages, number, size. Each row is the scraped product’s own columns plus the matching fields listed in Matched row fields.

{ "content": [ { "title": "Rain jacket M blue", "price": 89.9, "sku": "RJ-100-M-BLU", "masterRef": "66f0a1...", "matchDecision": "MATCH", "matchScore": 94.5, "matchLevel": "VARIANT", "sourceName": "shop-a", "matcherRunId": "3f2c...", "matchedAt": "2026-10-19T06:12:03Z" } ], "totalElements": 8120, "totalPages": 9, "number": 0, "size": 1000 }

With loadAll=true the response is content, totalElements (the true size of the bucket), cap (5,000), targetField, matchedFields and masterRows. When totalElements is larger than the rows in content, the bucket was over the cap — page through it instead.

Response — wide view

view=wide returns {view, building, stale, unavailable, columns, content, totalElements, totalPages, number, size}. The wide view holds matched products only. building: true means the wide table is being prepared for the first time — ask again in a few seconds. Very large groups may not have a wide view; match/buckets tells you with wideAvailable.

Paging through a big result

See Pagination. To keep a multi-page read on one run, read activeMatcherRunId from match/buckets and add "matcherRunId":"<that id>" to filters on every page. If a new run becomes active mid-read, pages come back empty instead of mixing two runs — start again.

Errors — 400 (group not found, unknown bucket, bucket with view=wide) / 401 / 403 (N/A) / 404 (N/A — 400 used) / 429 / 500. A group with no finished matching run returns an empty page.

Bucket counts — GET /api/scraper/group/{id}/match/buckets

Counts of each bucket of the active run, and the active run’s ids.

curl -H "Authorization: Bearer $KEY" \ "https://portal.scrapewise.ai/api/scraper-api/api/scraper/group/grp_xyz/match/buckets"
{ "matched": 8120, "review": 640, "no": 1210, "other": 5000, "total": 14970, "activeConfigId": "cfg_...", "activeMatcherRunId": "3f2c...", "reviewWorklistCount": 512, "wideAvailable": true }
FieldMeaning
matched / review / noRows in each bucket
otherRows in no bucket — mostly the master rows
totalSame as totalElements of bucket=all
activeConfigIdThe matching setup of the active run. The review routes take this as configId
activeMatcherRunIdThe active run. Use it to pin a multi-page read
reviewWorklistCountReview pairs nobody has decided yet
wideAvailableWhether the wide view can be shown. null when it cannot be told

Counts are cached for about 15 seconds. All ids are null and all counts 0 when the group has no finished run.

Errors — 400 (group not found) / 401 / 403 (N/A) / 404 (N/A — 400 used) / 429 / 500.

Matching status — GET /api/scraper/group/{id}/match/config-status

Is matching running, when did it last finish, and is there new scraped data since then. Read-only.

curl -H "Authorization: Bearer $KEY" \ "https://portal.scrapewise.ai/api/scraper-api/api/scraper/group/grp_xyz/match/config-status"
{ "hasMatcherJob": true, "status": "COMPLETED", "lastMatchedAt": "2026-10-19T06:12:03Z", "hasNewData": false, "canConfigure": true, "engineManaged": true, "engineManagedCanEdit": false }
FieldMeaning
statusThe group’s newest matching job: QUEUED, INITIAL or RUNNING (a run is going), COMPLETED, FAILED or PENDING (a saved setup that has not run)
lastMatchedAtWhen matching last finished
hasNewDatatrue when a scrape has finished after the last matching run — the matched results do not include it yet

Errors — 400 (group not found) / 401 / 403 (N/A) / 404 (N/A — 400 used) / 409 (TAKE_OVER_ACTIVE, admin keys only) / 429 / 500.

Matching history — GET /api/scraper/group/{id}/match/history

The group’s matching runs and saved setups, newest first. Each row carries the setup that run used.

curl -H "Authorization: Bearer $KEY" \ "https://portal.scrapewise.ai/api/scraper-api/api/scraper/group/grp_xyz/match/history"

Each item has id, status, masterData, scope, created, expectedRowCount (matched rows of a finished run), failureReason and configId. Add withDeleted=true to also list runs hidden by “delete all matching data”.

Errors — 400 (group not found) / 401 / 403 (N/A) / 404 (N/A — 400 used) / 409 (TAKE_OVER_ACTIVE, admin keys only) / 429 / 500.

Start a matching run by hand

You can start a match yourself, outside any schedule.

  • Classic group. Save the setup with draft=false: PUT /api/scraper/group/{id}/match with a Full key, the MCP tool scrapewise_update_matcher_job, or Run on the portal canvas. To run again with the same setup, read it with GET …/match and send it back unchanged with draft=false.
  • Group on the newer matching engine. You save drafts only. A save with draft=false from your key is refused with 403 ENGINE_RUN_ADMIN_ONLY and nothing runs. The runs of such a group start after new scrapes, on its schedule, or are started by Scrapewise.
  • A Read key cannot start a run.

A group’s matching, classic or on the newer matching engine, can also run by itself: see Run matching on a schedule.

Daily matched file — GET /api/scraper/group/{id}/match/export/latest

One fixed address per group that always gives the newest file of all matched rows: gzip JSON Lines or CSV. Use it for a daily import. Full and Read keys both work. No token, no Excel row limit.

curl -H "Authorization: Bearer $KEY" -o matched.jsonl.gz \ "https://portal.scrapewise.ai/api/scraper-api/api/scraper/group/grp_xyz/match/export/latest?format=jsonl&bucket=matched"
ParamDescription
formatjsonl (default) or csv
bucketall (default), matched, review or no — the buckets
AnswerMeaning
200The file, gzip. It belongs to the group’s current matching run
202The file is being built. The body is {jobId, state, queuePosition, pollSeconds}. Wait Retry-After seconds and ask again. A GET starts a build when none runs
304You sent If-None-Match with the last ETag, and nothing changed
404EXPORT_NO_MATCHED_RUN: the group has no finished matching run yet
429EXPORT_DOWNLOAD_BUSY: too many of your downloads run at once. Wait Retry-After and retry

Errors — 400 (INVALID_EXPORT_REQUEST: unknown format; an unknown bucket) / 401 / 402 (plan without data export) / 404 (EXPORT_NO_MATCHED_RUN) / 429 (EXPORT_DOWNLOAD_BUSY) / 500.

Headers of a 200: ETag, X-Matcher-Run-Id, X-Matched-At, X-Export-Job-Id, X-Refresh-Stamp, X-Verdict-Epoch, X-Reviewed-Source. A HEAD request sends the headers only and never starts a build.

Is the file ready? — GET /api/scraper/group/{id}/match/export/latest/status

Read only and free. Never starts a build and never sends the file. Same format and bucket params. Also the MCP tool scrapewise_get_matched_export_latest_status.

{ "ready": true, "state": "READY", "matcherRunId": "3f2c...", "matchedAt": "2026-10-23T05:12:03Z", "refreshStamp": null, "stale": false, "bytes": 1840221, "rows": 8120, "builtAt": "2026-10-23T05:14:40Z", "format": "jsonl", "bucket": "matched", "url": "https://portal.scrapewise.ai/api/scraper-api/api/scraper/group/grp_xyz/match/export/latest?format=jsonl&bucket=matched", "etag": "\"…\"" }
FieldMeaning
readytrue: the file at url is complete and belongs to the current matching run
stateWith ready: false: QUEUED or RUNNING = a file is being built; null = none exists yet (a GET of url starts one)
matchedAtWhen the matching run of the file finished
staletrue: a newer file is on its way. The current file stays downloadable — do not stop your import on stale

What is in the file

Every row has your ID column (the one your matching setup uses, for example sku), then matchDecision (MATCH, REVIEW or NO), matchScore, reviewed, reviewVerdict (approve, reject or empty), pricesRefreshedAt, priceRefreshedAt, priceSourceJobId, matcherRunId, then the row’s own columns. JSON Lines: the first line is {"_meta": …} with the run id; then one object per row. CSV: comma, UTF-8 without BOM.

The file name is <group>-matched-<bucket>-<runId>-<time>.jsonl.gz (or .csv.gz).

Keep a daily file ready — PUT /api/scraper/group/{id}/match/export/auto

curl -X PUT -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \ "https://portal.scrapewise.ai/api/scraper-api/api/scraper/group/grp_xyz/match/export/auto" \ -d '{"enabled":true,"formats":["jsonl"],"bucket":"matched"}'

With this on, Scrapewise builds a new file after each matching run and after review decisions, so your GET gets a 200 at once. Off by default. Full key only; a Read key is refused with 403. GET …/match/export/auto reads the setting.

For a one-off file with your own filters or columns, use the export jobs flow.

  1. Wait for the FILE_READY webhook, or poll …/latest/status until ready is true.
  2. GET …/latest with If-None-Match: <the last ETag you stored>. A 304 means you already have it.
  3. On 202 or 429, wait Retry-After seconds, then ask again.
  4. Do not start every import exactly at 00:00. Spread the times.

Matched row fields

Every matched row keeps the scraped product’s own columns and adds these fields. They are written by the current matching engine. Rows of older runs may lack some of them.

FieldMeaning
(your identity field)On a MATCH row, the column your matching setup uses as the product identity (for example sku) holds your master SKU
masterRefId of the linked master row (MATCH rows and master rows)
reviewMasterRefOn REVIEW and NO rows: the SKU of the closest master product. Kept as a record on a pair that was approved in review
masterModelKeyThe master product model the row was compared with
matchDecisionMATCH, REVIEW or NO
matchScore / totalScoreMatch score, 0–100
matchLevelVARIANT (the exact variant matched) or MODEL (only the product model matched)
variantAlignmentHow the variant was matched: GTIN, SIZE_COLOUR, SIZE, COLOUR or MODEL_ONLY
matchBasisWhat decided a MATCH: EAN, MPN, CODE or TEXT (only when code matching is on)
matchCodeThe product code that decided the match, when matchBasis is CODE
signalsPer-signal scores (name, brand, size, colour …)
gatesFiredNames of the checks that fired while deciding (for example a colour or size check)
eanVerdictEQUAL, UNEQUAL or ABSENT — the EAN comparison
priceSuspecttrue when the price looks wrong, for example a different currency than expected for that source
allOutOfStocktrue when all the matched variants are out of stock
v2ScraperIdThe scraper that produced the row
sourceNameThe shop name, when the scraped row has one
sourceJobFinishedWhen the scrape that produced the row finished
matcherRunIdThe matching run that wrote the row
matchedAtWhen the row was written
isMastertrue on the master rows of the run
__sourceIdentityThe row’s own identity value, as scraped

Review verdicts — POST /api/scraper/group/{id}/match/{configId}/review/verdicts

Approve or reject REVIEW pairs. An approved pair moves to the matched bucket. Needs the text-matching plan feature. configId is activeConfigId from match/buckets.

  1. GET /api/scraper/group/{id}/match/{configId}/review/ids — the pairs nobody has decided yet: {snapshotTs, ids, keys}. keys are the stable pair keys, aligned one-to-one with ids. Optional sort=total|name|photo|desc.
  2. POST /api/scraper/group/{id}/match/{configId}/review/by-ids with {"ids": [...]} (at most 500 per call) — the pair details for display.
  3. POST …/review/verdicts — send the decisions.
POST /api/scraper/group/grp_xyz/match/cfg_123/review/verdicts Authorization: Bearer <key> Idempotency-Key: <uuid> Content-Type: application/json { "snapshotTs": 1760854323000, "rows": [ { "pairKey": "<key from review/ids>", "verdict": "approve" }, { "pairKey": "<another key>", "verdict": "reject", "note": "different colour" } ] }
  • At most 200 rows per call. Send larger sets in chunks.
  • verdict is approve or reject. note is optional, at most 2,000 characters.
  • The Idempotency-Key header is required. Send the same key only when you retry the same call.
  • Sending a verdict again for the same pair is safe — it is counted as accepted.

Response (200) — {"accepted": 2, "rejected": 0}.

Errors — 400 (group not found, no rows, more than 200 rows, IDEMPOTENCY_KEY_REQUIRED) / 401 / 402 (plan lacks text matching) / 403 (a Read key) / 404 (N/A — 400 used) / 409 (IDEMPOTENCY_IN_FLIGHT, ARGS_HASH_MISMATCH) / 429 / 500.

Your decisions are kept on the next run

A decision is saved for the pair, not only on today’s rows. On every new matching run of the group, an approved pair is matched again and a rejected pair is not suggested again. After you decide pairs, the group’s Match quality numbers are checked again by themselves; the old numbers stay on screen until the new ones are ready.

Undo a decision — POST /api/scraper/group/{id}/match/{configId}/review/unmatch

Unmatch one pair a person approved: its row goes back to REVIEW, the link to your product is removed, and the saved decision is deleted. The pair is back in Review.

POST /api/scraper/group/grp_xyz/match/cfg_123/review/unmatch Authorization: Bearer <key> Idempotency-Key: <uuid> Content-Type: application/json { "stablePairKey": "<key from review/ids>" }

Instead of stablePairKey you may send the pair as scraperId (the source), scopeRowIdentity (the source row’s identity) and masterSku (your SKU). Send one form, not both.

Response (200) — {"reverted": 1, "verdictsDeleted": 1, "configs": 1}.

  • A pair with no saved decision is refused with 400: a pair matched by the engine, or a pair you already unmatched (a repeat).
  • While a matching run of the group is going, the call answers 409. Try again when it has finished.

Undo all decisions — POST /api/scraper/group/{id}/match/review/unmatch-all

Unmatches every pair a person approved in the group, in all setups. The answer counts them, with configs = the setups touched. If some pairs fail, call it again: it finishes the rest. 409 while a run is going.

In the portal, Unmatch is on each approved row of Results (long view), and Unmatch all reviewed… is in the Danger zone below History.

Both calls need a Full key and an Idempotency-Key header. A Read key is refused with 403.

What’s next