Skip to content

Watchers

A watcher tells VelociFile to notice changes to one filing or to everything a search matches, and to deliver an event for each change to your webhook destination. Events are also stored, so you can read them back and recover from outages.

POST /watchers returns 201 with the watcher and an ETag. The Idempotency-Key header is required.

{
"scope": {
"resource_type": "filing_query",
"query": { "states": ["TX"], "product_lines": ["commercial_auto"] }
},
"delivery_target_id": "<your delivery_target_id>",
"client_reference": "subscription_demo_001"
}
Field Meaning
scope Either {"resource_type": "filing", "filing_id": "..."} or {"resource_type": "filing_query", "query": {...}}. The query has the same shape as in search.
delivery_target_id Your destination, such as <your delivery_target_id>, a value VelociFile gives you. It is set up for your account, never supplied through the API.
client_reference Your own ID for this subscription, up to 128 characters. It is copied into every event.

Scope, client_reference and delivery_target_id cannot be changed after creation.

Creation records a baseline atomically: the current version of the filing, or every current match of the query. Nothing is sent for what already exists. Events begin with changes after starts_at.

  • Idempotency-Key (1 to 128 visible ASCII characters) makes a retry safe. Repeating the same key with the same body returns the original response. The same key with a different body answers 409 IDEMPOTENCY_CONFLICT. Keys are kept for 7 days.
  • A client_reference identifies one watcher definition. Creating again with the same reference and the same definition returns the same watcher. The same reference with a different definition answers 409 CLIENT_REFERENCE_CONFLICT, and that holds after the watcher is deleted: references are never reused.
  • Two watchers with the same query and different references are independent. Pausing one leaves the other active.

A query watcher whose scope matches more than 25,000 filings is refused with 422 WATCHER_SCOPE_TOO_BROAD. Narrow the filters, for example by adding states or a product line, or create several watchers. See deviation 5. An account can hold 100 active or paused watchers. The 101st answers 422 WATCHER_LIMIT_REACHED.

import os
import uuid
import httpx
BASE = "https://api.dev.velocifile.ai/v1"
headers = {"Authorization": f"Bearer {os.environ['VELOCIFILE_API_KEY']}"}
created = httpx.post(
f"{BASE}/watchers",
headers={**headers, "Idempotency-Key": str(uuid.uuid4())},
json={
"scope": {
"resource_type": "filing_query",
"query": {"states": ["TX"], "product_lines": ["commercial_auto"]},
},
"delivery_target_id": os.environ["VF_DELIVERY_TARGET_ID"],
"client_reference": f"docs_sample_{uuid.uuid4().hex[:12]}",
},
timeout=60,
)
created.raise_for_status()
watcher = created.json()
print(watcher["watcher_id"], watcher["status"], "revision", watcher["revision"])
# Pause it. If-Match carries the revision from the ETag.
paused = httpx.patch(
f"{BASE}/watchers/{watcher['watcher_id']}",
headers={**headers, "If-Match": created.headers["ETag"]},
json={"status": "paused"},
timeout=30,
)
paused.raise_for_status()
print("now", paused.json()["status"], "etag", paused.headers["ETag"])
# Delete it: terminal.
deleted = httpx.patch(
f"{BASE}/watchers/{watcher['watcher_id']}",
headers={**headers, "If-Match": paused.headers["ETag"]},
json={"status": "deleted"},
timeout=30,
)
deleted.raise_for_status()
print("now", deleted.json()["status"])

PATCH /watchers/{watcher_id} with {"status": "active" | "paused" | "deleted"}. The If-Match header is required and carries the watcher revision, which is the quoted number in the ETag response header (for example "3").

Situation Response
If-Match missing 428 PRECONDITION_REQUIRED
If-Match does not match the current revision 412 PRECONDITION_FAILED. Read the watcher and try again.
Same status as now 200, revision unchanged
A real change 200, revision increases by one, new ETag
Any change to a deleted watcher 422 CAPABILITY_UNSUPPORTED (deleted is terminal)

A paused watcher keeps accumulating events, and deliveries for them are held. When you resume it, the held deliveries are released and the retry schedule restarts from the first attempt. Deleting a watcher stops delivery of its queued events. A deleted watcher stops matching, and its event history stays readable for 30 days.

change_type Meaning
created A filing matched after the watcher started.
query_entered An existing filing began to match your query.
query_left A filing in your baseline no longer matches.
amended A new version with an amendment.
corrected A new version from a source or metadata correction.
withdrawn The filing was withdrawn.

observed_at is the source’s change date at 00:00Z. It is never later than the moment VelociFile detected the change (emitted_at).

Change detection runs every four minutes, and an event is created within 5 minutes of a qualifying change landing in the catalog. Each watcher has its own contiguous sequence starting at 1. The event body is fixed at creation, so a replayed event is the same event. The payload is described in Webhooks.

GET /watchers/{watcher_id}/events?after_sequence=N&limit=100 returns events with sequence > N. after_sequence is required. Use 0 to start from the beginning.

{
"watcher_id": "wch_...",
"high_watermark": 42,
"oldest_available_sequence": 1,
"items": [],
"next_after_sequence": null
}
Field Meaning
high_watermark The highest sequence that exists for this watcher.
oldest_available_sequence The oldest sequence still stored. Events are kept 30 days.
next_after_sequence Pass as the next after_sequence. null when you are caught up.
  • after_sequence above high_watermark answers 400 INVALID_SEQUENCE.
  • If the next event you need has been purged, you get 410 REPLAY_WINDOW_EXPIRED, not an empty page. Rebuild your state from search, then resume from high_watermark.
import os
import httpx
BASE = "https://api.dev.velocifile.ai/v1"
headers = {"Authorization": f"Bearer {os.environ['VELOCIFILE_API_KEY']}"}
def replay(watcher_id: str, last_seen: int) -> int:
"""Return the highest sequence processed. Call again later with that value."""
after = last_seen
while True:
response = httpx.get(
f"{BASE}/watchers/{watcher_id}/events",
headers=headers,
params={"after_sequence": after, "limit": 100},
timeout=30,
)
response.raise_for_status()
page = response.json()
for event in page["items"]:
print(event["sequence"], event["change_type"], event["filing_id"])
last_seen = event["sequence"]
if page["next_after_sequence"] is None:
return last_seen
after = page["next_after_sequence"]
listing = httpx.get(f"{BASE}/watchers", headers=headers, params={"limit": 5}, timeout=30)
listing.raise_for_status()
for watcher in listing.json()["items"]:
print(watcher["watcher_id"], "caught up to", replay(watcher["watcher_id"], 0))