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
| Param | Type | Description |
|---|---|---|
page | int | Zero-based page number. Default 0 |
size | int | Rows per page. Default 20, maximum 10,000 |
sort | string | field,asc or field,desc, e.g. matchScore,desc |
filters | string | URL-encoded JSON filter, same syntax as the scraped data read, e.g. {"sourceName":"shop-a"} |
view | string | long (default): one row per compared product. wide: one row per master product, with the sources side by side |
bucket | string | all (default), matched, review or no. Long view only. Any other value is refused with 400 |
loadAll | boolean | true: 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
| Bucket | Rows |
|---|---|
matched | Products linked to a master product (matchDecision = MATCH), including pairs a person approved in review |
review | Products with a likely master product that need a person to decide (matchDecision = REVIEW) |
no | Products with no fitting master product (matchDecision = NO) |
all | Everything 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
}| Field | Meaning |
|---|---|
matched / review / no | Rows in each bucket |
other | Rows in no bucket — mostly the master rows |
total | Same as totalElements of bucket=all |
activeConfigId | The matching setup of the active run. The review routes take this as configId |
activeMatcherRunId | The active run. Use it to pin a multi-page read |
reviewWorklistCount | Review pairs nobody has decided yet |
wideAvailable | Whether 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
}| Field | Meaning |
|---|---|
status | The group’s newest matching job: QUEUED, INITIAL or RUNNING (a run is going), COMPLETED, FAILED or PENDING (a saved setup that has not run) |
lastMatchedAt | When matching last finished |
hasNewData | true 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}/matchwith a Full key, the MCP toolscrapewise_update_matcher_job, or Run on the portal canvas. To run again with the same setup, read it withGET …/matchand send it back unchanged withdraft=false. - Group on the newer matching engine. You save drafts only. A save with
draft=falsefrom your key is refused with403 ENGINE_RUN_ADMIN_ONLYand 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"| Param | Description |
|---|---|
format | jsonl (default) or csv |
bucket | all (default), matched, review or no — the buckets |
| Answer | Meaning |
|---|---|
200 | The file, gzip. It belongs to the group’s current matching run |
202 | The 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 |
304 | You sent If-None-Match with the last ETag, and nothing changed |
404 | EXPORT_NO_MATCHED_RUN: the group has no finished matching run yet |
429 | EXPORT_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": "\"…\""
}| Field | Meaning |
|---|---|
ready | true: the file at url is complete and belongs to the current matching run |
state | With ready: false: QUEUED or RUNNING = a file is being built; null = none exists yet (a GET of url starts one) |
matchedAt | When the matching run of the file finished |
stale | true: 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.
Recommended daily flow
- Wait for the
FILE_READYwebhook, or poll…/latest/statusuntilreadyistrue. GET …/latestwithIf-None-Match: <the last ETag you stored>. A304means you already have it.- On
202or429, waitRetry-Afterseconds, then ask again. - 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.
| Field | Meaning |
|---|---|
| (your identity field) | On a MATCH row, the column your matching setup uses as the product identity (for example sku) holds your master SKU |
masterRef | Id of the linked master row (MATCH rows and master rows) |
reviewMasterRef | On REVIEW and NO rows: the SKU of the closest master product. Kept as a record on a pair that was approved in review |
masterModelKey | The master product model the row was compared with |
matchDecision | MATCH, REVIEW or NO |
matchScore / totalScore | Match score, 0–100 |
matchLevel | VARIANT (the exact variant matched) or MODEL (only the product model matched) |
variantAlignment | How the variant was matched: GTIN, SIZE_COLOUR, SIZE, COLOUR or MODEL_ONLY |
matchBasis | What decided a MATCH: EAN, MPN, CODE or TEXT (only when code matching is on) |
matchCode | The product code that decided the match, when matchBasis is CODE |
signals | Per-signal scores (name, brand, size, colour …) |
gatesFired | Names of the checks that fired while deciding (for example a colour or size check) |
eanVerdict | EQUAL, UNEQUAL or ABSENT — the EAN comparison |
priceSuspect | true when the price looks wrong, for example a different currency than expected for that source |
allOutOfStock | true when all the matched variants are out of stock |
v2ScraperId | The scraper that produced the row |
sourceName | The shop name, when the scraped row has one |
sourceJobFinished | When the scrape that produced the row finished |
matcherRunId | The matching run that wrote the row |
matchedAt | When the row was written |
isMaster | true on the master rows of the run |
__sourceIdentity | The 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.
GET /api/scraper/group/{id}/match/{configId}/review/ids— the pairs nobody has decided yet:{snapshotTs, ids, keys}.keysare the stable pair keys, aligned one-to-one withids. Optionalsort=total|name|photo|desc.POST /api/scraper/group/{id}/match/{configId}/review/by-idswith{"ids": [...]}(at most 500 per call) — the pair details for display.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.
verdictisapproveorreject.noteis optional, at most 2,000 characters.- The
Idempotency-Keyheader 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
- Paging and limits → REST overview
- Raw scraped rows → Scraped data
- Full reference → Reference