Agent integration guide¶
Standalone Docbank is daemon-first: the CLI, external agents, and scripts all
use the same HTTP API. An external integration never opens docbank.db or the
blob store directly. Go applications that own a separately rooted archive can
instead use the embedded API.
Choose the interface¶
Use the CLI for human-directed shell work and simple orchestration. Use HTTP for structured agent workflows, pagination, machine-readable errors, and revision-aware mutations.
For simple shell orchestration, CLI exit codes distinguish invalid usage (2),
missing vault objects (3), stale state (4), busy resources (5), and
integrity findings (6) from general failures (1). Verification may emit a
complete report before exiting 6; never infer success merely because stdout
contains JSON. The CLI reference
defines the full contract. Independent integrations should use the richer HTTP
problem code values below.
For a small shell workflow, mv, rm, and restore accept --json and
return the daemon's complete resulting node receipt. A trash receipt's path
is only its pre-trash recovery context; carry the stable id and revision
forward instead.
Before operating on an unfamiliar machine or switching archives, identify the selected vault explicitly:
Treat vault_id as the durable identity and vault_path as machine-local
placement. DOCBANK_HOME=/path/to/another/vault docbank info --json selects
and confirms another independently owned archive without changing a global
profile or opening its database directly.
A workflow that depends on watched ingestion can inspect the daemon's effective configuration and runner state rather than assuming the local file is active:
Each item reports the machine-local source, virtual destination, full settle
window, optional minimum source age, scan interval, literal exclusions, and
current job record. A nonzero minimum age composes with rather than replaces
the settle window, which is useful when append-heavy session JSONL may pause
without being complete. This is a read-only view; changing config.toml still
requires a daemon restart.
The canonical contract is generated from the running route definitions:
A running daemon also serves /openapi.json, /openapi.yaml, and interactive
docs at /docs. These contract routes are auth-exempt so a client generator
can discover them before authentication is configured.
The rendered documentation is available to people at directory routes such as
/agents/integration/. The same maintained source is published for agents at
the sibling /agents/integration.md URL.
Give an independent client a stable endpoint¶
The docbank CLI can discover an ephemeral port and per-run key from the same-user runtime record. An independent long-lived client should instead use an explicit loopback port and a strong API key:
# ~/.docbank/config.toml
[server]
bind_addr = "127.0.0.1"
api_port = 7486
api_key = "replace-with-a-long-random-secret"
idle_timeout = "0"
Restart after changing config:
The daemon rejects non-loopback binds. Remote access is not a separate mode: use an SSH tunnel or VPN that terminates at the daemon host's loopback listener, and protect the API key as a vault credential.
Examples below assume:
Prove reachability and authentication separately¶
/health is intentionally auth-exempt:
Then make an authenticated request. Resolve / to obtain the root node and
its ID:
curl --fail-with-body --get \
-H "Authorization: Bearer $DOCBANK_API_KEY" \
--data-urlencode 'path=/' \
"$DOCBANK_URL/api/v1/path"
A node response includes stable id, mutable revision, kind, and
timestamps. File nodes also include stable current_version_id, immutable
SHA-256 blob_hash, and raw size; directories omit content identity. Live
single-node responses include the current path. Node and version IDs survive
renames; use a live path only for display or a one-shot path operation.
The CLI exposes the same distinction without requiring JSON parsing. Human
listings print copyable selectors such as id:42, and existing-node commands
accept either that stable selector or an absolute path:
docbank stat id:42 --json
docbank cat id:42
docbank versions list id:42 --json
docbank mv id:42 /review/approved.pdf --json
Use docbank stat when a shell agent needs one authoritative node snapshot.
Its JSON includes the node revision and, for files, the current version,
SHA-256, size, and MIME type. A trashed ID remains inspectable but has no live
path.
The mv destination stays a path because it describes a new coordinate. In
JSON and HTTP requests, node IDs remain numeric rather than id: strings.
Trash is the important exception. A successful trash response returns the
node's pre-trash path to explain where a restore would try to put it. That
path no longer resolves to the trashed node and may later resolve to a different
node if its name is reused. Retain the response's id and revision for
subsequent ID-addressed inspection or restore, and treat every path attached to
a trashed node as display or recovery context rather than identity.
Read a tree without unbounded responses¶
The CLI tree view is bounded by default to four levels and 1,000 nodes. Set
explicit limits for the task and inspect truncated plus omissions before
assuming the result is complete:
Use --all only when the complete subtree is known to be appropriately sized.
For finer control, directory children are paginated and sorted with directories
first, then by name. Use total, limit, and offset until the required page
set is read:
curl --fail-with-body \
-H "X-Api-Key: $DOCBANK_API_KEY" \
"$DOCBANK_URL/api/v1/nodes/1/children?limit=500&offset=0"
Search is bounded separately. Always inspect truncated; increase the limit
or refine the query rather than assuming the returned array is complete.
Each result's match is name or content. Name matches keep their established
ranking and always precede content-only matches, so adding content indexing does
not reorder an agent's filename-based workflow. Content search covers only the
current version of verified UTF-8 plain text, Markdown, JSON, and JSONL documents
up to 16 MiB. Extraction runs asynchronously; after a write, inspect
docbank jobs or retry briefly instead of treating an immediate content miss as
permanent. PDF, Office, image, and OCR text extraction are not implemented.
To restrict the same ranking to one current tag assignment, send the canonical
tag UUID as tag_id; retain the echoed tag_id as the filter authority rather
than relying on the tag's mutable display name.
To restrict results by the current file version's format, send a valid
parameter-free base type as mime_type. The response echoes its normalized
spelling. For example, text/plain includes stored charset parameters but does
not match a retained historical version or a directory.
To restrict results to one subtree, resolve its live directory once and send
the stable ID as under_node_id. The selected directory is excluded; all
descendant name and content candidates share the same constraint. Retain the
echoed ID as authority instead of treating the directory's mutable path as the
filter identity.
Use modified_since and modified_before for an absolute current-node time
window. The first bound is inclusive and the second exclusive. Both accept
RFC3339 offsets; the response echoes canonical UTC values. These fields refer
to the live node's modified_at, not source-file provenance or historical
content-version time.
curl --fail-with-body --get \
-H "X-Api-Key: $DOCBANK_API_KEY" \
--data-urlencode 'q=tax return' \
--data 'tag_id=<tag-uuid>' \
--data-urlencode 'mime_type=application/pdf' \
--data 'under_node_id=42' \
--data-urlencode 'modified_since=2026-01-01T00:00:00Z' \
--data-urlencode 'modified_before=2026-04-01T00:00:00Z' \
--data 'limit=100' \
"$DOCBANK_URL/api/v1/search"
For shell automation, prefer get when the bytes must become a local file. It
keeps incomplete bytes private and emits a structured proof receipt only after
the complete stream verifies and is atomically published:
Independent HTTP clients should retrieve file bytes by ID, not path:
curl --fail \
-H "X-Api-Key: $DOCBANK_API_KEY" \
"$DOCBANK_URL/api/v1/nodes/42/content" \
--output document.bin.staging
The content response sends X-Docbank-Content-Version,
X-Docbank-Blob-Hash, and X-Docbank-Blob-Size before the body. After the body
it sends an
RFC 9530 Content-Digest
trailer computed from the bytes actually streamed. A client
that needs independent transfer proof writes to private staging, hashes the
staged bytes itself, and compares that digest, the trailer, and the file node's
blob_hash. Require the version
header to equal the node's current_version_id; do not treat catalog headers
alone as a fresh physical verification. Sync and close the staging file before
publishing it at the caller-visible destination.
List a node's immutable versions with bounded pagination, then address one record or byte stream without relying on its current path:
curl --fail-with-body \
-H "X-Api-Key: $DOCBANK_API_KEY" \
"$DOCBANK_URL/api/v1/nodes/42/versions?limit=100&offset=0"
curl --fail-with-body \
-H "X-Api-Key: $DOCBANK_API_KEY" \
"$DOCBANK_URL/api/v1/versions/$VERSION_ID"
curl --fail \
-H "X-Api-Key: $DOCBANK_API_KEY" \
"$DOCBANK_URL/api/v1/versions/$VERSION_ID/content" \
--output version.bin
The listing is newest-first and returns items, total, limit, and
offset. A version record includes its node, node revision, blob identity,
recording time, transition kind, and introducing operation UUID. Version-byte
responses use the same headers and terminal digest contract as current-node
content.
Resolve known bytes to every authoritative node/version reference without guessing from paths or physical storage:
curl --fail-with-body --get \
-H "X-Api-Key: $DOCBANK_API_KEY" \
--data-urlencode "sha256=$SHA256" \
--data 'limit=100' \
--data 'offset=0' \
"$DOCBANK_URL/api/v1/content-references"
The response is a bounded page ordered with live current references first, then live prior versions, then trash. A result's path is present only for a live node. No result means no logical content version currently retains the hash, even if unreferenced physical bytes have not yet been swept by GC.
Read the response through successful EOF and require the trailer. A readable prefix is not verified content: if the request is cancelled, the body ends in error, or the trailer is absent, discard any staged output rather than publishing it. Docbank does not drain an abandoned response merely to complete verification.
For a bounded server-side check, send the revision from the node response:
curl --fail-with-body -X POST \
-H "X-Api-Key: $DOCBANK_API_KEY" \
-H 'If-Match: "7"' \
"$DOCBANK_URL/api/v1/nodes/42/verify"
A successful proof returns blob_hash, computed_hash, size,
computed_size, and verified: true, bound to node_id, version_id, and
revision.
Missing or damaged content returns HTTP 200 with verified: false and
problem: "missing", "corrupt", or "unreadable"; those are completed
checks with negative evidence, not request failures. A 412 stale_revision
means the node changed during or since inspection—read it again before deciding
what content to verify.
Organize with stable tags¶
Create a tag once, then retain its returned UUID and revision/ETag. Names are mutable display labels; IDs are durable authority. A tag revision covers both its definition and complete assignment set.
curl --fail-with-body -X POST \
-H "X-Api-Key: $DOCBANK_API_KEY" \
-H "Content-Type: application/json" \
--data '{"name":"taxes"}' \
"$DOCBANK_URL/api/v1/tags"
curl --fail-with-body -X PUT \
-H "X-Api-Key: $DOCBANK_API_KEY" \
-H 'If-Match: "7"' \
"$DOCBANK_URL/api/v1/nodes/42/tags/$TAG_ID"
Assignment receipts return changed, the resulting node revision/ETag, and
the tag's current revision and assignment count. changed: false is a successful
idempotent convergence, not an error. Page GET /nodes/{id}/tags and GET
/tags/{tag_id}/nodes; the latter includes trashed nodes without pretending
they have a live path. Set live_only=true when one bounded response must
contain only live nodes and paths from the same metadata snapshot;
omitted_trashed reports the assignments excluded by that projection. Rename
and delete by UUID with the most recently
inspected tag ETag in If-Match; a concurrent definition or assignment change
returns 412 stale_revision. Deleting a tag removes assignments only, never
nodes or document bytes.
When the desired target is a path, send {"path":"/records/report.pdf"} to
PUT or DELETE /path/tags/{tag_id}. Do not resolve the path with GET /path
and then mutate by node ID: an ancestor can move without advancing the target
node's revision. The path endpoint resolves and changes authority in one store
transaction. Use the ID-addressed form only when the stable node ID itself is
the intended authority.
Follow backup progress without scraping a CLI¶
Agents that create snapshots can use POST
/api/v1/backup/snapshots/stream. It accepts the same JSON object as the
single-response endpoint:
curl --no-buffer --fail-with-body \
-H "X-Api-Key: $DOCBANK_API_KEY" \
-H "Content-Type: application/json" \
--data '{"repo":"/absolute/server/path","tag":"before-edit","jobs":1}' \
"$DOCBANK_URL/api/v1/backup/snapshots/stream"
The response is NDJSON. Each progress line contains stage, done, total,
bytes_done, bytes_total, and final. The last line is either result with
the stable snapshot summary or error with the normal problem fields. Treat
EOF before that terminal line as failure. In particular, do not interpret HTTP
200 as snapshot success: it only confirms that streaming began.
Inspect daemon background work¶
Before relying on a configured background feature, inspect its task state:
The response is {items: [...]}, sorted by stable task name. Branch on
status: running is active; completed, failed, and cancelled are
terminal for this daemon run. Surface a failed task's bounded error to the
operator, but do not parse its prose as a protocol. An absent item is not proof
that work completed—it can mean the feature is unconfigured or the daemon
restarted, because status history is intentionally process-local.
Use revisions for read-modify-write¶
ID-addressed move, trash, and restore operations require If-Match. The
revision belongs to the node state the agent evaluated:
# A prior GET returned id=42 and revision=7.
curl --fail-with-body -X PATCH \
-H "X-Api-Key: $DOCBANK_API_KEY" \
-H 'If-Match: "7"' \
-H 'Content-Type: application/json' \
--data '{"new_parent_id": 18, "new_name": "filed.pdf"}' \
"$DOCBANK_URL/api/v1/nodes/42"
If another actor changed the node first, the API returns 412 with
code: "stale_revision". Do not blindly replay the old decision:
- Re-read the node by ID.
- Re-evaluate the intended move, name, or deletion against its new state.
- Retry with the new revision only if the intent still applies.
- Bound retries; repeated conflicts require human or higher-level policy.
A missing precondition returns 428 precondition_required. An invalid header
returns 400 validation.
Content replacement follows the same read-decide-write rule and adds byte evidence. Compute the local file's SHA-256 and size, retain the revision from the node response, then send raw bytes:
curl --fail-with-body -X PUT \
-H "X-Api-Key: $DOCBANK_API_KEY" \
-H 'If-Match: "7"' \
-H "X-Docbank-Blob-Hash: $SHA256" \
-H "X-Docbank-Blob-Size: $SIZE" \
-H 'Content-Type: application/pdf' \
--data-binary @revised.pdf \
"$DOCBANK_URL/api/v1/nodes/42/content"
Do not accept HTTP 200 alone. Require the receipt's computed_hash and
computed_size to equal the local values; require its node and version to both
name node 42; require content_replace, the next node revision, matching blob
identity, and node.current_version_id == version.id; and require the response
ETag to encode that resulting revision. The old version remains addressable.
A 412 means the decision is stale—re-read and decide again rather than
blindly retrying with a fresh revision.
docbank edit is a human-directed wrapper around this same contract: it opens
an interactive local editor and intentionally has no JSON mode. Agents should
use the raw replacement API or typed client so they can retain and validate the
full byte-identity receipt themselves.
Reversion applies the same concurrency rule without uploading bytes. Select a prior version belonging to the inspected node and send:
curl --fail-with-body -X POST \
-H "X-Api-Key: $DOCBANK_API_KEY" \
-H 'If-Match: "8"' \
-H 'Content-Type: application/json' \
--data '{"source_version_id":"11111111-1111-4111-8111-111111111111"}' \
"$DOCBANK_URL/api/v1/nodes/42/revert"
Require source_version.id to equal the requested ID and all three records to
name node 42. Require the new version to be content_revert, to name that source,
and to reproduce its hash, size, and media type exactly. The node must install
the new version at revision 9 and the ETag must agree. Reversion is metadata-only,
so it has no computed digest receipt; it relies on already-authoritative source
bytes and does not copy them. Use the ordinary content verification surface when
a fresh physical proof is part of the workflow.
Version retention is unlimited by default. To release unwanted non-current history, preview exactly one selector through the authenticated pruning route:
curl --fail-with-body -X POST \
-H "X-Api-Key: $DOCBANK_API_KEY" \
-H 'If-Match: "9"' \
-H 'Content-Type: application/json' \
--data '{"keep_newest":3}' \
"$DOCBANK_URL/api/v1/nodes/42/versions/prune"
The other request selectors are version_ids, older_than, and all_prior;
exactly one is allowed. Omitted or false run is a dry run. After evaluating
the returned candidate IDs, logical bytes, retained revert dependencies, and
loose/packed maintenance consequences, repeat with "run":true and the same
inspected revision. Do not blindly replace a stale If-Match: re-read the node
and re-evaluate the selection.
older_than is evaluated against the cutoff returned by each request. Time
can move versions across that boundary without changing the node ETag, so a
later age-based run can contain additional candidates. If execution must match
the preview exactly, send its candidate IDs through version_ids instead of
repeating the age selector. Explicit-ID requests accept at most 1,000 IDs; for
larger sets, execute batches and inspect the advanced node revision before
sending each next batch.
For a dry run, require the response node and ETag to match the inspected node
and revision, changed:false, deleted_versions:0, and no checkpoint. For an
executed change, require deleted_versions to equal the candidate count,
changed:true, and exactly one revision advance. When
checkpoint_required:true, execution must return a source-free
content_replace checkpoint installed as the current version. Blob counts must
partition into shared versus releasable. A releasable blob may have loose
locations pending GC, packed locations pending repack, or both;
mixed_blobs_pending_maintenance reports that overlap. The byte totals cover
every authoritative location across every store. These are future maintenance
candidates, not bytes reclaimed by pruning.
Path mutations are intentionally different. POST /api/v1/path/move and
POST /api/v1/path/trash resolve and mutate inside one store transaction, so
they do not accept If-Match. Use them for a one-shot instruction tied to the
path as it exists when the transaction runs. Use ID plus revision when an
agent previously inspected a particular node and wants lost-update protection.
For a reorganization that must not partially apply, send one bounded plan to
POST /api/v1/batch/move or use docbank mv batch. Each source is either an
absolute source_path, resolved inside the transaction, or a stable node_id
with the revision the agent inspected. All destination_path values are
exact final coordinates whose parents resolve in the planned final tree; an
existing directory does not invoke ordinary mv's “move into” shorthand.
Docbank validates the complete final tree before changing it. This permits file and
directory swaps without temporary names. Require a receipt for every request
item, in the same order, and reconcile its stable node ID, prior path, final
path, and resulting revision. Any error means the entire plan was rejected.
Inspect document provenance¶
Use the stable node ID to retrieve the immutable facts describing where a file was ingested from:
curl --fail-with-body \
-H "X-Api-Key: $DOCBANK_API_KEY" \
"$DOCBANK_URL/api/v1/nodes/42/provenance?limit=100&offset=0"
Do not confuse node.path, Docbank's current virtual coordinate, with a fact's
original_path, which names an external source as it was observed by the
ingest. Branch on active when the workflow needs facts that have not been
superseded, but retain fact identities: a correction adds a successor and keeps
the superseded record addressable. Paginate using total, limit, and
offset. A trashed file is
still inspectable by stable ID and returns an empty live path.
Provenance is evidence, not ownership of the external source. Reading it does not open or modify that source, and it does not make a content version a retention root.
Create and ingest safely¶
For a one-shot instruction tied to an exact virtual coordinate, create the directory by path. Its parent must already exist:
curl --fail-with-body -X POST \
-H "X-Api-Key: $DOCBANK_API_KEY" \
-H 'Content-Type: application/json' \
--data '{"path":"/receipts/2026"}' \
"$DOCBANK_URL/api/v1/path/mkdir"
The parent resolves inside the mutation transaction. When the workflow has already selected a particular stable parent identity, create beneath that ID instead so a concurrent parent move does not change the intended owner:
curl --fail-with-body -X POST \
-H "X-Api-Key: $DOCBANK_API_KEY" \
-H 'Content-Type: application/json' \
--data '{"parent_id": 1, "name": "receipts", "kind": "dir"}' \
"$DOCBANK_URL/api/v1/nodes"
A 409 exists response is not automatically success: resolve the existing
name and verify that it is the directory the workflow intended.
POST /api/v1/ingest reads absolute paths on the daemon host and is restricted
to loopback callers. It is not a file-upload endpoint:
For a large tree, inventory the exact selection first. This request reads filesystem metadata but does not open file content or mutate the vault:
curl --fail-with-body -X POST \
-H "X-Api-Key: $DOCBANK_API_KEY" \
-H 'Content-Type: application/json' \
--data '{"paths":["/Users/me/Dropbox"],"exclude":[".git",".Trash"]}' \
"$DOCBANK_URL/api/v1/ingest/preflight"
Require errors == 0 and rejected.files == 0, inspect every returned
finding, and retain the exact exclusion list for ingest. Findings and extension
groups are bounded; their count and truncation fields say when the detailed
arrays are samples rather than complete lists. A non-UTF-8 filesystem entry is
an error with an escaped printable path and is never opened or imported.
curl --fail-with-body -X POST \
-H "X-Api-Key: $DOCBANK_API_KEY" \
-H 'Content-Type: application/json' \
--data '{"paths":["/Users/me/Downloads/receipt.pdf"],"dest":"/receipts","exclude":[]}' \
"$DOCBANK_URL/api/v1/ingest"
Inspect added, skipped, excluded, and every member of failed. Repeating the same
ingest after fixing a partial failure is safe; successful content converges to
skipped rather than another copy.
For an interactive or long-running local integration, send the same body to
POST /api/v1/ingest/stream with Accept: application/x-ndjson. Read every
line through EOF. progress events cover the metadata-only scan and content
ingest stages; a result carrying the final report or an error is the
single terminal event. HTTP 200 means only that streaming started. Treat EOF
without a terminal event, malformed events, or data after the terminal event
as failure. Cancelling or disconnecting cancels traversal and the active blob
write; only files whose individual publication already completed retain
authority and are safely reported as skipped on a rerun.
Remote writers use a file-granular multipart request. Compute the expected identity before sending bytes, and address the destination by stable directory ID:
FILE=receipt.pdf
HASH=$(shasum -a 256 "$FILE" | awk '{print $1}')
SIZE=$(wc -c < "$FILE" | tr -d ' ')
curl --fail-with-body -X POST \
-H "X-Api-Key: $DOCBANK_API_KEY" \
-H "X-Docbank-Blob-Hash: $HASH" \
-H "X-Docbank-Blob-Size: $SIZE" \
-F 'file=@receipt.pdf;filename=receipt.pdf;type=application/pdf' \
"$DOCBANK_URL/api/v1/uploads?parent_id=18&name=receipt.pdf"
Clients must percent-encode a nontrivial name query value. The request has
exactly one part named file, and its multipart filename must equal name.
The hash and size headers describe that file payload; top-level
Content-Digest would instead describe the multipart envelope and is therefore
not the write precondition.
On 201, require status: "added"; an idempotent retry returns 200 with
status: "skipped" and the same stable node. In both cases compare
computed_hash and computed_size with the locally calculated values, then
retain node.id, node.revision, and node.blob_hash. A
digest_mismatch or size_mismatch is a failed write with no new node/blob
authority. Upload many files as independent requests so each result is
unambiguous and independently retryable.
The receipt proves receive-time agreement. Use the revision-bound single-node verification endpoint later when policy requires evidence about bytes currently stored in the vault.
Enroll permanent history only after an exact preview¶
Audit enrollment is irreversible. An agent must first preview one live directory by path or stable node ID:
curl --fail-with-body -X POST \
-H "X-Api-Key: $DOCBANK_API_KEY" \
-H 'Content-Type: application/json' \
--data '{"path":"/taxes","agent_label":"records policy"}' \
"$DOCBANK_URL/api/v1/audit/preview"
Present or evaluate the returned protected node/version counts, logical and
unique bytes, vault-wide evidence counts, and baseline_digest. First
activation permanently retains enrollment-time names, topology, tags,
assignments, ingests, and provenance across the vault, including outside the
selected scope. Unrelated content versions do not become scope members, but the
metadata snapshot remains evidence. Only after that review may a client execute
the exact daemon-held plan:
curl --fail-with-body -X POST \
-H "X-Api-Key: $DOCBANK_API_KEY" \
-H 'Content-Type: application/json' \
--data '{"preview_token":"<one-use-token>","acknowledge_permanent_retention":true}' \
"$DOCBANK_URL/api/v1/audit/enable"
The token expires after ten minutes, is consumed by one attempt, and does not
survive daemon restart. On audit_preview_stale, preview again; never retry the
same execution blindly. GET /api/v1/audit/status reports vault-wide evidence.
Add either ?path=/taxes/file.pdf or ?node_id=57 to inspect sticky membership.
Read that node's canonical timeline with exactly one selector:
curl --fail-with-body \
-H "X-Api-Key: $DOCBANK_API_KEY" \
"$DOCBANK_URL/api/v1/audit/history?node_id=57&limit=50"
Events are newest first and bind stable event, operation, scope, node-revision,
and optional path/content identities. A path state distinguishes /live/paths
from canonical @trash/known/... and @trash/unknown/... coordinates. Tag and
provenance events include an attachment object with a discriminating kind,
stable identity, and typed before/after states, so clients do not need to
decode canonical audit internals. Follow next_cursor to read older events;
send it back unchanged and never derive meaning from its encoding. The cursor
is node-bound and remains stable when newer operations append. Treat
audit_not_enrolled as a valid answer that the node is outside permanent
retention, and invalid_audit_cursor as a request error. A protected node may
have an empty timeline when it was adopted at enrollment and has not changed;
use status membership, not event count, to decide protection.
To answer “what changed anywhere in this protected scope?”, use the stable scope ID returned by audit status:
curl --fail-with-body \
-H "X-Api-Key: $DOCBANK_API_KEY" \
"$DOCBANK_URL/api/v1/audit/scopes/<scope-id>/history?limit=50"
The page includes current scope evidence and events from every protected
member. Reconcile each event's scope_id, retain its node_id as the stable
subject, and follow next_cursor unchanged. Scope cursors are bound to that
scope and remain append-stable when newer events arrive.
Independently replay the authority and hash every protected blob with:
curl --fail-with-body -X POST \
-H "X-Api-Key: $DOCBANK_API_KEY" \
"$DOCBANK_URL/api/v1/audit/verify"
A successful HTTP response can still report failed verification. Require empty
metadata_problems and problems, verified_blobs == protected_blobs, and a
non-null evidence object when enabled is true. Record that stable evidence
outside the vault when rollback detection matters: it contains vault and
allocation-lineage identities, allocation count/head, the operation high-water
mark, and every scope count/head. This endpoint hashes protected content only;
the top-level /api/v1/verify also covers unaudited blobs.
To check a later vault against that trusted record, send the prior successful
report's evidence object as expected:
jq -c '{expected: .evidence}' audit-evidence.json > audit-expected-request.json
curl --fail-with-body -X POST \
-H "X-Api-Key: $DOCBANK_API_KEY" \
-H 'Content-Type: application/json' \
--data-binary @audit-expected-request.json \
"$DOCBANK_URL/api/v1/audit/verify"
Require evidence_check.extends: true as well as clean metadata and blob
results. Equal chains and valid extensions pass. Stable problem codes
are audit_not_enabled, vault_mismatch, lineage_mismatch,
allocation_shorter, allocation_diverged, scope_missing, scope_shorter,
and scope_diverged. Evidence mismatch is reported in the body rather than as
an HTTP transport error so agents can inspect the current verified evidence and
byte state before escalating.
The current public boundary permits one permanent scope per vault.
Treat destructive maintenance as a two-step decision¶
Trash empty and GC are dry-run operations when run is false. An agent should
present or evaluate the report before issuing a separate execution request:
curl --fail-with-body -X POST \
-H "X-Api-Key: $DOCBANK_API_KEY" \
-H 'Content-Type: application/json' \
--data '{"older_than":"30d","run":false}' \
"$DOCBANK_URL/api/v1/trash/empty"
curl --fail-with-body -X POST \
-H "X-Api-Key: $DOCBANK_API_KEY" \
-H 'Content-Type: application/json' \
--data '{"run":false}' \
"$DOCBANK_URL/api/v1/gc"
Only send run: true when policy explicitly authorizes permanent removal.
Treat the execution as a new decision: the vault may have changed since the
preview. Packed bytes reported as pending are logically dead but not yet
physically reclaimed.
POST /api/v1/verify validates logical metadata and audit history before
re-hashing every stored blob. It is read-only but can be expensive. Maintenance
requests serialize against mutations and may run without the ordinary request
timeout. A new mutation submitted after maintenance is running or queued gets
503 maintenance_busy; retry it after the operator-visible maintenance ends.
This is a transient refusal, not evidence that the requested mutation committed.
Treat either metadata_problems or blob problems as a failed verification.
Use POST /api/v1/nodes/{id}/verify when the decision concerns one inspected
file. Unlike the vault-wide operation it requires If-Match, stays bounded to
one blob, and returns the recorded and freshly computed identities directly.
POST /api/v1/storage/pack is explicit but non-destructive: it changes the
physical representation without changing document identity or blob read
authority. Use GET /api/v1/storage before and after when an operator needs an
auditable result. A positive max_bytes bounds raw-byte work softly—the blob
that crosses the budget is committed. budget_exhausted: true describes that
crossing, not whether eligible loose blobs remain; inspect storage status before
deciding to issue another request.
POST /api/v1/storage/repack physically retires empty packs and rewrites
eligible sparse packs without changing logical content authority. Its selection
thresholds are policy, not a preview guarantee: inspect storage status before
and after. bytes_repacked counts live raw bytes rewritten and must not be
reported as reclaimed disk space.
Branch on problem codes¶
Non-2xx responses use RFC 7807 problem JSON:
{
"title": "Conflict",
"status": 409,
"detail": "node \"report.pdf\" already exists",
"code": "exists"
}
Branch on code, never detail. Useful policy groups:
- Re-read and reconsider:
stale_revision. - Correct the request:
validation,precondition_required,invalid_name,invalid_tag,not_dir,not_file,is_root. - Reconcile desired state:
exists,cycle,not_trashed,not_found. - Stop and surface credentials or topology:
unauthorized,loopback_only. - Retry after the active maintenance operation ends:
maintenance_busy. - Release external file locks, then run
storage packreconciliation:pack_retirement_deferred. The preceding repack catalog change already committed; never restore the retired mapping or assume rollback. - Stop automation and preserve evidence:
internal.
The complete mapping lives in HTTP API and the OpenAPI document.
A safe filing loop¶
A robust inbox-filing agent follows this sequence:
- Resolve
/inboxand page through its children. - Read metadata or content for candidate files by ID.
- Decide a destination; create missing directories deliberately.
- Re-read the candidate if the decision took long enough for concurrent work to be plausible.
- Move by ID with the revision the decision was based on.
- On
412, re-read and reconsider rather than replaying. - Record the returned ID, path, and revision as the outcome.
Keep planning and mutation separate in logs. Never log the API key, shutdown token, or document content by default. Use request IDs from your own workflow for correlation; docbank's stable node ID is the durable object identity.