Skip to content

REST API

indice serve exposes a small HTTP API alongside the human-facing pages. The base URL is wherever you serve (default http://127.0.0.1:8080); behind a reverse proxy everything sits under whatever path you mount it at.

Full-text search as JSON, using the same engine behind the search page.

Query parameters

Param Required Description
q yes The query. Supports the full search syntax, including field filters like site:example.com, year:2021, type:pdf, lang:en, and collection:<slug>, so facets are expressed inside q. Capped at 2048 bytes; a longer one gets 400 naming the limit.
limit no Max results to return. Default 20, capped at 200.
Terminal window
curl 'http://127.0.0.1:8080/api/search?q=climate+site:example.com&limit=5'

Response (illustrative, abbreviated):

{
"total": 264,
"capped": false,
"results": [
{
"doc_type": "html",
"url": "https://example.com/report",
"domain": "example.com",
"timestamp": "20250802185600",
"title": "Climate report: annual summary",
"crawl_id": "4eefdff3",
"crawl_name": "example crawl",
"collection": "example-collection",
"snippet": "…an annual <mark>climate</mark> report…",
"capture_count": 3,
"status": 200
}
],
"facets": [
{ "field": "collection", "label": "Collection",
"buckets": [ { "value": "example-collection", "count": 220 } ] },
{ "field": "year", "label": "Year",
"buckets": [ { "value": "2025", "count": 129 } ] }
]
}

Fields

  • total: number of matching documents; capped is true when that count is approximate (very large result sets).
  • results[]: up to limit hits, each with: doc_type (media bucket, e.g. html, pdf, or annotation for a note), url, domain, timestamp (the capture time), title, author (set for annotation hits), crawl_id (8-char id) and crawl_name, collection (slug), snippet (a hit-highlighted excerpt, with <mark> around matches), capture_count (how many times this URL was captured, grouped into one result), and status (the archived HTTP status).
  • facets[]: facet groups, each { field, label, buckets: [{ value, count }] }, matching the sidebar on the search page.

The public read side of annotations: the notes on one capture, or every note in a collection.

Query parameters

Param Required Description
collection yes The collection slug.
url no A capture URL. Provide with ts to get the notes on that one capture; omit both for every note in the collection.
ts no The 14-digit capture timestamp. Must be given together with url.
Terminal window
curl 'http://127.0.0.1:8080/api/annotations?collection=example-collection'

Response (abbreviated): { "can_annotate": false, "annotations": [ … ] }, where each note carries its id, author (a display name, never the login identity, since this endpoint is public), created/modified, the target url + timestamp, the selector (the quoted passage, for a passage note), and the note body as both note_md (Markdown) and note_html (sanitized). can_annotate reflects whether the current request is allowed to write.

Creating, editing, and deleting notes are management-gated write endpoints (POST /api/annotations, POST /api/annotations/{id}, POST /api/annotations/{id}/delete). See the management write API below.

A liveness check: returns 200 OK with the body ok. Useful for container/orchestrator health probes.

Routes that should answer quickly carry a 30-second budget and return 503 if they exceed it. That is a backstop rather than a service level: in practice these answer in milliseconds, and a 503 here means something is wrong on the server, not that you asked for too much.

Four kinds of request are deliberately outside it, because they have no sensible time bound: GET /files/{id} streams whole archives, the SSE progress stream follows a job that may run for hours, uploads are as slow as the link, and the Browsertrix and Archive-It browse endpoints wait on someone else’s service.

A server limits each client. A workstation doesn’t.

  • Most routes allow a burst of 100 requests per client, then 20 a second. Past that the answer is 429 with Retry-After. Across all clients, at most 256 of these requests are in flight at once, and beyond that the answer is an immediate 503 with Retry-After.
  • GET /files/{id} isn’t rate-limited, because one replay makes hundreds of ranged reads. Instead a client may hold 64 streams open at once, and 512 may be open in total. A stream stays open until its body has been read or dropped.
  • /health is never limited.

A client is its IP address, and for IPv6 its /64. Behind a reverse proxy on the same machine or network, indice uses the rightmost X-Forwarded-For entry that isn’t itself internal. The shipped Caddy stack sets that header. Curators and admins are exempt from the per-client limits, so a script that reads hard needs a roster entry.

The operator sees a refusal in the server log twice: once when it starts, naming the address and the limit, and once a minute later with how many requests were refused. One line per refused request would turn a flood the limits are turning away into a flood of log lines.

These serve ReplayWeb.page and are handy for tooling, though their primary consumer is the in-browser player. They answer on both the site and the replay origin; the player reads them from the replay origin:

  • GET /files/{id}: the raw WACZ for a crawl, honoring HTTP Range requests (partial fetches). This is how replay reads archived resources without downloading the whole file.
  • GET /collection/{id}/replay.json: a multi-WACZ replay manifest for a collection (every member crawl), handed to wabac.js so cross-crawl links resolve.
  • GET /collection/{id}/pages: the collection’s page list and URL→capture resolution, scoped to the collection.

See How indice works for how these fit together.

indice also carries write endpoints under /api/ (add archives, create or edit collections, delete crawls, Browsertrix and Archive-It import), plus SSE progress streams. These power the workroom UI. They are always mounted; who may call them is decided by Access, which is loopback trust on a workstation and a forward-auth proxy plus users.yaml on a server. An anonymous caller gets a 403. Treat them as an implementation detail of that UI rather than a stable public API.