Groups
A group is the level-1 organisational unit: every scraper belongs to exactly one group, and the group’s dataTable field names the MongoDB collection where its scrapers’ rows land. Use groups to:
- Apply uniform scheduling to a set of scrapers (
startType=MANUAL,DAILY,WEEKLY,NONE). - Share a coherent dataset between customers (Shared groups).
- Match your list against the other scrapers of the group (Product matching) and merge data between them (Merge jobs).
- Share one list of post-process rules across all scrapers in the group (Rules for all scrapers in a group).
- Let AI check and repair the group’s scrapers on a schedule (AI control).
Endpoint summary
| Method | Path | Operation ID | Auth scope |
|---|---|---|---|
| GET | /api/scraper/group/list | scrapewise_get_scraper_group_list | bearer |
| PUT | /api/scraper/group | scrapewise_create_scraper_group | bearer + idempotency-key |
| PUT | /api/scraper/group/{id}/start-type/{startType} | scrapewise_update_scraper_group_start_type | bearer + plan feature + idempotency-key |
| PUT | /api/scraper/group/{id}/match | scrapewise_update_matcher_job (also MCP) | bearer + TEXT_MATCHING + idempotency-key |
| GET | /api/scraper/group/{id}/match | scrapewise_get_matcher_job (also MCP) | bearer |
| POST | /api/scraper/group/{id}/preview-delete | scrapewise_delete_scraper_group_preview | bearer |
| DELETE | /api/scraper/group/{id} | scrapewise_delete_scraper_group | bearer + idempotency-key |
| GET | /api/scraper/group/{id}/post-process-rules | scrapewise_get_scraper_group_post_process_rules | bearer |
| PUT | /api/scraper/group/{id}/post-process-rules | scrapewise_update_scraper_group_post_process_rules | bearer + idempotency-key |
| PUT | /api/scraper/group/{id}/ai-control | scrapewise_update_scraper_group_ai_control | bearer + idempotency-key |
| GET | /api/scraper/group/{id}/ai-control/status | scrapewise_get_scraper_group_ai_control_status | bearer |
List groups — GET /api/scraper/group/list
curl -H "Authorization: Bearer $KEY" \
https://portal.scrapewise.ai/api/scraper-api/api/scraper/group/listReturns the groups you OWN. For groups shared TO you, use GET /api/scraper/shared/group/list.
Response (200) — List<GroupDTO> with id, name, dataTable name, startType, schedule metadata, member-scraper count. Empty list if you have no groups.
Errors — 400 (N/A) / 401 / 403 (N/A) / 404 (N/A) / 429 / 500.
Create or update a group — PUT /api/scraper/group
PUT /api/scraper/group
Authorization: Bearer <key>
Idempotency-Key: <uuid>
Content-Type: application/json
{
"id": null,
"name": "competitor-prices",
"dataTable": "competitor_prices",
"startType": "DAILY",
"shouldMatchProducts": false
}Upsert semantics: omit id to create; set id to update. Creating a new group is plan-feature gated by MAX_GROUPS; updating is not.
Response (200) — the persisted GroupDTO including the assigned id and any server-stamped fields.
Errors — 400 (validation) / 401 / 402 (MAX_GROUPS hit on create) / 403 (N/A) / 404 (N/A) / 429 / 500.
Set group-wide start type — PUT /api/scraper/group/{id}/start-type/{startType}
PUT /api/scraper/group/5f9a.../start-type/DAILY
Authorization: Bearer <key>
Idempotency-Key: <uuid>Updates EVERY scraper in the group to share the same StartType. Values: MANUAL, DAILY, WEEKLY, NONE (paused).
Each non-NONE value is plan-feature gated:
MANUAL→MANUAL_RUNDAILY→DAILY_SCHEDULERWEEKLY→WEEKLY_SCHEDULER
Response (200) — List<StartTypeForGroupDTO> — per-scraper records reflecting the new state.
Errors — 400 (group not found) / 401 / 402 (plan lacks the chosen feature) / 403 (N/A) / 404 (N/A — 400 used instead) / 429 / 500.
Product matching — PUT /api/scraper/group/{id}/match
Matching links rows of your list (the main scraper of the group) to rows of the sources you compare it with. It works the same for any kind of data: products, leads or locations. You say which column of your list is compared with which column of each source. Scrapewise saves that setup and runs the matching in the background.
Many of these routes are also MCP tools, for example scrapewise_get_matcher_job, scrapewise_update_matcher_job, scrapewise_get_matcher_setup_view, scrapewise_get_matcher_config_status and scrapewise_get_matcher_history — see Matching setups for the full list. Make matching setups in the portal (the match canvas), with this API or with those tools. A setup made here is drawn on the portal canvas by itself — see How the portal draws your setup.
| Method | Path | Operation ID | Auth scope |
|---|---|---|---|
| PUT | /api/scraper/group/{id}/match | scrapewise_update_matcher_job | bearer + TEXT_MATCHING + idempotency-key |
| GET | /api/scraper/group/{id}/match | scrapewise_get_matcher_job | bearer |
| GET | /api/scraper/group/{id}/match/fields | scrapewise_get_matcher_fields | bearer |
| GET | /api/scraper/group/{id}/match/setup | scrapewise_get_matcher_setup_view | bearer |
| PATCH | /api/scraper/group/{id}/match/schedule | scrapewise_update_matcher_schedule | bearer + idempotency-key |
Save or run a setup
PUT /api/scraper/group/5f9a.../match
Authorization: Bearer <key>
Idempotency-Key: <uuid>
Content-Type: application/json
{
"masterData": {
"jobId": "65b0...",
"targetField": "sku",
"matchers": [
{ "field": "name", "threshold": 80 },
{ "field": "brand", "threshold": 90, "kind": "exact" }
],
"matchersByScope": {
"65c1...": [
{ "field": "name", "scopeField": "title", "threshold": 75 }
]
}
},
"scope": ["65c1...", "65c2...", "65c3..."],
"draft": false
}| Field | Meaning |
|---|---|
masterData.jobId | A run of your list (the main scraper). |
scope | Runs of the sources to compare with your list. Each must be a run of a scraper in this group. |
masterData.targetField | The ID column (★) that names a match, for example sku for products or regCode for leads. Required when there are lines. |
masterData.matchers | Lines for every source. A source in scope with no list of its own uses these lines. field is the column of your list; the source column has the same name. |
masterData.matchersByScope | Lines for one source, keyed by that source’s run id in scope. When a source has its own list, the lines for every source do not apply to it. scopeField names the source column when it differs from field. |
threshold | 0–100: how alike the two values must be for this line. |
kind | Optional: exact, fuzzy or number. A number line compares numbers (for example a price) with a tolerance. |
masterData.reviewMaxByScope / reviewFloorByScope | Optional. Own review levels for one source, keyed by its run id. |
masterData.targetFieldByScope | Optional. Own ID column for one source. |
draft | true saves the setup without running it. false saves and runs. A save with an empty scope is always a draft. |
Who may save. The group’s owner saves the setup with a Full key or in the portal. On a group on the newer matching engine the owner saves drafts of the text settings (lines, levels); the runs start after new scrapes or are started by Scrapewise, and picture settings are kept by Scrapewise. See Start a matching run by hand.
A per-source map that is left out of the body keeps what is stored. An empty map {} clears it.
Response (200) — the saved setup (MatcherJobDTO) with its id and status. Matching runs in the background. Read the result in the portal or with GET …/match.
Errors
| Code | Meaning |
|---|---|
| 400 | The body is not valid, for example a scope run that is not in this group, a blank column name, or lines without targetField. The message says what to fix. |
| 400 | Group on the newer matching engine: a change of the ID column or of the main list. Contact support with the group id. |
| 401 | Missing or invalid bearer |
| 402 | Plan lacks TEXT_MATCHING |
| 403 | A Read key. Or, on a group on the newer matching engine: ENGINE_RUN_ADMIN_ONLY (you sent draft=false; send draft=true), FIELD_ADMIN_ONLY (a picture line or picture setting differs from the saved one). |
| 404 | N/A (400 used instead) |
| 409 | Matching of this group is queued, running or being cleaned up, or the setup changed since you read it. Nothing was changed. Try again when it finishes, or read again first. |
| 423 | A group on the newer matching engine before its first run. Nothing was changed. |
| 429 | Rate-limited |
| 500 | Unexpected error |
Read a setup — GET /api/scraper/group/{id}/match
Returns the saved setup of the group in the same shape as the PUT body. 404 when the group has no matching setup yet.
Read the setup as the canvas draws it — GET /api/scraper/group/{id}/match/setup
Read only (operation ID and MCP tool scrapewise_get_matcher_setup_view). Returns the matching setup of the group as it is now, for the same setup GET …/match returns.
| Answer | Meaning |
|---|---|
200 { "view": true, "managed": true, "flags": …, "jobId", "builtAt", "upToDate", … } | Scrapewise sets up the matching of this group. The body has the full setup the next run uses: every line per source, the enrichment joins, pictures, the order steps and the group rules (column names and roles only). upToDate: false means a change is still being applied, usually within a minute. |
200 { "view": false, "managed": true, "flags": … } | Same kind of group, but the setup is still being prepared (for example right after a save). Ask again in a minute. |
200 { "view": false, "managed": false, "flags": … } | The canvas is drawn from your saved setup (GET …/match) alone. |
204 | Matching has not reported yet. Ask again later. |
404 | The group is not yours. |
flags says which matching switches are on for the group (number lines, pictures). It is null when the matching service has not reported for more than 5 minutes; the portal then makes no claim about them.
Run matching on a schedule — PATCH /api/scraper/group/{id}/match/schedule
Sets when the group’s matching runs again by itself, for example every Monday at 06:00. It works for classic groups and for groups on the newer matching engine. Also the MCP tool scrapewise_update_matcher_schedule. Full key only; a Read key is refused with 403.
PATCH /api/scraper/group/5f9a.../match/schedule
Authorization: Bearer <key>
Idempotency-Key: <uuid>
Content-Type: application/json
{ "type": "WEEKLY", "daysOfWeek": ["MONDAY"], "time": "06:00", "zone": "Europe/Tallinn" }type | Runs |
|---|---|
AFTER_SCRAPES | After each new scrape of the group |
DAILY | Every day at time |
EVERY_N_DAYS | Every intervalDays days (1–366) at time |
WEEKLY | On the daysOfWeek (at least one, for example ["MONDAY"]) at time |
timeisHH:mm(24 h).zoneis a time zone name; defaultEurope/Tallinn.- Send the body
nullto clear the schedule: matching then runs only when you start it. - A new or changed schedule starts from now. Missed earlier times are not run.
- The
PATCHnever starts a run and does not change the setup or its results. - The answer is the setup with
schedule,lastScheduledRun,nextRunAt(the next time it runs) and a newversion.
Errors — 400 (a bad schedule field; the message names it) / 401 / 403 (a Read key, or a plan without matching) / 404 (the group has no matching setup yet) / 409 (the setup was replaced meanwhile — read it again and retry with a new key) / 429 / 500. Nothing was changed in each.
How the portal draws your setup
Every matching setup is drawn on the match canvas in the portal, whoever made it: the canvas itself, this API, or Scrapewise for a group whose matching it sets up. Enrichment saved with PUT …/merge (also the MCP tool scrapewise_run_scraper_data_group) is drawn too. You never need to paste or copy anything for the lines to appear.
An open canvas picks up a change made elsewhere by itself, within about a minute. If you have unsaved edits on the canvas, nothing is overwritten: the note “This setup was changed elsewhere.” appears with a Reload button. Saving while the note shows asks first: “This setup was changed elsewhere. Saving replaces it. Save anyway?”
| On the canvas (legend) | What it shows |
|---|---|
| Your lines — solid black line | A line from matchers or matchersByScope. A line in matchers is drawn to every source that has no list of its own. |
| Set for the group — black dotted line | A line Scrapewise sets for the whole group. A line you draw for the same column replaces it. |
| Price and stock checks | Price, currency and stock columns. They are compared only to check a match: a very different price lowers the score; stock decides between variants. |
| Own setting for this source — orange dashed line | A line whose setting for one source differs from the rest. |
Number compared — line with a # mark | A kind: "number" line. |
| Pictures compared — line with a picture mark | Picture matching between the two picture columns. |
| Not used here — pale line | The group setup compares another column for this source, so this line is not used. |
| Adds data by key — grey line with key marks | Enrichment: a source that adds data to another source, joined by a key column. “joined by a rule” or “joined by N keys” when the key is not one plain column. |
| Fills a column — grey dotted line | Which column the enrichment fills. Shown only when you select a box that adds data. |
- Why each box is there. Every box has one short label: Your list, Compared, Adds data · key …, Not used or In this group only.
- All / Matching / Enrichment. A switch above the canvas shows all lines, only the matching lines, or only the enrichment lines. Each choice shows how many lines it has, for example All (12). It only changes the view, never what is saved.
- Full screen. The canvas fills the screen when it opens, and boxes never overlap: the compared sources stack next to your list. Full screen shows the canvas on the whole page; press Esc or Exit full screen to leave.
- Order used in this group. A strip shows the steps 1 Codes → 2 Text → 3 Pictures, each with On or Off (or on for some sources). Step 1 decides first. The order is fixed; only the on/off switches differ per group or source. Each matching line shows the number of its step; price and stock checks belong to step 2. Click a step to see how it is set up. On older setups the strip shows one step, All lines together, and each line keeps its threshold label.
- Columns that only carry a value get no line. For example the column that groups rows into one product: its hover says “This column groups your file’s rows into one product. It is not compared.”
- Rules for the whole group. A panel above the canvas lists the settings that are not lines: levels, how many days of source data are used (Uses source data from the last N days), codes, text weights, pictures and number lines. A source with its own setting shows it on its box, for example Codes: sku or Needs a code to match.
- Pictures and number lines. They are drawn as full lines. When they are switched off for the group, the line keeps its style and gets a small off badge; hover it to see why.
- Skipped sources. A source that the latest matching run could not use says why on its box, for example No finished scrape yet (in the latest run). The label is about the latest run only and goes away after the next run that can use the source.
- Columns not in the latest scrape. When a line needs a column the box’s latest scrape does not have, the box shows it as an extra row so the line is still drawn: Built during matching (Scrapewise builds the column while matching) or Not in the latest scrape (the setup uses it, but the latest scrape of this source does not have it).
- Move the boxes. Drag a box to where you want it. The canvas saves the spot for this group, so the box is in the same place after a reload and on your other devices (spots belong to your account). A new source gets a free spot and never covers a box you placed. Reset layout (shown once you moved a box) puts every box back where the canvas places it. If a spot cannot be saved, it stays in this browser and the toolbar shows Layout not saved — retry.
- Only connected rows. This switch in the toolbar hides the rows of each box that have no line in the current view. A box with no line left shows No lines in this view. The switch is remembered in this browser and only changes the view.
- Show one type of line. The legend items are filters. Click one, for example Price and stock checks, to see only those lines. Click more items to add them; click an active item to remove it. Show all brings every line back. Own setting for this source marks a setting, not a type of line, so it is not a filter. The filter is for this visit only and never changes what is saved. With Only connected rows on, rows of hidden lines are hidden too.
- Lines you can add. In edit mode, a list under the canvas shows the sources that have no lines of their own yet but could get useful ones, with the lines each would get (for example
category → title). A source is listed only when it has a finished scrape that has the column and the line would change its matching. Add lines adds them to the canvas; + key line sku → sku means the click also adds the source’s key line (★). More than 5 sources: Show all N. The list is hidden when no source qualifies. - Unsaved lines and Undo. Lines added with Add lines are not saved yet. Each shows Unsaved · Undo on the line, and the bar above Save shows Unsaved changes with Undo (removes the lines you added last). Undo removes every line of that click. It is not offered when you drew more lines on the same source afterwards; remove those by hand. Nothing is stored until you press Save or Run.
- The key line (★) stays while a source has other lines. Deleting it then is refused with Mark another key line first. Mark another line as the key, or delete the other lines first. A source’s only line can always be deleted.
- Run and Stop. While a matching run of the group is waiting or running, the Run button shows Running and a Stop button sits next to it. Stop asks Stop this matching run?; Stop run stops it.
Matching in the portal: the tabs
The group’s Matching page has up to four tabs: History · Match the data · Results · Match quality.
- Match the data (the canvas) is always there.
- History and Results appear after the group’s first finished run. Match quality appears when that run was checked.
- While a run is going, the tabs of the last finished run stay. Until the first run has finished, stop a run with Stop next to Run on the canvas.
- Opening Matching shows Results when it is there, else Match the data. After Run you land on History when it is there.
History
One table, one row per run, newest first:
| Column | What it shows |
|---|---|
| Run | When the run finished (or started), for example 14 Oct 16:24. The run whose results you see has an Active chip; click that row to open its setup on the canvas. |
| Status | Completed, Failed (the reason on hover), Stopped, Running, Queued, Draft or Held. A waiting or running row has Stop. |
| Sources compared | How many sources the run compared with your list. |
| Items in scope | Rows of the compared sources. |
| Matched | Source rows that got a match. |
| Your products matched | Your products with at least one match. |
| Waiting for review | Pairs left for a person to decide. |
| Match rate | Matched out of items in scope. |
| Pictures | On when the run compared pictures. Shown only when a run on the page did. |
A column that is empty on every row of the page is left out. Tick two rows and press Compare 2 runs to compare them. Show deleted runs also lists deleted runs. The Danger zone below the table deletes matching data.
Results
The matched rows of the active run, in three pills with their counts: Matched, No match and Review. The Review count is the number of pairs waiting for review. When the side-by-side (wide) view cannot be built for the run, Results opens straight in the long view (one row per match). Rank by (Total, Name, Photo, Description) orders the pairs; Show system fields shows the matcher’s own columns.
Match quality
Cards with the numbers of the active run. A card is shown only when the run has a real number for it:
| Card | What it counts |
|---|---|
| Your products with a competitor price | Your products that got at least one competitor match. |
| Competitor items matched | Matched rows, of N in K compared sources, with a bar for the match rate. |
| Waiting for review | Pairs left to decide. Review opens the Review pill in Results. |
| Are the matches right? · Did we find all of them? · How much could we check? | Only when the run could be checked against known codes. |
The line above the cards says when the run ran and which days the data was scraped. By source lists every compared source: Items in scope, Matched, Waiting for review and Match rate (more columns when the run has those numbers). Problems in this run, How right is each way of matching?, Right item, right variant? and Settings used for this run stay closed until you open them.
Held. On a group on the newer matching engine, a run whose result looks wrong in size (for example far fewer or far more rows than the results you have now) is held: it is not delivered. History shows Held · N vs M rows · previous results kept, and your current results stay as they are. When nothing was delivered before, it shows Held · N rows · no results yet. Over the API, match/history shows such a run with status FAILED and a guardHold block (code, rows, deliveredRows).
The Review count in Results is live: it goes down while people decide pairs. After you decide pairs, Match quality checks the run again by itself; the old numbers stay on screen until the new ones are ready.
Review
Review shows one pair per card: your item on the left, the source item on the right, each with its picture.
- Fields chooses which values show under both pictures. The list has four groups: Both sides (columns that exist on both sides, by the same name or paired by your matching lines, shown as
title ↔ name, plus photo description, the text read from each photo), Left only, Right only, and Technical last (scores, status, ids). A Both sides field sits on the same line under both pictures, so the values are side by side; a missing value shows —. Nothing technical is chosen for you. The choice is saved per group. - Pairs narrows the review to some source pairs, each shown as your list ↔ the source with the number of pairs to decide, for example Products ↔ Shop A · 1,234. Pick one or more; the list and its count follow. The filter runs on the server, so it works for any number of pairs. Show all clears it. Saved per group. The control is hidden when the review has only one source.
- Rank by and Pairs cannot be changed while you have answers that are not sent yet.
Delete a group (destructive — two-call protocol) — DELETE /api/scraper/group/{id}
Destructive operation. Deleting a group cascades to delete every scraper inside it. Optionally also drops the entire MongoDB data collection (withData=true) — all historical scraped rows are lost.
ADR-012 two-call pattern: first preview, then commit within 5 minutes.
Steps
POST /api/scraper/group/{id}/preview-delete[?withData=true]— mints a 5-minute token + preview summary.DELETE /api/scraper/group/{id}[?withData=true]— commits the delete (idempotency-key required).
Skipping the preview step deletes rows without confirmation.
Step 1 — Preview
POST /api/scraper/group/5f9a.../preview-delete?withData=true
Authorization: Bearer <key>Response (200)
{
"token": "9b2d-...",
"opName": "scrapewise_delete_scraper_group",
"targetEntityId": "5f9a...",
"previewSummary": {
"entityName": "5f9a...",
"entityType": "scraper_group",
"cascadeCounts": {},
"warnings": [
"withData=true: the group's entire MongoDB data collection will be dropped (all historical scraped rows lost, irreversible)."
]
}
}When withData=true, the warning array surfaces the irreversible-data-loss notice — the user must see this before committing.
Step 2 — Commit
DELETE /api/scraper/group/5f9a...?withData=true
Authorization: Bearer <key>
Idempotency-Key: <uuid>withData query param semantics:
false(default) → drops the group + all its scrapers; preserves the MongoDB data collection.true→ also drops the data collection (irreversible).
Response — 204 No Content.
Errors (both steps)
| Code | Meaning |
|---|---|
| 400 | Group doesn’t exist for this customer |
| 401 | Missing/invalid bearer |
| 403 | N/A |
| 404 | N/A (400 used instead) |
| 429 | Rate-limited |
| 500 | Persistence failure mid-delete |
Rules for all scrapers in a group
A group can have one shared list of post-process rules. The list runs on every scraper in the group, so you write a rule once instead of copying it into each scraper. In the portal: group card → ”…” → Rules for all scrapers.
Group rules use the same rule kinds as scraper rules (CURRENCY_CONVERT, ENUM_MAP, REGEX_CLEAN, QUANTITY_NORMALIZE, NUMBER_EXTRACT — see Scrapers → per-kind reference). On top of that, a group rule can have conditions and can sit in a choice group.
Order: scraper rules first, then group rules
For each row:
- The scraper’s own
postProcessRulesrun first, top to bottom, as before. - Then the group rules run, top to bottom.
A group rule may read a column that a scraper rule wrote (for example price or pack_grams). A group rule may read the output of an earlier group rule, with the same chaining limits as scraper rules (Rule order and chaining). A scraper rule must not read a group rule’s output — it does not exist yet when scraper rules run.
Each row gets the group rules once, even on run paths that save rows in two steps.
Scraper override
If a scraper has its own rule for the same outputField, the scraper’s rule wins on all rows of that scraper. The group rule does not run for that scraper.
If a scraper overrides any rule inside a choice group, the whole choice group is skipped for that scraper. That scraper then owns that calculation.
The GET response shows, per scraper in scrapers, which outputs it overrides (overriddenOutputs) and which choice groups it owns (ownedSets).
Conditions (“only when …”)
A group rule can have up to 3 conditions. All must be true (AND). If they are not all true, the rule does not run on that row and its output is left out of the row.
op | In the portal | True when |
|---|---|---|
equals | is | the value equals value |
in | is one of | the value equals one of values (1–50 items) |
contains | contains | the value contains value (plain text, not a pattern) |
notBlank | has a value | the value is not empty |
blank | is empty | the value is missing, null, "" or only spaces |
- Comparisons ignore upper and lower case, and leading and trailing spaces.
- Numbers compare in plain form:
50.0is"50",0.1is"0.1".true/falsecompare as text. - A list or object value only counts for
notBlank. valueis 1–200 characters; each item ofvaluestoo. No regex, no expressions, no OR, no NOT.fieldfollows the column name rules: not blank, no.or$, and it does not start with_sw_orctx_.
{ "field": "product_type", "op": "equals", "value": "yarn" }
{ "field": "product_type", "op": "in", "values": ["yarn", "mixed"] }
{ "field": "name", "op": "contains", "value": "mat" }
{ "field": "pack_grams", "op": "notBlank" }
{ "field": "pack_grams", "op": "blank" }Where the value comes from: the row’s column at the moment the group rule runs, if it is not empty. If the row has no value there, the scraper’s group value of the same name is used (see below). If neither exists, the value is missing.
Conditions exist only on group rules. A scraper rule with conditions, branchSet or branch is refused with 400.
Choice groups (branch sets)
Sometimes one calculation has several versions, and each row must use exactly one. For example: price per 50 g for yarn, per 100 g for fillings, per piece for mats.
Put those rules in one choice group: give each rule a branchSet (the group’s name, e.g. basis) and a branch (the choice, e.g. per_50g).
branchSetandbranchare both set or both empty. Each is 1–50 characters.- Every rule in a choice group has at least 1 condition. Rules with the same
branchSetandbranchhave identical conditions (else 400). - A choice group has 2–10 choices. A rule list has at most 10 choice groups.
Choices may share an output column. Rules in different choices of the same choice group may write the same outputField, so every row gets its result in one column whatever choice it took. Example: per_50g, per_100g and per_piece all write unit_price, unit_price_eur and unit (a fixed value, for example 50 g, 100 g or piece).
- All writers of a shared column are in the same choice group, each in a different choice, with the same
kindandoutputType(and, forCURRENCY_CONVERT, the sameto). Any other repeatedoutputField→ 400, as before. - A rule inside a choice reads a shared column only after a rule of its own choice wrote it. A rule outside the choice group reads it only after all its writers (400 otherwise).
{ "kind": "QUANTITY_NORMALIZE", "sourceField": "price", "outputField": "unit_price", "outputType": "NUMBER",
"params": { "quantityField": "pack_grams", "targetQuantity": 50 },
"branchSet": "basis", "branch": "per_50g",
"conditions": [{ "field": "product_type", "op": "equals", "value": "yarn" }] }
{ "kind": "QUANTITY_NORMALIZE", "sourceField": "price", "outputField": "unit_price", "outputType": "NUMBER",
"params": { "quantityField": "pack_pieces", "targetQuantity": 1 },
"branchSet": "basis", "branch": "per_piece",
"conditions": [{ "field": "product_type", "op": "equals", "value": "accessory" }] }To test one rule of a choice group with POST /api/scraper/preview-rule, remove conditions, branchSet and branch first. The preview refuses them with 400. It never evaluates conditions.
For each row, the platform counts the choices whose conditions are true:
| Choices that match | Result |
|---|---|
| exactly 1 | the rules of that choice run |
| 0 | none run; no output of the choice group is written; the row gets RULE_NO_BRANCH:<set> |
| 2 or more | none run; no output of the choice group is written (also no shared column); the row gets RULE_MULTI_BRANCH:<set> |
There is never a default choice. The platform does not guess. Write the conditions so that the choices exclude each other.
Group values per scraper (labels)
Some scrapers cannot carry a fixed column (for example AI scrapers). So the rule list can store group values: short labels per scraper, kept on the group, not on the scraper. Example: product_type = yarn on one scraper, accessory on another.
scraperValuesmaps scraper id → name → value, for example"6a01...": { "product_type": "yarn" }.- At most 5 names per group. Each value is 1–50 characters.
- Group values are only read by conditions. They are never written into the rows.
- A row column with the same name wins when the row has a value (see “Where the value comes from”). This lets an uploaded file carry the label per row.
- A fallback scraper without its own value uses the value of its main scraper.
- When a scraper leaves the group, its values are dropped. The GET does not return them, and the next PUT drops them with a
warningsentry.
Row safety
Group rules never overwrite your data and never write a guessed value.
- An input column (
sourceField,quantityFieldorfromField) is missing, null or blank → the rule is skipped on that row. Its output is left out. ForCURRENCY_CONVERT, the shared<to>_rate/<to>_rate_datepair stays only if another conversion to that currency gave a value on the row (see Rate columns). - The row already has a column with the rule’s output name (from the page or the file) → the rule is skipped and the existing column is kept as it is.
- A group
ENUM_MAPwith a missing source writes nothing (no default). - A row that already has its own
eur_rate/eur_rate_date(scraped, API or uploaded file) keeps it in both the scraper and the group phase. A scraperCURRENCY_CONVERTrule still writes its value, and a different computed rate addsRULE_UNUSABLE:eur_rate. A groupCURRENCY_CONVERTrule to that currency writes nothing on that row and addsRULE_UNUSABLE:<output>, the same as when its output name is already a column. overwritesRawFieldcannot be used on group rules (400).- Rows that failed to scrape (
_sw_statusother thanOK) get no group rules.
Row markers
When a group rule cannot give a value where one is expected, the row gets a marker in its _sw_warnings column (see Scraped data). Most markers come from group rules; the two rate markers come from any CURRENCY_CONVERT rule (scraper or group).
| Marker | Meaning |
|---|---|
RULE_NO_BRANCH:<set> | No choice of this choice group matched the row. |
RULE_MULTI_BRANCH:<set> | More than one choice matched the row. |
RULE_NO_INPUT:<output> | The rule’s conditions were true, but its quantityField or fromField column is missing. |
RULE_UNUSABLE:<output> | The conditions were true and the input is there, but it cannot be used (divisor 0 or not a number, unknown currency), or the output name (for CURRENCY_CONVERT also its <to>_rate / <to>_rate_date pair) is already a column in the row. |
RULE_UNUSABLE:<to>_rate | The row already had its own <to>_rate / <to>_rate_date (e.g. eur_rate), and the rate the platform computed is different. The row’s own column is kept; the converted value is still written (scraper rules; a group rule writes nothing, see above). |
RULE_RATE_CONFLICT:<to>_rate | Two conversions to the same currency on this row needed different rates (for example different source currencies). Both converted values are written; <to>_rate and <to>_rate_date are left empty. |
A missing sourceField gives no marker. Example: a product with no sale_price simply gets no sale_price_… columns.
Uploaded files
Group rules also run on uploaded files (File scrapers): on a replace import and on the new rows of an append import. So an uploaded customer file can get the same shared columns (for example price_eur) as the scraped rows. File rows get the same markers.
- Rows that an append import fills in (rows matched to an existing row) get no rules. The import summary says how many rows that was.
- A file column with the same name as a group rule’s output is kept; the rule skips it with
RULE_UNUSABLE. - Numbers stored as text in a CSV (for example
"16.95") are read as numbers.
When changes apply
A run reads the group rules once, when it starts. A change applies to runs and samples that start after the save. A run that is already going keeps the rules it started with. Samples use the group rules too, so a sample shows the same columns as a run.
Group rules are separate from the scraper’s own settings. Changing them does not restart a resumable run.
Recommended price-compare layout
A typical group that compares competitor prices needs only this rule list. Every scraper writes the same input
columns (price, sale_price, currency, pack_grams or pack_pieces); the group adds the rest.
- Outside any choice:
CURRENCY_CONVERTprice → price_eurandsale_price → sale_price_eur, both with"fromField": "currency". - One choice group
basis, one choice per unit (per_50g,per_100g,per_piece). Each choice has 5 rules that write the same columns:QUANTITY_NORMALIZEprice → unit_priceandsale_price → sale_unit_price,CURRENCY_CONVERTunit_price → unit_price_eurandsale_unit_price → sale_unit_price_eur, and anENUM_MAPfixed valueprice → unit(50 g,100 gorpiece). - The fixed value is written only where its
sourceFieldis filled. Read it from a column every row has (price), never from a label: labels are not in the row. - Each scraper gets a label in
scraperValues(for exampleproduct_type:yarn,accessoryormixed). Labels only steer the conditions; they are never written to rows. To show the kind in the data, add anENUM_MAPfixed value inside each choice. - Conditions that exclude each other:
per_50g=product_typein [yarn, mixed] ANDpack_gramsnotBlank;per_100g=product_typeequals accessory ANDpack_gramsnotBlank;per_piece=product_typein [accessory, mixed] ANDpack_piecesnotBlank ANDpack_gramsblank. A row with no pack size getsRULE_NO_BRANCH:basisand no unit price; that is expected, not an error.
The per_50g choice in full (the other two choices differ only in branch, conditions, quantityField,
targetQuantity and the unit default):
[
{ "kind": "QUANTITY_NORMALIZE", "sourceField": "price", "outputField": "unit_price", "outputType": "NUMBER",
"params": { "quantityField": "pack_grams", "targetQuantity": 50 },
"branchSet": "basis", "branch": "per_50g",
"conditions": [{ "field": "product_type", "op": "in", "values": ["yarn", "mixed"] },
{ "field": "pack_grams", "op": "notBlank" }] },
{ "kind": "QUANTITY_NORMALIZE", "sourceField": "sale_price", "outputField": "sale_unit_price", "outputType": "NUMBER",
"params": { "quantityField": "pack_grams", "targetQuantity": 50 },
"branchSet": "basis", "branch": "per_50g",
"conditions": [{ "field": "product_type", "op": "in", "values": ["yarn", "mixed"] },
{ "field": "pack_grams", "op": "notBlank" }] },
{ "kind": "CURRENCY_CONVERT", "sourceField": "unit_price", "outputField": "unit_price_eur", "outputType": "NUMBER",
"params": { "fromField": "currency", "to": "EUR" },
"branchSet": "basis", "branch": "per_50g",
"conditions": [{ "field": "product_type", "op": "in", "values": ["yarn", "mixed"] },
{ "field": "pack_grams", "op": "notBlank" }] },
{ "kind": "CURRENCY_CONVERT", "sourceField": "sale_unit_price", "outputField": "sale_unit_price_eur", "outputType": "NUMBER",
"params": { "fromField": "currency", "to": "EUR" },
"branchSet": "basis", "branch": "per_50g",
"conditions": [{ "field": "product_type", "op": "in", "values": ["yarn", "mixed"] },
{ "field": "pack_grams", "op": "notBlank" }] },
{ "kind": "ENUM_MAP", "sourceField": "price", "outputField": "unit", "outputType": "TEXT",
"params": { "mapping": {}, "default": "50 g" },
"branchSet": "basis", "branch": "per_50g",
"conditions": [{ "field": "product_type", "op": "in", "values": ["yarn", "mixed"] },
{ "field": "pack_grams", "op": "notBlank" }] }
]The platform writes eur_rate and eur_rate_date on each converted row; never name a column that. After the
save, read warnings.
Get the group rules — GET /api/scraper/group/{id}/post-process-rules
curl -H "Authorization: Bearer $KEY" \
"https://portal.scrapewise.ai/api/scraper-api/api/scraper/group/5f9a.../post-process-rules?page=0&size=200"Query (all optional; without them the answer is the same as before):
| Param | Meaning |
|---|---|
page, size | Page of the scrapers list: page from 0 (default 0), size 1–200 (default 200) |
filter | ALL (default), OWN (scrapers with their own rule for a group column) or PROBLEMS (scrapers with row warnings). Any other value is a 400 |
q | Only scrapers whose name contains this text (not case-sensitive). At most 100 characters, else 400 |
split | true with filter=ALL: the scrapers with row warnings come in problemScrapers, and scrapers pages only the others. A scraper is never in both |
summary | true adds summary (problem lines and scrapers with their own rules). It ignores q |
Response (200)
| Field | Meaning |
|---|---|
groupId | The group |
rules | The group rules, in order |
scraperValues | Group values per scraper (only scrapers now in the group) |
version | Current version; send it back as expectedVersion on the next PUT. 0 = the group has no rules yet |
updated, updatedBy | When and by whom the rules were last changed (null when there are no rules yet) |
counts | Over all scrapers of the group (never changed by q or filter): scrapers, overridden, missingDeclaredInput, missingValue, withRowWarnings |
scrapers | One page of per-scraper rows, sorted by name: scraperId, name, type, values, overriddenOutputs, ownedSets, undeclaredInputs, missingLabels, rowWarnings |
page, size, totalScrapers | Paging of scrapers; totalScrapers = all scrapers of the group |
matchingScrapers | Scrapers behind scrapers after q, filter and split (the paging runs over these) |
problemScrapers, problemScrapersTotal | Only with split=true: scrapers with row warnings that match q, most rows first (at most 200), and how many there are |
summary | Only with summary=true: problemLines (one per scraper and warning, most rows first, at most 100), problemLinesTotal, ownRuleScrapers (at most 200: scraperId, name, overriddenOutputs), ownRuleScrapersTotal |
runWarningsAvailable | false when the row warnings could not be read this time; rowWarnings are then empty |
In counts:
overridden— scrapers whose own rules override at least one group output;missingDeclaredInput— scrapers with at least one entry inundeclaredInputs;missingValue— scrapers that have neither a column nor a group value for a name that a condition reads (the names are in each row’smissingLabels);withRowWarnings— scrapers with at least one entry inrowWarnings.
rowWarnings counts the rows of the scraper’s latest run that were stored with a choice-group marker: {code, choiceGroup, rows} where code is RULE_NO_BRANCH (no choice fits the row) or RULE_MULTI_BRANCH (more than one choice fits). Only choice groups of the saved rules count: rename or delete a choice group and its warnings go away. The latest run is the newest run that stored rows (with its continuation runs); a run that is still going counts its rows so far. A run that stored no rows does not hide the run before it. Deleted rows (a deleted run, removed duplicates, data older than your retention) are not counted. Scrapers show no row warnings until their next run after this feature.
undeclaredInputs lists inputs (sourceField, quantityField, fromField) of group rules that run for this scraper, which the scraper does not have. For uploaded-file scrapers it is always empty: their columns come from the file.
{
"groupId": "6abbc37b049426c568144f5e",
"rules": [],
"scraperValues": { "6a01...": { "product_type": "yarn" } },
"version": 3,
"updated": "2026-09-30T10:00:00Z",
"updatedBy": "someone@example.com",
"counts": { "scrapers": 33, "overridden": 1, "missingDeclaredInput": 0, "missingValue": 0, "withRowWarnings": 1 },
"scrapers": [
{
"scraperId": "6a01...",
"name": "rito.dk",
"type": "MULTIPLE_PRODUCTS",
"values": { "product_type": "yarn" },
"overriddenOutputs": ["price_eur"],
"ownedSets": [],
"undeclaredInputs": [],
"missingLabels": [],
"rowWarnings": [{ "code": "RULE_NO_BRANCH", "choiceGroup": "basis", "rows": 4 }]
}
],
"page": 0,
"size": 200,
"totalScrapers": 33,
"matchingScrapers": 33,
"runWarningsAvailable": true
}Errors
| Code | Meaning |
|---|---|
| 400 | Group not found for your account (a group shared with you is refused too), size above 200, an unknown filter, or q longer than 100 characters |
| 401 | Missing/invalid bearer |
| 403 | N/A |
| 404 | N/A (400 used instead) |
| 429 | Rate-limited |
| 500 | Server error |
Save the group rules — PUT /api/scraper/group/{id}/post-process-rules
PUT /api/scraper/group/5f9a.../post-process-rules
Authorization: Bearer <key>
Idempotency-Key: <uuid>
Content-Type: application/json
{
"expectedVersion": 3,
"scraperValues": {
"6a01...": { "product_type": "yarn" },
"6a02...": { "product_type": "accessory" }
},
"rules": [
{
"kind": "CURRENCY_CONVERT",
"sourceField": "price",
"outputField": "price_eur",
"outputType": "NUMBER",
"params": { "fromField": "currency", "to": "EUR" }
},
{
"kind": "QUANTITY_NORMALIZE",
"sourceField": "price",
"outputField": "price_per_50g",
"outputType": "NUMBER",
"params": { "quantityField": "pack_grams", "targetQuantity": 50 },
"branchSet": "basis",
"branch": "per_50g",
"conditions": [
{ "field": "product_type", "op": "equals", "value": "yarn" },
{ "field": "pack_grams", "op": "notBlank" }
]
},
{
"kind": "QUANTITY_NORMALIZE",
"sourceField": "price",
"outputField": "price_per_piece",
"outputType": "NUMBER",
"params": { "quantityField": "pack_pieces", "targetQuantity": 1 },
"branchSet": "basis",
"branch": "per_piece",
"conditions": [
{ "field": "product_type", "op": "in", "values": ["accessory", "mixed"] },
{ "field": "pack_pieces", "op": "notBlank" },
{ "field": "pack_grams", "op": "blank" }
]
}
]
}The PUT replaces the whole rule list and all group values. expectedVersion is the version from your last GET (0 when the group has no rules yet). To remove all group rules, send "rules": [] and "scraperValues": {} with the current expectedVersion.
Response (200) — the same shape as the GET (page 0, size 200), plus warnings (a list of plain sentences; only on PUT). A warning is not an error; the save succeeded. You get one when:
- a rule reads a column that no scraper in the group has;
- a condition field is both a column of some scraper and a group-value name (the row column wins there);
- a group value was dropped because that scraper is no longer in the group.
Limits (400 when broken): at most 50 rules; the whole document at most 64 KB; at most 3 conditions per rule; in with 1–50 values; value 1–200 characters; at most 10 choice groups with 2–10 choices each; at most 5 group-value names; group values 1–50 characters; every scraper id in scraperValues must be one of your scrapers; each CURRENCY_CONVERT has exactly one of from / fromField; overwritesRawField must be false or absent. The normal per-kind rule checks also apply.
Errors
| Code | Meaning |
|---|---|
| 400 | Validation failed (limits above, bad rule settings, a scraper id that is not yours), or group not found for your account (a group shared with you is refused too) |
| 401 | Missing/invalid bearer |
| 403 | N/A |
| 404 | N/A (400 used instead) |
| 409 | expectedVersion is not the current version — someone changed the rules in the meantime. Body: {"errors":[{"code":"CONFLICT","message":"…"}]}. GET, merge, and try again |
| 429 | Rate-limited |
| 500 | Server error |
MCP tools
The two routes are also MCP tools: scrapewise_get_scraper_group_post_process_rules and scrapewise_update_scraper_group_post_process_rules. They take the same fields as the REST routes.
GET /api/scraper/config/parameters lists the condition ops as postProcessConditionOps and the limits above as groupRuleLimits (maxRules, maxConditionsPerRule, maxInValues, maxConditionValueLength, maxBranchSets, maxBranchesPerSet, maxBranchNameLength, maxValueNames, maxScraperValueLength, maxDocumentBytes, maxPageSize). If these fields are missing, the API does not support group rules yet.
Deleting or moving a group
Deleting a group also deletes its group rules. Group rules stay with the group when the group is moved to another account.
AI control — keep a group’s scrapers working
Websites change, and a scraper that worked last week can return empty or wrong rows today. AI control watches the scrapers of one group for you. It is switched on per group.
What it does when it is on:
- On the schedule you choose (
DAILY,TWICE_WEEKLYorWEEKLY), it checks the latest run of the scrapers in the group. - It looks for failed runs, empty runs and values that went missing.
- When it finds a problem, it proposes a fix and tests the fix on live pages before it saves it.
- It undoes a fix when the next real run is worse.
- Each check looks at up to
scrapersPerCheckscrapers (read the number from the status below), the ones checked longest ago first.
Which scrapers it can repair:
- It can repair scrapers that read the page with selectors (built automatically or by hand), and JSON-LD and API scrapers. We switch each kind of repair on.
- AI-schema scrapers are checked and you get an alert. They are not repaired yet.
- Supplier endpoints and uploaded files are not checked.
Good to know:
- We are in a test phase. Some problems are only recorded, not repaired, for now.
- After you switch it on, the first check usually starts within minutes (when the group is due for a check).
- Switching on needs your first wallet top-up. Switching off is always allowed.
- In the portal, open the group’s AI control dialog to do the same.
Switch AI control on or off — PUT /api/scraper/group/{id}/ai-control
PUT /api/scraper/group/{id}/ai-control
Authorization: Bearer <key>
Idempotency-Key: <uuid>
Content-Type: application/json
{
"enabled": true,
"cadence": "WEEKLY"
}| Field | Meaning |
|---|---|
enabled | true switches AI control on, false switches it off |
cadence | DAILY, TWICE_WEEKLY or WEEKLY. Optional: left out keeps the current choice (the first default is WEEKLY) |
Changing the cadence, or switching off and on again, never brings the next check earlier than the chosen cadence allows.
Response (200) — the group (GroupDTO) with aiControlEnabled and aiControlCadence.
Errors
| Code | Meaning |
|---|---|
| 400 | No group with this id on your account, or cadence is not one of the three values |
| 401 | Missing/invalid bearer |
| 402 | Switching on before your account’s first wallet top-up (PlanError). Switching off is never refused |
| 403 | An LLM_READ MCP key cannot switch AI control (use an LLM_FULL key) |
| 429 | Rate-limited |
| 500 | Server error |
Read the AI control status — GET /api/scraper/group/{id}/ai-control/status
GET /api/scraper/group/{id}/ai-control/status
Authorization: Bearer <key>Response (200)
{
"enabled": true,
"cadence": "WEEKLY",
"phase": "SCHEDULED",
"nextCheckAt": "2026-10-22T06:10:00Z",
"lastCheckAt": "2026-10-15T06:12:41Z",
"lastResultAt": "2026-10-15T06:12:41Z",
"nothingNewSinceResult": false,
"results": [
{ "kind": "REPAIRED", "count": 1, "reasons": ["VERIFIED"] },
{ "kind": "NOTHING_TO_REPAIR", "count": 11, "reasons": ["NO_ACTION"] }
],
"checkedCount": 12,
"scrapersPerCheck": 50
}| Field | Meaning |
|---|---|
enabled, cadence | The group’s choice |
phase | Where the check stands (see below) |
nextCheckAt | When the next check is planned. Empty when OFF or PAUSED, or not planned yet |
lastCheckAt | When the last check ended, also a check that found no new runs |
lastResultAt | When the last check that had a result ended |
nothingNewSinceResult | true when the last check found no new runs after the last result |
results | The last result: one line per kind, with count (number of scrapers) and reasons |
checkedCount | How many scrapers the last result judged |
scrapersPerCheck | The most scrapers one check looks at |
reasons are internal codes; read kind.
Phases
phase | Meaning |
|---|---|
OFF | AI control is switched off for this group |
PAUSED | Checks are paused on our side at the moment. Your choice is kept, and checks go on by themselves when the pause ends |
RUNNING | A check of this group is running now |
QUEUED | A first check is queued; it usually starts within minutes |
DUE | The next check is due and starts soon |
SCHEDULED | The next check is planned at nextCheckAt |
Result kinds
kind | Meaning |
|---|---|
CHECK_FAILED | The check could not be finished. It is tried again next time |
IN_PROGRESS | A repair is being made |
REPAIR_APPLIED | A repair was made. The next real run confirms it |
REPAIRED | A repair was made and confirmed by the next real run |
REPAIR_UNDONE | A repair was undone because the next real run was worse |
REPAIR_KEPT_WORSE | A repair was kept, but the next real run looked worse |
FIX_NOT_SAFE | A problem was found. The fix did not pass the safety test, so nothing was changed |
REPAIR_PAUSED | A problem keeps coming back. Repairs are paused for that scraper |
PROBLEM_NOT_FIXABLE | A problem was found that AI cannot fix for this kind of scraper yet |
PROBLEM_RECORDED | A problem was found and recorded. During the test phase it is not repaired |
BLOCKED | The scraper could not be checked this time. It is tried again at the next check |
ACCEPTED_CHANGE | A column stayed empty; this is now treated as normal |
NOTHING_TO_REPAIR | Checked, nothing to repair |
Why a scraper can be BLOCKED:
- Our own daily limit for AI checks was reached, or the AI service was busy. This is not your wallet.
- The scraper was busy (for example a run was going on).
- The website or the network failed.
- The test did not give a clear answer.
- That kind of repair is switched off.
- The scraper was changed at the same time.
You do not need to ask again and again: the next check time is in the answer.
Errors
| Code | Meaning |
|---|---|
| 400 | The id is not valid, or no group with this id on your account (a group of another account gives the same 400) |
| 401 | Missing/invalid bearer |
| 429 | Rate-limited |
| 500 | Server error |
MCP tools
Both routes are also MCP tools, in the setup tools:
scrapewise_update_scraper_group_ai_controlswitches AI control on or off. It needs anLLM_FULLkey.scrapewise_get_scraper_group_ai_control_statusreads the status. AnLLM_READkey can call it.
They take the same fields as the REST routes.
See also
- Scrapers — scrapers belong to a group
- Scraper jobs — load-history + data merge are group-scoped
- Shared groups — share a group’s read access cross-customer
- Scraped data — read group data
- Scrapers — per-kind rule reference — the rule kinds a group rule can use