Every HTTP route and its SDK wrapper. The route inventory changes with each SDK
release; AGENTS.md carries only the auth rule and a pointer here. The bus
contracts Archiver produces and consumes are in BUS.md and
BUS_CONSUMERS.md.
The Archiver exposes authoring helpers under /api/v1/tools/* and mutating sub-resource routes under /api/v1/info-items/{id}/*. All routes use X-API-Key auth (only /health and /openapi.json are open). Each route has an ergonomic SDK wrapper on ArchiverClient (v5.x; see CHANGELOG.md for version history).
Domain endpoints (v4.1+):
| Endpoint | HTTP | SDK method |
|---|---|---|
| List Domains | GET /domains?is_active=&archived=&limit=&offset= |
list_domains(is_active=None, archived=None, limit=None, offset=None) |
| Get a Domain | GET /domains/{name} |
get_domain(name) |
| Upsert a Domain | PATCH /domains/{name} |
upsert_domain(name, notes=None, is_active=None) |
| Delete a Domain | DELETE /domains/{name} |
delete_domain(name) (409 if sources exist) |
| Archive a Domain | POST /domains/{name}/archive |
archive_domain(name) |
| Restore a Domain | POST /domains/{name}/restore |
restore_domain(name) |
InfoSourceOut gains domain_name: str | None (hostname auto-set from URL at create time).
GET /info-sources gains ?domain_name= filter.
Tools: read-only authoring helpers, plus operator controls that write - the registry republish, DLQ discard/reprocess (#238) and outbox rearm/discard (#191). Those take production action on 8000 and run only when the operator asks.
| Tool | HTTP | SDK method |
|---|---|---|
validate_source_spec |
POST /tools/validate-source-spec |
validate_source_spec(doc) |
validate_rep_spec |
POST /tools/validate-rep-spec |
validate_rep_spec(doc) |
validate_rep_fields |
POST /tools/validate-rep-fields |
validate_rep_fields(bag, required_fields=None) — a required <key>_slug is satisfied by its raw field (archiver#206); a raw value that normalizes to nothing derives no companion and stays missing |
validate_watch_spec |
POST /tools/validate-watch-spec |
generated only (no hand-written wrapper — no SDK consumer yet) |
| Republish registry announcements | POST /tools/republish-registry-announcements |
generated only (no hand-written wrapper — operator control, 202; 409 when the bus is dormant) |
| List a DLQ | GET /tools/dead-letters/{dlq} |
generated only (operator control, archiver#238): paginated {items, has_more, limit, offset}, oldest first, each entry decoded against the running co-core, with its provenance (original source_id, parking group/consumer, reason), parked_as (handler_poison / undecodable, from the reason's prefix) and owned (archiver's own group parked it); {dlq} outside content.revisions.dlq / content.artifacts.dlq is a 422, a dormant bus a 409, an unreadable broker a 503 |
| Reprocess DLQ entries | POST /tools/dead-letters/{dlq}/reprocess |
generated only (operator control, archiver#238): same body as discard → {results: [{entry_id, outcome, detail}]}. Runs each owned entry through the queue's own handler and deletes what it settles (reprocessed); not_found, not_owned, undecodable, rejected (handler poison), failed and deferred stay. A broker failure part-way through is a 503 whose data carries results and in_doubt. Runbook: BUS_CONSUMERS.md |
| Discard DLQ entries | POST /tools/dead-letters/{dlq}/discard |
generated only (operator control, archiver#238): {"entry_ids": [...]} (1-500 exact <ms>-<seq>, each half a uint64) → {discarded, not_found}; each frame logged to journald, then XDELed; a broker failure part-way through is a 503 whose data carries discarded, not_found and in_doubt (the id whose delete was in flight, or null). Runbook: BUS_CONSUMERS.md |
| List dead-lettered outbox rows | GET /tools/outbox/dead-lettered |
generated only (operator control, archiver#191): paginated {items, has_more, limit, offset}, oldest dead-lettering first; each item {row_id, topic, event_type, payload, last_error, publish_attempts, created_at, dead_lettered_at, rearmable}. A database read, so it works bus-dormant. Runbook: BUS.md |
| Rearm dead-lettered outbox rows | POST /tools/outbox/dead-lettered/rearm |
generated only (operator control, archiver#191): {"row_ids": [...]} (1-500 ULIDs) → {results: [{row_id, outcome, detail}]}, one per distinct id in request order. rearmed clears dead_lettered_at and resets publish_attempts; not_found, refused (topic not info.changes) and rejected (payload still does not build) change nothing |
| Discard dead-lettered outbox rows | POST /tools/outbox/dead-lettered/discard |
generated only (operator control, archiver#191): {"row_ids": [...]} (1-500 ULIDs) → {discarded, not_found}; each row logged in full to journald, then deleted, in one transaction. A live or published row is never deleted and comes back in not_found |
resolve_rep_fields |
POST /tools/resolve-rep-fields |
resolve_rep_fields(bag) |
find_info_item |
GET /tools/find-info-items?q=… |
find_info_item(query, limit=20) |
fetch_and_render |
POST /tools/fetch-and-render |
fetch_and_render(url) |
preview_extraction |
POST /tools/preview-extraction |
preview_extraction(url, source_spec) |
propose_selectors |
POST /tools/propose-selectors |
propose_selectors(url, description, top_k=5) |
Mutating endpoints:
| Endpoint | HTTP | SDK method |
|---|---|---|
| Atomic InfoItem create | POST /info-items |
create_info_item(name, ..., initial_url=None, initial_source_specs=None, initial_rep_spec_assignments=None, rep_fields=None) |
| Bind a Source to an Item | POST /info-items/{id}/info-sources |
add_info_source(info_item_id, info_source_id) |
| Deactivate a source binding | DELETE /info-items/{id}/info-sources/{source_id} |
deactivate_info_source_binding(info_item_id, info_source_id) |
| Delete an InfoItem | DELETE /info-items/{id} |
delete_info_item(info_item_id) |
| Author a top-level InfoSource | POST /info-sources |
create_info_source(url, source_specs) |
| Update InfoSource specs | PATCH /info-sources/{id}/source-specs |
update_info_source_specs(info_source_id, source_specs) |
| Get an InfoSource | GET /info-sources/{id} |
get_info_source(id) |
| List InfoSources (filter by URL or domain, paginated) | GET /info-sources?url=…&domain_name=…&limit=&offset= |
list_info_sources(url=None, domain_name=None, limit=None, offset=None) |
| Author a RepSpec | POST /rep-specs |
create_rep_spec(provider, name, document) |
| Get a RepSpec | GET /rep-specs/{id} |
get_rep_spec(id) |
| Update a RepSpec (name always; document only while draft) | PATCH /rep-specs/{id} |
update_rep_spec(id, name=None, document=None) |
| List RepSpecs (filter by provider, paginated) | GET /rep-specs?provider=…&limit=&offset= |
list_rep_specs(provider=None, limit=None, offset=None) |
| Assign a RepSpec | POST /info-items/{id}/rep-spec-assignments |
assign_rep_spec(info_item_id, rep_spec_id, activated_at=None) |
| Deactivate an assignment | DELETE /info-items/{id}/rep-spec-assignments/{aid} |
deactivate_rep_spec_assignment(info_item_id, assignment_id) |
| Public-URL writeback | PATCH /info-items/{id}/rep-spec-assignments/{aid} |
set_public_url(info_item_id, assignment_id, public_url) |
| Replace an item's rep_fields bag | PUT /info-items/{id}/rep-fields |
set_rep_fields(info_item_id, rep_fields, allow_destination_change=False) |
| Replace an item's cadence policy | PUT /info-items/{id}/watch-spec |
generated only (no hand-written wrapper — no SDK consumer yet) |
| Pause / resume an item | PUT /info-items/{id}/watch-active |
generated only (no hand-written wrapper — no SDK consumer yet) |
| Record a SourceRevision (idempotent) | POST /source-revisions |
post_source_revision(...) |
| Clear cache fields | PATCH /source-revisions/{id} |
patch_source_revision_cache(id, content_cache_uri=None, content_cache_expires_at=None) |
POST /info-sources accepts {url, source_specs}. Multiple InfoSources at the same URL are valid. Returns 422 on invalid URL or spec validation failure.
POST /info-items/{id}/rep-spec-assignments refuses before the RepSpec document freezes (#83),
because afterwards nothing can fix it: 422 code="rep_fields_incomplete" (one error per missing
required_fields key) or code="rep_fields_unrenderable" (path /rep_fields; the bag is present
but cannot render path_template, e.g. "WA LCB" as a path segment - this was a 500 until
archiver#301); 409 kind="conflict" when the spec is already actively assigned to the item, with
data.existing_assignment_id - deactivate it first. POST /info-items refuses the same spec listed
twice in initial_rep_spec_assignments (422, code="duplicate_assignment"). One active row per
(item, spec) is a database invariant (SCHEMA.md § InfoItemRepSpec).
PUT /info-items/{id}/rep-fields accepts {rep_fields, allow_destination_change=false} and
replaces the bag whole (archiver#302). It is the only way to change a bag after create, and it
holds every later bag to the standard assignment did, so it cannot break an active assignment. The
InfoItem row is locked FOR UPDATE, and assign_rep_spec takes the same lock, so a save and a
concurrent assign cannot each pass on their own snapshot. Refusals leave the stored bag untouched:
- 422
code="rep_fields_invalid": not Rep Fields v1 (src/core/rep_fields_schema/v1.json).POST /info-itemsnow refuses the same, assignments or not. - 422
kind="domain",code="rep_fields_incomplete"/"rep_fields_unrenderable": the bag would break an active assignment. Every broken assignment is reported, not just the first:data.refusalsis[{assignment_id, rep_spec_id, rep_spec_name, code, errors}], and eachFieldErrormessage names its RepSpec. - 409
kind="conflict",code="rep_fields_moves_destination": the bag is valid, but an active assignment would render to a different path from now on, beside everything already published at the old one in a permanent bucket.data.movesis[{assignment_id, rep_spec_id, rep_spec_name, before, after}], rendered against one placeholder occasion so the two differ only where the bags do. Resend withallow_destination_change: trueto store it; the move is logged at WARNING. Repairing a bag that could not render is not a move.
Error paths point into the request body (/rep_fields/org/title_slug). Nothing is announced:
rep_fields rides no bus stream. POST /info-items uses two path conventions: its new
rep_fields_invalid paths are body-relative like these, but its rep_fields_incomplete paths
stay bag-relative (/org/title_slug), as they were before #302. Changing them would break
existing callers.
DELETE /info-items/{id} returns 204 and cascades the item's source bindings and rep-spec
assignments; the InfoSource and its SourceRevisions survive (the physical layer is shared). 404 on
an already-deleted item, not a silent 204. It exists to give the registry's exit a transactional
home (archiver#141): "gone from the registry" is announced as a revoked tombstone that has to be
written in the deletion's own transaction, which raw SQL cannot do — and the periodic full republish
does not repair a missed one, since absence-from-a-full-set is deliberately not the delete signal.
The deletion is announced as a tombstone on info.registry (BUS.md), but watcher#254 does not document
tombstone handling, so an orphaned WatchedItem may still need removing there by hand. Deleting an
item that Watcher has reported on (a watch_status row exists) logs a WARNING naming it, so the
cleanup is discoverable from journald rather than only from this paragraph.
PUT /info-items/{id}/watch-spec accepts {document} and replaces the cadence document whole
— it is not a merge, because omitting interval is the only way to say "the consumer applies its
own default" and a merge would make that state unreachable once an interval had been set. Invalid
documents return the 422 envelope with per-field errors and leave the stored policy untouched,
including a pre-rework document that still nests active (rejected, not silently dropped).
Both policy PUTs store on any item but announce only an announceable one (archiver#167): an
active binding whose source has non-empty source_specs. Pause and cadence cannot change
announceability, so on an unbound or spec-less item the write is stored and info.registry stays
silent, with no tombstone and no generation bump. The first live announcement after a bind carries
the stored policy, so configuring an item before binding it works.
PUT /info-items/{id}/watch-active accepts {active: bool} — required, idempotent. Two routes
rather than one body because the two fields need opposite absence rules: an omitted interval
means "consumer default", while pause state has no omitted case at all (NULL is reachable only by
never having written). Splitting them also keeps a dashboard pause from becoming a read-modify-write
of a cadence document it does not otherwise touch. watch_spec and watch_active are both additive
on InfoItemOut. Contract and rationale: SCHEMA.md.
Pagination: GET /info-items, GET /info-sources, and GET /rep-specs return a Page envelope — {items, has_more, limit, offset}. All accept limit (default 100, max 500) and offset (default 0) query params. Ordering is stable: (created_at, id). has_more is computed via a limit+1 probe — no total count. SDK methods list_info_items / list_info_sources / list_rep_specs return PageInfoItemOut / PageInfoSourceOut / PageRepSpecOut; pass limit/offset to forward to the server.
SDK version history: see CHANGELOG.md.