From fbd9472218c0a5225e8a6b61baa068e14155125e Mon Sep 17 00:00:00 2001 From: dgtlmoon Date: Wed, 2 Sep 2026 16:46:27 +0200 Subject: [PATCH] fix(api): accept an existing tag UUID in the watch `tag` field, and deprecate it (#4361) `tag` on POST /watch was documented as taking a tag UUID, but the value went to add_tag(title): a UUID silently created a junk tag *titled* with that UUID and never applied the tag the caller asked for. `tags` (UUIDs) was the only thing that worked. - `tag=` now resolves an existing tag UUID to that tag, still falling back to title matching/creation for names. A UUID-shaped value matching nothing is skipped with a warning rather than becoming a group named after a UUID. - Blank tokens ("One,,Two,") no longer store False in watch['tags'] - add_tag() returns False for an empty title and it was appended unguarded. Consumers tolerate it (get_all_tags_for_watch() dictfilt()s over known tags) but it is not valid data. - add_tag()'s title search is extracted to tag_uuid_for_title(), so existence can be tested without creating as a side effect. add_tag()'s contract is unchanged. - api-spec: `tag` is marked `deprecated: true` (so Redoc renders the badge) and states plainly that it takes names, not UUIDs. `tags` now says what it really does - applied verbatim, never creates, unknown UUIDs stored as dangling refs. Rendered docs rebuilt. Every claim in the new field docs is asserted in test_api_tags.py against the real API. Co-authored-by: Claude Opus 5 (1M context) --- changedetectionio/store/__init__.py | 53 +++++++++++--- changedetectionio/tests/test_api_tags.py | 77 ++++++++++++++++++++ docs/api-spec.yaml | 15 +++- docs/api_v1/index.html | 93 ++++++++++++++++++------ 4 files changed, 206 insertions(+), 32 deletions(-) diff --git a/changedetectionio/store/__init__.py b/changedetectionio/store/__init__.py index 251124496..d54b4629a 100644 --- a/changedetectionio/store/__init__.py +++ b/changedetectionio/store/__init__.py @@ -49,6 +49,9 @@ dictfilt = lambda x, y: dict([(i, x[i]) for i in x if i in set(y)]) # Is there an existing library to ensure some data store (JSON etc) is in sync with CRUD methods? # Open a github issue if you know something :) # https://stackoverflow.com/questions/6190468/how-to-trigger-function-on-value-change +_TAG_UUID_RE = re.compile(r'^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', re.IGNORECASE) + + class ChangeDetectionStore(DatastoreUpdatesMixin, FileSavingDataStore): __version_check = True @@ -791,11 +794,31 @@ class ChangeDetectionStore(DatastoreUpdatesMixin, FileSavingDataStore): return None if tag and type(tag) == str: - # Then it's probably a string of the actual tag by name, split and add it - for t in tag.split(','): - # for each stripped tag, add tag as UUID - for a_t in t.split(','): - tag_uuid = self.add_tag(a_t) + # A comma separated string of tag *titles*, created when they don't exist yet. + # An existing tag's UUID is accepted here too: the API documented this field as taking + # a UUID for years, and honouring that beats creating a tag *titled* with the UUID. + existing_tag_uuids = self.__data['settings']['application'].get('tags', {}) + + for tag_name in tag.split(','): + tag_name = tag_name.strip() + if not tag_name: + continue + + if _TAG_UUID_RE.match(tag_name): + if tag_name in existing_tag_uuids: + apply_extras['tags'].append(tag_name) + continue + # UUID-shaped but no such tag, and no tag literally titled that either - + # a stale or foreign ID. Skip it rather than leave behind a group named + # after a UUID, which is never what the caller wanted. + if not self.tag_uuid_for_title(tag_name): + logger.warning(f"Tag '{tag_name}' looks like a UUID but no such tag exists, skipping") + continue + + tag_uuid = self.add_tag(tag_name) + # add_tag() returns False for a title it won't create - never let that into the list, + # a falsy entry blows up every lookup of watch['tags'] + if tag_uuid: apply_extras['tags'].append(tag_uuid) # Or if UUIDs given directly @@ -1069,6 +1092,18 @@ class ChangeDetectionStore(DatastoreUpdatesMixin, FileSavingDataStore): return ret + def tag_uuid_for_title(self, title): + """UUID of the tag with this title (case/space insensitive), or None. Creates nothing.""" + n = title.strip().lower() + if not n: + return None + + for uuid, tag in self.__data['settings']['application'].get('tags', {}).items(): + if n == tag.get('title', '').lower().strip(): + return uuid + + return None + def add_tag(self, title): # If name exists, return that n = title.strip().lower() @@ -1076,10 +1111,10 @@ class ChangeDetectionStore(DatastoreUpdatesMixin, FileSavingDataStore): if not n: return False - for uuid, tag in self.__data['settings']['application'].get('tags', {}).items(): - if n == tag.get('title', '').lower().strip(): - logger.warning(f"Tag '{title}' already exists, skipping creation.") - return uuid + existing_uuid = self.tag_uuid_for_title(title) + if existing_uuid: + logger.warning(f"Tag '{title}' already exists, skipping creation.") + return existing_uuid # Eventually almost everything todo with a watch will apply as a Tag # So we use the same model as a Watch diff --git a/changedetectionio/tests/test_api_tags.py b/changedetectionio/tests/test_api_tags.py index b25ea5fae..43060c482 100644 --- a/changedetectionio/tests/test_api_tags.py +++ b/changedetectionio/tests/test_api_tags.py @@ -321,3 +321,80 @@ def test_roundtrip_API(client, live_server, measure_memory_usage, datastore_path date_created = res.json.get('date_created') assert date_created != 454444444444, "ReadOnly date_created should not be updateable" assert date_created != "454444444444", "ReadOnly date_created should not be updateable" + + +def test_api_watch_tag_field_accepts_names_and_uuids(client, live_server, measure_memory_usage, datastore_path): + """The `tag` field on a watch takes tag *names*, `tags` takes UUIDs. + + `tag` was documented as taking a UUID for years while the code fed it to add_tag(title), + so a UUID silently created a junk group *titled* with that UUID and never applied the tag + the caller asked for. Both spellings now resolve to the same tag. + """ + api_key = live_server.app.config['DATASTORE'].data['settings']['application'].get('api_access_token') + datastore = live_server.app.config['DATASTORE'] + test_url = url_for('test_endpoint', _external=True) + hdr = {'x-api-key': api_key, 'content-type': 'application/json'} + + def titles_of(watch_uuid): + tags = datastore.data['settings']['application']['tags'] + return sorted(tags[t].get('title') for t in datastore.data['watching'][watch_uuid].get('tags')) + + # A name creates the group + res = client.post(url_for("createwatch"), data=json.dumps({"url": test_url, "tag": "helloworld"}), headers=hdr) + assert res.status_code == 201 + assert titles_of(res.json['uuid']) == ['helloworld'] + + # An existing tag's UUID links to that tag rather than making a group named after the UUID + res = client.post(url_for("tag"), data=json.dumps({"title": "RealTag"}), headers=hdr) + assert res.status_code == 201 + real_tag_uuid = res.json['uuid'] + tag_count_before = len(datastore.data['settings']['application']['tags']) + + res = client.post(url_for("createwatch"), data=json.dumps({"url": f"{test_url}?p=2", "tag": real_tag_uuid}), headers=hdr) + assert res.status_code == 201 + assert real_tag_uuid in datastore.data['watching'][res.json['uuid']].get('tags') + assert titles_of(res.json['uuid']) == ['RealTag'] + assert len(datastore.data['settings']['application']['tags']) == tag_count_before, "No junk tag titled with a UUID" + + # `tags` with UUIDs keeps working + res = client.post(url_for("createwatch"), data=json.dumps({"url": f"{test_url}?p=3", "tags": [real_tag_uuid]}), headers=hdr) + assert res.status_code == 201 + assert titles_of(res.json['uuid']) == ['RealTag'] + + # Names and UUIDs can be mixed, and blank entries from a trailing comma are dropped - + # add_tag() returns False for those and a falsy entry breaks every watch['tags'] lookup + res = client.post(url_for("createwatch"), + data=json.dumps({"url": f"{test_url}?p=4", "tag": f"Mixed,,{real_tag_uuid},"}), headers=hdr) + assert res.status_code == 201 + assert titles_of(res.json['uuid']) == ['Mixed', 'RealTag'] + assert all(datastore.data['watching'][res.json['uuid']].get('tags')), "No falsy entries in tags" + + # A UUID that matches no tag is skipped rather than becoming a group named after it + unknown_uuid = '0be0272a-19dc-4c97-8aae-5a68df319489' + tag_count_before = len(datastore.data['settings']['application']['tags']) + res = client.post(url_for("createwatch"), + data=json.dumps({"url": f"{test_url}?p=5", "tag": unknown_uuid}), headers=hdr) + assert res.status_code == 201 + assert datastore.data['watching'][res.json['uuid']].get('tags') == [] + assert len(datastore.data['settings']['application']['tags']) == tag_count_before + + # Names are matched case-insensitively against existing tags, as the spec claims + tag_count_before = len(datastore.data['settings']['application']['tags']) + res = client.post(url_for("createwatch"), + data=json.dumps({"url": f"{test_url}?p=6", "tag": "rEaLtAg"}), headers=hdr) + assert res.status_code == 201 + assert datastore.data['watching'][res.json['uuid']].get('tags') == [real_tag_uuid] + assert len(datastore.data['settings']['application']['tags']) == tag_count_before, "Casing must not fork a second tag" + + # `tags` is applied verbatim and never creates: an unknown UUID is stored as a dangling + # reference that simply resolves to no group. Documented, and harmless because the lookup + # is a dictfilt() over known tags - pinned here so changing it has to be deliberate. + bogus = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee' + tag_count_before = len(datastore.data['settings']['application']['tags']) + res = client.post(url_for("createwatch"), + data=json.dumps({"url": f"{test_url}?p=7", "tags": [bogus]}), headers=hdr) + assert res.status_code == 201 + assert datastore.data['watching'][res.json['uuid']].get('tags') == [bogus] + assert len(datastore.data['settings']['application']['tags']) == tag_count_before + assert datastore.get_all_tags_for_watch(res.json['uuid']) == {} + assert client.get(url_for("watchlist.index")).status_code == 200, "A dangling tag ref must not break the list" diff --git a/docs/api-spec.yaml b/docs/api-spec.yaml index 182475a7d..1b81cba26 100644 --- a/docs/api-spec.yaml +++ b/docs/api-spec.yaml @@ -299,13 +299,24 @@ components: maxLength: 5000 tag: type: string - description: Tag UUID to associate with this web page change monitor (watch) + deprecated: true + description: | + **Deprecated - use `tags` instead.** Kept working for API v1 and may be removed in v2. + + Takes comma-separated tag *names* (not UUIDs, despite what earlier revisions of this + document said). Names are matched case-insensitively against existing tags and created + when they don't exist yet. An existing tag's UUID is also accepted and resolves to that + tag; a UUID matching no tag is ignored. maxLength: 5000 + example: "Production, Price tracking" tags: type: array items: type: string - description: Array of tag UUIDs + description: | + Array of tag UUIDs. Unlike `tag`, these are applied exactly as given - no tag is ever + created from this field. The UUIDs are not validated: an unknown one is stored but + resolves to no group. paused: type: boolean description: Whether the web page change monitor (watch) is paused diff --git a/docs/api_v1/index.html b/docs/api_v1/index.html index d55e2c82e..a76a1e7a9 100644 --- a/docs/api_v1/index.html +++ b/docs/api_v1/index.html @@ -59,6 +59,9 @@ data-styled.g14[id="sc-kcLKEh"]{content:"fRdsOi,"}/*!sc*/ .jKYZgc{height:1.5em;width:1.5em;min-width:1.5em;vertical-align:middle;float:left;transition:transform 0.2s ease-out;transform:rotateZ(-90deg);}/*!sc*/ .jKYZgc polygon{fill:#d41f1c;}/*!sc*/ data-styled.g15[id="sc-dntSTA"]{content:"dUlzCe,FtowP,cGxVlA,iuNpUs,dOPmTa,jKYZgc,"}/*!sc*/ +.cCjOXR{display:inline-block;padding:2px 8px;margin:0;background-color:#ffa500;color:#ffffff;font-size:13px;vertical-align:middle;line-height:1.6;border-radius:4px;font-weight:600;font-size:12px;}/*!sc*/ +.cCjOXR +span[type]{margin-left:4px;}/*!sc*/ +data-styled.g16[id="sc-kvnevz"]{content:"cCjOXR,"}/*!sc*/ .gdmNWp{border-left:1px solid #7c7cbb;box-sizing:border-box;position:relative;padding:10px 10px 10px 0;}/*!sc*/ @media screen and (max-width: 50rem){.gdmNWp{display:block;overflow:hidden;}}/*!sc*/ tr:first-of-type>.gdmNWp,tr.last>.gdmNWp{border-left-width:0;background-position:top left;background-repeat:no-repeat;background-size:1px 100%;}/*!sc*/ @@ -515,10 +518,22 @@ notification preferences, and content filtering options.

" class="sc-eVqvcJ sc-fszimp kIppRw drqpJr">

URL to monitor for changes

title
string or null <= 5000 characters

Custom title for the web page change monitor (watch), not to be confused with page_title

-
tag
string <= 5000 characters

Tag UUID to associate with this web page change monitor (watch)

-
tags
Array of strings

Array of tag UUIDs

+
tag
string <= 5000 characters
Deprecated

Deprecated - use tags instead. Kept working for API v1 and may be removed in v2.

+

Takes comma-separated tag names (not UUIDs, despite what earlier revisions of this +document said). Names are matched case-insensitively against existing tags and created +when they don't exist yet. An existing tag's UUID is also accepted and resolves to that +tag; a UUID matching no tag is ignored.

+
tags
Array of strings

Array of tag UUIDs. Unlike tag, these are applied exactly as given - no tag is ever +created from this field. The UUIDs are not validated: an unknown one is stored but +resolves to no group.

paused
boolean

Whether the web page change monitor (watch) is paused

notification_muted
boolean

Custom server

{protocol}://{host}/api/v1/watch/{uuid}

Request samples

curl -X GET "http://localhost:5000/api/v1/watch/095be615-a8ad-4c33-8e9c-c7612fbf6c9f" \
   -H "x-api-key: YOUR_API_KEY"
-

Response samples

Content type
{
  • "uuid": "095be615-a8ad-4c33-8e9c-c7612fbf6c9f",
  • "date_created": 0,
  • "title": "string",
  • "tag": "string",
  • "tags": [
    ],
  • "paused": true,
  • "notification_muted": true,
  • "method": "GET",
  • "fetch_backend": "system",
  • "headers": {
    },
  • "body": "string",
  • "proxy": "string",
  • "ignore_status_codes": true,
  • "webdriver_delay": 0,
  • "webdriver_js_execute_code": "string",
  • "time_between_check": {
    },
  • "time_between_check_use_default": true,
  • "notification_urls": [
    ],
  • "notification_title": "string",
  • "notification_body": "string",
  • "notification_format": "text",
  • "track_ldjson_price_data": true,
  • "browser_steps": [
    ],
  • "processor": "restock_diff",
  • "include_filters": [
    ],
  • "subtractive_selectors": [
    ],
  • "ignore_text": [
    ],
  • "trigger_text": [
    ],
  • "text_should_not_be_present": [
    ],
  • "extract_lines_containing": [
    ],
  • "extract_text": [
    ],
  • "trim_text_whitespace": false,
  • "sort_text_alphabetically": false,
  • "remove_duplicate_lines": false,
  • "check_unique_lines": false,
  • "strip_ignored_lines": true,
  • "filter_text_added": true,
  • "filter_text_removed": true,
  • "filter_text_replaced": true,
  • "in_stock_only": true,
  • "follow_price_changes": true,
  • "price_change_threshold_percent": 0,
  • "has_ldjson_price_data": true,
  • "notification_screenshot": false,
  • "filter_failure_notification_send": true,
  • "use_page_title_in_list": true,
  • "history_snapshot_max_length": 1,
  • "time_schedule_limit": {
    },
  • "conditions": [
    ],
  • "conditions_match_logic": "ALL",
  • "llm_backend_profile": true,
  • "llm_intent": "",
  • "llm_change_summary": "",
  • "llm_change_summary_mode": "replace",
  • "llm_prefilter": "string",
  • "llm_evaluation_cache": { },
  • "llm_last_tokens_used": 0,
  • "llm_tokens_used_cumulative": 0,
  • "llm_tokens_this_period": 0,
  • "llm_tokens_period_key": "string",
  • "last_checked": 0,
  • "last_changed": 0,
  • "last_error": "string",
  • "last_viewed": 0,
  • "link": "string",
  • "page_title": "string",
  • "check_count": 0,
  • "fetch_time": 0,
  • "previous_md5": "string",
  • "previous_md5_before_filters": "string",
  • "consecutive_filter_failures": 0,
  • "last_notification_error": "string",
  • "notification_alert_count": 0,
  • "content-type": "string",
  • "remote_server_reply": "string",
  • "browser_steps_last_error_step": 0,
  • "viewed": 0,
  • "history_n": 0,
  • "processor_config_restock_diff": {
    },
  • "processor_config_restock_diff_source": "string"
}

Update watch

Response samples

Content type
{
  • "uuid": "095be615-a8ad-4c33-8e9c-c7612fbf6c9f",
  • "date_created": 0,
  • "title": "string",
  • "tag": "Production, Price tracking",
  • "tags": [
    ],
  • "paused": true,
  • "notification_muted": true,
  • "method": "GET",
  • "fetch_backend": "system",
  • "headers": {
    },
  • "body": "string",
  • "proxy": "string",
  • "ignore_status_codes": true,
  • "webdriver_delay": 0,
  • "webdriver_js_execute_code": "string",
  • "time_between_check": {
    },
  • "time_between_check_use_default": true,
  • "notification_urls": [
    ],
  • "notification_title": "string",
  • "notification_body": "string",
  • "notification_format": "text",
  • "track_ldjson_price_data": true,
  • "browser_steps": [
    ],
  • "processor": "restock_diff",
  • "include_filters": [
    ],
  • "subtractive_selectors": [
    ],
  • "ignore_text": [
    ],
  • "trigger_text": [
    ],
  • "text_should_not_be_present": [
    ],
  • "extract_lines_containing": [
    ],
  • "extract_text": [
    ],
  • "trim_text_whitespace": false,
  • "sort_text_alphabetically": false,
  • "remove_duplicate_lines": false,
  • "check_unique_lines": false,
  • "strip_ignored_lines": true,
  • "filter_text_added": true,
  • "filter_text_removed": true,
  • "filter_text_replaced": true,
  • "in_stock_only": true,
  • "follow_price_changes": true,
  • "price_change_threshold_percent": 0,
  • "has_ldjson_price_data": true,
  • "notification_screenshot": false,
  • "filter_failure_notification_send": true,
  • "use_page_title_in_list": true,
  • "history_snapshot_max_length": 1,
  • "time_schedule_limit": {
    },
  • "conditions": [
    ],
  • "conditions_match_logic": "ALL",
  • "llm_backend_profile": true,
  • "llm_intent": "",
  • "llm_change_summary": "",
  • "llm_change_summary_mode": "replace",
  • "llm_prefilter": "string",
  • "llm_evaluation_cache": { },
  • "llm_last_tokens_used": 0,
  • "llm_tokens_used_cumulative": 0,
  • "llm_tokens_this_period": 0,
  • "llm_tokens_period_key": "string",
  • "last_checked": 0,
  • "last_changed": 0,
  • "last_error": "string",
  • "last_viewed": 0,
  • "link": "string",
  • "page_title": "string",
  • "check_count": 0,
  • "fetch_time": 0,
  • "previous_md5": "string",
  • "previous_md5_before_filters": "string",
  • "consecutive_filter_failures": 0,
  • "last_notification_error": "string",
  • "notification_alert_count": 0,
  • "content-type": "string",
  • "remote_server_reply": "string",
  • "browser_steps_last_error_step": 0,
  • "viewed": 0,
  • "history_n": 0,
  • "processor_config_restock_diff": {
    },
  • "processor_config_restock_diff_source": "string"
}

Update watch

Update an existing web page change monitor (watch) using JSON. Accepts the same structure as returned in get single watch information.

Authorizations:
ApiKeyAuth
path Parameters
uuid
required
string <uuid>

Web page change monitor (watch) unique ID

@@ -689,10 +704,22 @@ No watch was created - delete one to make room.

" class="sc-eVqvcJ sc-fszimp kIppRw drqpJr">

URL to monitor for changes

title
string or null <= 5000 characters

Custom title for the web page change monitor (watch), not to be confused with page_title

-
tag
string <= 5000 characters

Tag UUID to associate with this web page change monitor (watch)

-
tags
Array of strings

Array of tag UUIDs

+
tag
string <= 5000 characters
Deprecated

Deprecated - use tags instead. Kept working for API v1 and may be removed in v2.

+

Takes comma-separated tag names (not UUIDs, despite what earlier revisions of this +document said). Names are matched case-insensitively against existing tags and created +when they don't exist yet. An existing tag's UUID is also accepted and resolves to that +tag; a UUID matching no tag is ignored.

+
tags
Array of strings

Array of tag UUIDs. Unlike tag, these are applied exactly as given - no tag is ever +created from this field. The UUIDs are not validated: an unknown one is stored but +resolves to no group.

paused
boolean

Whether the web page change monitor (watch) is paused

notification_muted
boolean

Production server

https://yourdomain.com/api/v1/watch/{uuid}

Custom server

-
{protocol}://{host}/api/v1/watch/{uuid}

Request samples

Content type
application/json
{
  • "title": "string",
  • "tag": "string",
  • "tags": [
    ],
  • "paused": true,
  • "notification_muted": true,
  • "method": "GET",
  • "fetch_backend": "system",
  • "headers": {
    },
  • "body": "string",
  • "proxy": "string",
  • "ignore_status_codes": true,
  • "webdriver_delay": 0,
  • "webdriver_js_execute_code": "string",
  • "time_between_check": {
    },
  • "time_between_check_use_default": true,
  • "notification_urls": [
    ],
  • "notification_title": "string",
  • "notification_body": "string",
  • "notification_format": "text",
  • "track_ldjson_price_data": true,
  • "browser_steps": [
    ],
  • "processor": "restock_diff",
  • "include_filters": [
    ],
  • "subtractive_selectors": [
    ],
  • "ignore_text": [
    ],
  • "trigger_text": [
    ],
  • "text_should_not_be_present": [
    ],
  • "extract_lines_containing": [
    ],
  • "extract_text": [
    ],
  • "trim_text_whitespace": false,
  • "sort_text_alphabetically": false,
  • "remove_duplicate_lines": false,
  • "check_unique_lines": false,
  • "strip_ignored_lines": true,
  • "filter_text_added": true,
  • "filter_text_removed": true,
  • "filter_text_replaced": true,
  • "in_stock_only": true,
  • "follow_price_changes": true,
  • "price_change_threshold_percent": 0,
  • "notification_screenshot": false,
  • "filter_failure_notification_send": true,
  • "use_page_title_in_list": true,
  • "history_snapshot_max_length": 1,
  • "time_schedule_limit": {
    },
  • "conditions": [
    ],
  • "conditions_match_logic": "ALL",
  • "llm_backend_profile": true,
  • "llm_intent": "",
  • "llm_change_summary": "",
  • "llm_change_summary_mode": "replace",
  • "last_viewed": 0
}

Delete watch

{protocol}://{host}/api/v1/watch/{uuid}

Request samples

Content type
application/json
{
  • "title": "string",
  • "tag": "Production, Price tracking",
  • "tags": [
    ],
  • "paused": true,
  • "notification_muted": true,
  • "method": "GET",
  • "fetch_backend": "system",
  • "headers": {
    },
  • "body": "string",
  • "proxy": "string",
  • "ignore_status_codes": true,
  • "webdriver_delay": 0,
  • "webdriver_js_execute_code": "string",
  • "time_between_check": {
    },
  • "time_between_check_use_default": true,
  • "notification_urls": [
    ],
  • "notification_title": "string",
  • "notification_body": "string",
  • "notification_format": "text",
  • "track_ldjson_price_data": true,
  • "browser_steps": [
    ],
  • "processor": "restock_diff",
  • "include_filters": [
    ],
  • "subtractive_selectors": [
    ],
  • "ignore_text": [
    ],
  • "trigger_text": [
    ],
  • "text_should_not_be_present": [
    ],
  • "extract_lines_containing": [
    ],
  • "extract_text": [
    ],
  • "trim_text_whitespace": false,
  • "sort_text_alphabetically": false,
  • "remove_duplicate_lines": false,
  • "check_unique_lines": false,
  • "strip_ignored_lines": true,
  • "filter_text_added": true,
  • "filter_text_removed": true,
  • "filter_text_replaced": true,
  • "in_stock_only": true,
  • "follow_price_changes": true,
  • "price_change_threshold_percent": 0,
  • "notification_screenshot": false,
  • "filter_failure_notification_send": true,
  • "use_page_title_in_list": true,
  • "history_snapshot_max_length": 1,
  • "time_schedule_limit": {
    },
  • "conditions": [
    ],
  • "conditions_match_logic": "ALL",
  • "llm_backend_profile": true,
  • "llm_intent": "",
  • "llm_change_summary": "",
  • "llm_change_summary_mode": "replace",
  • "last_viewed": 0
}

Delete watch

Delete a web page change monitor (watch) and all related history

Authorizations:
ApiKeyAuth
path Parameters
uuid
required
string <uuid>

Web page change monitor (watch) unique ID

@@ -1101,10 +1128,22 @@ multiple related watches.

" class="sc-eVqvcJ sc-fszimp kIppRw drqpJr">

URL to monitor for changes

title
required
string or null <= 5000 characters

Custom title for the web page change monitor (watch), not to be confused with page_title

-
tag
string <= 5000 characters

Tag UUID to associate with this web page change monitor (watch)

-
tags
Array of strings

Array of tag UUIDs

+
tag
string <= 5000 characters
Deprecated

Deprecated - use tags instead. Kept working for API v1 and may be removed in v2.

+

Takes comma-separated tag names (not UUIDs, despite what earlier revisions of this +document said). Names are matched case-insensitively against existing tags and created +when they don't exist yet. An existing tag's UUID is also accepted and resolves to that +tag; a UUID matching no tag is ignored.

+
tags
Array of strings

Array of tag UUIDs. Unlike tag, these are applied exactly as given - no tag is ever +created from this field. The UUIDs are not validated: an unknown one is stored but +resolves to no group.

paused
boolean

Whether the web page change monitor (watch) is paused

notification_muted
boolean

Custom server

{protocol}://{host}/api/v1/tag/{uuid}

Request samples

curl -X GET "http://localhost:5000/api/v1/tag/550e8400-e29b-41d4-a716-446655440000" \
   -H "x-api-key: YOUR_API_KEY"
-

Response samples

Content type
{
  • "uuid": "095be615-a8ad-4c33-8e9c-c7612fbf6c9f",
  • "date_created": 0,
  • "title": "string",
  • "tag": "string",
  • "tags": [
    ],
  • "paused": true,
  • "notification_muted": true,
  • "method": "GET",
  • "fetch_backend": "system",
  • "headers": {
    },
  • "body": "string",
  • "proxy": "string",
  • "ignore_status_codes": true,
  • "webdriver_delay": 0,
  • "webdriver_js_execute_code": "string",
  • "time_between_check": {
    },
  • "time_between_check_use_default": true,
  • "notification_urls": [
    ],
  • "notification_title": "string",
  • "notification_body": "string",
  • "notification_format": "text",
  • "track_ldjson_price_data": true,
  • "browser_steps": [
    ],
  • "processor": "restock_diff",
  • "include_filters": [
    ],
  • "subtractive_selectors": [
    ],
  • "ignore_text": [
    ],
  • "trigger_text": [
    ],
  • "text_should_not_be_present": [
    ],
  • "extract_lines_containing": [
    ],
  • "extract_text": [
    ],
  • "trim_text_whitespace": false,
  • "sort_text_alphabetically": false,
  • "remove_duplicate_lines": false,
  • "check_unique_lines": false,
  • "strip_ignored_lines": true,
  • "filter_text_added": true,
  • "filter_text_removed": true,
  • "filter_text_replaced": true,
  • "in_stock_only": true,
  • "follow_price_changes": true,
  • "price_change_threshold_percent": 0,
  • "has_ldjson_price_data": true,
  • "notification_screenshot": false,
  • "filter_failure_notification_send": true,
  • "use_page_title_in_list": true,
  • "history_snapshot_max_length": 1,
  • "time_schedule_limit": {
    },
  • "conditions": [
    ],
  • "conditions_match_logic": "ALL",
  • "llm_backend_profile": true,
  • "llm_intent": "",
  • "llm_change_summary": "",
  • "llm_change_summary_mode": "replace",
  • "llm_prefilter": "string",
  • "llm_evaluation_cache": { },
  • "llm_last_tokens_used": 0,
  • "llm_tokens_used_cumulative": 0,
  • "llm_tokens_this_period": 0,
  • "llm_tokens_period_key": "string",
  • "overrides_watch": true,
  • "url_match_pattern": "string",
  • "tag_colour": "string"
}

Update tag

Response samples

Content type
{
  • "uuid": "095be615-a8ad-4c33-8e9c-c7612fbf6c9f",
  • "date_created": 0,
  • "title": "string",
  • "tag": "Production, Price tracking",
  • "tags": [
    ],
  • "paused": true,
  • "notification_muted": true,
  • "method": "GET",
  • "fetch_backend": "system",
  • "headers": {
    },
  • "body": "string",
  • "proxy": "string",
  • "ignore_status_codes": true,
  • "webdriver_delay": 0,
  • "webdriver_js_execute_code": "string",
  • "time_between_check": {
    },
  • "time_between_check_use_default": true,
  • "notification_urls": [
    ],
  • "notification_title": "string",
  • "notification_body": "string",
  • "notification_format": "text",
  • "track_ldjson_price_data": true,
  • "browser_steps": [
    ],
  • "processor": "restock_diff",
  • "include_filters": [
    ],
  • "subtractive_selectors": [
    ],
  • "ignore_text": [
    ],
  • "trigger_text": [
    ],
  • "text_should_not_be_present": [
    ],
  • "extract_lines_containing": [
    ],
  • "extract_text": [
    ],
  • "trim_text_whitespace": false,
  • "sort_text_alphabetically": false,
  • "remove_duplicate_lines": false,
  • "check_unique_lines": false,
  • "strip_ignored_lines": true,
  • "filter_text_added": true,
  • "filter_text_removed": true,
  • "filter_text_replaced": true,
  • "in_stock_only": true,
  • "follow_price_changes": true,
  • "price_change_threshold_percent": 0,
  • "has_ldjson_price_data": true,
  • "notification_screenshot": false,
  • "filter_failure_notification_send": true,
  • "use_page_title_in_list": true,
  • "history_snapshot_max_length": 1,
  • "time_schedule_limit": {
    },
  • "conditions": [
    ],
  • "conditions_match_logic": "ALL",
  • "llm_backend_profile": true,
  • "llm_intent": "",
  • "llm_change_summary": "",
  • "llm_change_summary_mode": "replace",
  • "llm_prefilter": "string",
  • "llm_evaluation_cache": { },
  • "llm_last_tokens_used": 0,
  • "llm_tokens_used_cumulative": 0,
  • "llm_tokens_this_period": 0,
  • "llm_tokens_period_key": "string",
  • "overrides_watch": true,
  • "url_match_pattern": "string",
  • "tag_colour": "string"
}

Update tag

Update an existing tag using JSON

Authorizations:
ApiKeyAuth
path Parameters
uuid
required
string <uuid>

Tag unique ID

@@ -1295,10 +1334,22 @@ Leave empty to use the colour auto-generated from the tag name.

" class="sc-eVqvcJ sc-fszimp kIppRw drqpJr">

URL to monitor for changes

title
string or null <= 5000 characters

Custom title for the web page change monitor (watch), not to be confused with page_title

-
tag
string <= 5000 characters

Tag UUID to associate with this web page change monitor (watch)

-
tags
Array of strings

Array of tag UUIDs

+
tag
string <= 5000 characters
Deprecated

Deprecated - use tags instead. Kept working for API v1 and may be removed in v2.

+

Takes comma-separated tag names (not UUIDs, despite what earlier revisions of this +document said). Names are matched case-insensitively against existing tags and created +when they don't exist yet. An existing tag's UUID is also accepted and resolves to that +tag; a UUID matching no tag is ignored.

+
tags
Array of strings

Array of tag UUIDs. Unlike tag, these are applied exactly as given - no tag is ever +created from this field. The UUIDs are not validated: an unknown one is stored but +resolves to no group.

paused
boolean

Whether the web page change monitor (watch) is paused

notification_muted
boolean

Production server

https://yourdomain.com/api/v1/tag/{uuid}

Custom server

-
{protocol}://{host}/api/v1/tag/{uuid}

Request samples

Content type
application/json
{
  • "title": "string",
  • "tag": "string",
  • "tags": [
    ],
  • "paused": true,
  • "notification_muted": true,
  • "method": "GET",
  • "fetch_backend": "system",
  • "headers": {
    },
  • "body": "string",
  • "proxy": "string",
  • "ignore_status_codes": true,
  • "webdriver_delay": 0,
  • "webdriver_js_execute_code": "string",
  • "time_between_check": {
    },
  • "time_between_check_use_default": true,
  • "notification_urls": [
    ],
  • "notification_title": "string",
  • "notification_body": "string",
  • "notification_format": "text",
  • "track_ldjson_price_data": true,
  • "browser_steps": [
    ],
  • "processor": "restock_diff",
  • "include_filters": [
    ],
  • "subtractive_selectors": [
    ],
  • "ignore_text": [
    ],
  • "trigger_text": [
    ],
  • "text_should_not_be_present": [
    ],
  • "extract_lines_containing": [
    ],
  • "extract_text": [
    ],
  • "trim_text_whitespace": false,
  • "sort_text_alphabetically": false,
  • "remove_duplicate_lines": false,
  • "check_unique_lines": false,
  • "strip_ignored_lines": true,
  • "filter_text_added": true,
  • "filter_text_removed": true,
  • "filter_text_replaced": true,
  • "in_stock_only": true,
  • "follow_price_changes": true,
  • "price_change_threshold_percent": 0,
  • "notification_screenshot": false,
  • "filter_failure_notification_send": true,
  • "use_page_title_in_list": true,
  • "history_snapshot_max_length": 1,
  • "time_schedule_limit": {
    },
  • "conditions": [
    ],
  • "conditions_match_logic": "ALL",
  • "llm_backend_profile": true,
  • "llm_intent": "",
  • "llm_change_summary": "",
  • "llm_change_summary_mode": "replace",
  • "overrides_watch": true,
  • "url_match_pattern": "string",
  • "tag_colour": "string"
}

Delete tag

{protocol}://{host}/api/v1/tag/{uuid}

Request samples

Content type
application/json
{
  • "title": "string",
  • "tag": "Production, Price tracking",
  • "tags": [
    ],
  • "paused": true,
  • "notification_muted": true,
  • "method": "GET",
  • "fetch_backend": "system",
  • "headers": {
    },
  • "body": "string",
  • "proxy": "string",
  • "ignore_status_codes": true,
  • "webdriver_delay": 0,
  • "webdriver_js_execute_code": "string",
  • "time_between_check": {
    },
  • "time_between_check_use_default": true,
  • "notification_urls": [
    ],
  • "notification_title": "string",
  • "notification_body": "string",
  • "notification_format": "text",
  • "track_ldjson_price_data": true,
  • "browser_steps": [
    ],
  • "processor": "restock_diff",
  • "include_filters": [
    ],
  • "subtractive_selectors": [
    ],
  • "ignore_text": [
    ],
  • "trigger_text": [
    ],
  • "text_should_not_be_present": [
    ],
  • "extract_lines_containing": [
    ],
  • "extract_text": [
    ],
  • "trim_text_whitespace": false,
  • "sort_text_alphabetically": false,
  • "remove_duplicate_lines": false,
  • "check_unique_lines": false,
  • "strip_ignored_lines": true,
  • "filter_text_added": true,
  • "filter_text_removed": true,
  • "filter_text_replaced": true,
  • "in_stock_only": true,
  • "follow_price_changes": true,
  • "price_change_threshold_percent": 0,
  • "notification_screenshot": false,
  • "filter_failure_notification_send": true,
  • "use_page_title_in_list": true,
  • "history_snapshot_max_length": 1,
  • "time_schedule_limit": {
    },
  • "conditions": [
    ],
  • "conditions_match_logic": "ALL",
  • "llm_backend_profile": true,
  • "llm_intent": "",
  • "llm_change_summary": "",
  • "llm_change_summary_mode": "replace",
  • "overrides_watch": true,
  • "url_match_pattern": "string",
  • "tag_colour": "string"
}

Delete tag

Delete a tag/group and remove it from all web page change monitors (watches)

Authorizations:
ApiKeyAuth
path Parameters
uuid
required
string <uuid>

Tag unique ID

@@ -1966,7 +2017,7 @@ installed processors (e.g. processor_config_restock_diff from the b curl -X GET "http://localhost:5000/api/v1/full-spec"