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.
Create a watcher
Section titled “Create a watcher”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.
Baseline: no backlog
Section titled “Baseline: no backlog”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 and client references
Section titled “Idempotency and client references”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 answers409 IDEMPOTENCY_CONFLICT. Keys are kept for 7 days.- A
client_referenceidentifies one watcher definition. Creating again with the same reference and the same definition returns the same watcher. The same reference with a different definition answers409 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.
The 25,000 cap
Section titled “The 25,000 cap”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 osimport 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"])REF="docs_sample_$(date +%s)"curl -sS --fail-with-body -i https://api.dev.velocifile.ai/v1/watchers \ -H "Authorization: Bearer $VELOCIFILE_API_KEY" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d "{\"scope\": {\"resource_type\": \"filing_query\", \"query\": {\"states\": [\"TX\"], \"product_lines\": [\"commercial_auto\"]}}, \"delivery_target_id\": \"$VF_DELIVERY_TARGET_ID\", \"client_reference\": \"$REF\"}"Pause, resume and delete
Section titled “Pause, resume and delete”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.
Events
Section titled “Events”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.
Read and replay history
Section titled “Read and replay history”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_sequenceabovehigh_watermarkanswers400 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 fromhigh_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))