Reference
Nova HTTP API
The public-facing HTTP API surface of a Nova deployment.
- Spec version
- 0.2.0-beta.1+ad629d7
- OpenAPI
- 3.1.0
- Operations
- 66 across 9 groups
- Base URL
- https://nova.thebugreport.dev
- Generated
- 2026-09-11 10:30 UTC
Regenerated from docs/specs/openapi.yaml on Nova's main
branch, so it follows the source rather than being kept in step by
hand. Where it and the specification disagree, the specification
wins. All documentation →
content-blob
Content Blob
Generic blob content routes (nova-storage).
GET/blob/{cid}
Retrieve the bytes of a blob by CID.
Returns the bytes verbatim with the stored Content-Type. For
encrypted collections the bytes are plaintext (the gateway
decrypts on the way out). For collections the operator marked
public_archival: true, the bytes are stored unencrypted and
the gateway streams them directly.
Cache-Control varies by collection visibility and blob state (v2 stratified policy):
| collection | state | request | Cache-Control |
|---|---|---|---|
| public | active | anonymous | public, max-age=31536000, immutable |
| public-archival | active | anonymous | public, max-age=31536000, immutable |
| unlisted | active | anonymous (CID is the capability) | private, max-age=300, must-revalidate |
| private | active | signed URL / bearer | private, max-age=300, must-revalidate |
| any | soft_deleted/quarantined/tombstoned | any | no-store |
Range requests are supported only on public-archival
(plaintext) blobs. A Range request against an encrypted
blob returns 416 Range Not Satisfiable. Streaming AEAD
is a Phase 2 deliverable; until then, single-shot decryption
cannot serve byte ranges.
Parameters
| Name | In | Type | Notes |
|---|---|---|---|
| cid | path · required | Cid | |
| Range | header | string | HTTP/1.1 byte range, e.g., bytes=0-1023. |
Responses
| Code | Meaning | Type | Shape |
|---|---|---|---|
| 200 | Blob bytes. | application/octet-stream | string (binary) |
| 206 | Partial blob bytes (Range request). | application/octet-stream | string (binary) |
| 401 | The blob belongs to a private collection; provide a valid signed URL or a bearer token for an authorized user. | application/json | Error |
| 404 | No object with that identifier is known to this coordinator. For content routes this means the CID has never been ingested here — content addressing makes it a statement about this federation, not about the bytes, so the same CID may well resolve on another one. Distinguish from | application/json | Error |
| 410 | The CID was deleted; the per-blob encryption key has been shredded and the gateway can no longer decrypt the bytes (which may still exist as ciphertext on volunteer disks). | application/json | Error |
| 416 | The A syntactically malformed | application/json | Error |
| 451 | The CID is under moderation hold. Reads are suspended pending operator review. Quarantine is reversible, and deliberately so: the bytes and the
per-blob key both still exist, which is what makes a counter-notice
meaningful. If the hold is lifted the same URL resolves again,
unchanged, because the address is a hash of content that never moved.
If it is not lifted, the blob is tombstoned when the window closes
and this becomes a Signed URLs minted for the CID are revoked at the moment of quarantine, so an embedded copy stops rendering without waiting for its own expiry. | application/json | Error |
HEAD/blob/{cid}
Probe a blob's existence and metadata via headers.
Parameters
| Name | In | Type | Notes |
|---|---|---|---|
| cid | path · required | Cid |
Responses
| Code | Meaning | Type | Shape |
|---|---|---|---|
| 200 | Blob exists; metadata returned in headers. | ||
| 404 | No object with that identifier is known to this coordinator. For content routes this means the CID has never been ingested here — content addressing makes it a statement about this federation, not about the bytes, so the same CID may well resolve on another one. Distinguish from | application/json | Error |
| 410 | The CID was deleted; the per-blob encryption key has been shredded and the gateway can no longer decrypt the bytes (which may still exist as ciphertext on volunteer disks). | application/json | Error |
GET/blob/{cid}.json
Public JSON metadata for a blob.
Parameters
| Name | In | Type | Notes |
|---|---|---|---|
| cid | path · required | Cid |
Responses
| Code | Meaning | Type | Shape |
|---|---|---|---|
| 200 | Blob metadata. | application/json | Blob |
| 404 | No object with that identifier is known to this coordinator. For content routes this means the CID has never been ingested here — content addressing makes it a statement about this federation, not about the bytes, so the same CID may well resolve on another one. Distinguish from | application/json | Error |
| 410 | The CID was deleted; the per-blob encryption key has been shredded and the gateway can no longer decrypt the bytes (which may still exist as ciphertext on volunteer disks). | application/json | Error |
content-image
Content Image
Image content with on-the-fly transforms (nova-image).
GET/i/{cid}
Retrieve an image at its original format.
Equivalent to /blob/{cid} but rejects with 415 if the
underlying blob is not an image. Same caching headers.
Parameters
| Name | In | Type | Notes |
|---|---|---|---|
| cid | path · required | Cid |
Responses
| Code | Meaning | Type | Shape |
|---|---|---|---|
| 200 | Image bytes. | image/jpeg, image/png, image/webp, image/avif | string (binary) |
| 401 | The blob belongs to a private collection; provide a valid signed URL or a bearer token for an authorized user. | application/json | Error |
| 404 | No object with that identifier is known to this coordinator. For content routes this means the CID has never been ingested here — content addressing makes it a statement about this federation, not about the bytes, so the same CID may well resolve on another one. Distinguish from | application/json | Error |
| 410 | The CID was deleted; the per-blob encryption key has been shredded and the gateway can no longer decrypt the bytes (which may still exist as ciphertext on volunteer disks). | application/json | Error |
| 415 | The CID resolves, but the stored object is not an image and the image transform pipeline cannot act on it. Fetch it through | application/json | Error |
| 451 | The CID is under moderation hold. Reads are suspended pending operator review. Quarantine is reversible, and deliberately so: the bytes and the
per-blob key both still exist, which is what makes a counter-notice
meaningful. If the hold is lifted the same URL resolves again,
unchanged, because the address is a hash of content that never moved.
If it is not lifted, the blob is tombstoned when the window closes
and this becomes a Signed URLs minted for the CID are revoked at the moment of quarantine, so an embedded copy stops rendering without waiting for its own expiry. | application/json | Error |
HEAD/i/{cid}
Probe an image's existence and metadata via headers.
Parameters
| Name | In | Type | Notes |
|---|---|---|---|
| cid | path · required | Cid |
Responses
| Code | Meaning | Type | Shape |
|---|---|---|---|
| 200 | Image exists. | ||
| 404 | No object with that identifier is known to this coordinator. For content routes this means the CID has never been ingested here — content addressing makes it a statement about this federation, not about the bytes, so the same CID may well resolve on another one. Distinguish from | application/json | Error |
GET/i/{cid}.json
Public JSON metadata for an image (incl. perceptual hash).
Parameters
| Name | In | Type | Notes |
|---|---|---|---|
| cid | path · required | Cid |
Responses
| Code | Meaning | Type | Shape |
|---|---|---|---|
| 200 | Image metadata. | application/json | Image |
| 404 | No object with that identifier is known to this coordinator. For content routes this means the CID has never been ingested here — content addressing makes it a statement about this federation, not about the bytes, so the same CID may well resolve on another one. Distinguish from | application/json | Error |
| 415 | The CID resolves, but the stored object is not an image, so there is no image metadata to return. Use | application/json | Error |
GET/i/{cid}.{ext}
Retrieve the image, transcoded if necessary.
Parameters
| Name | In | Type | Notes |
|---|---|---|---|
| cid | path · required | Cid | |
| ext | path · required | string | Output format file extension. The gateway re-encodes if necessary. avif and jxl are operator-gated (off by default). |
Responses
| Code | Meaning | Type | Shape |
|---|---|---|---|
| 200 | Image bytes. | image/jpeg, image/png, image/webp, image/avif | string (binary) |
| 404 | No object with that identifier is known to this coordinator. For content routes this means the CID has never been ingested here — content addressing makes it a statement about this federation, not about the bytes, so the same CID may well resolve on another one. Distinguish from | application/json | Error |
| 406 | The extension names a format this operator has not enabled. The enabled set is deployment configuration rather than a property of the stored image; | application/json | Error |
| 410 | The CID was deleted; the per-blob encryption key has been shredded and the gateway can no longer decrypt the bytes (which may still exist as ciphertext on volunteer disks). | application/json | Error |
GET/i/{cid}/p/{preset}.{ext}
Retrieve the image rendered through a named operator preset.
Parameters
| Name | In | Type | Notes |
|---|---|---|---|
| cid | path · required | Cid | |
| preset | path · required | string | Operator-defined preset name (e.g., thumb, og, hero). |
| ext | path · required | string | Output format file extension. avif and jxl are operator-gated (off by default). |
Responses
| Code | Meaning | Type | Shape |
|---|---|---|---|
| 200 | Image bytes. | image/jpeg, image/png, image/webp, image/avif | string (binary) |
| 404 | Either no object with that CID is known here, or the named preset is not configured on this deployment. The two are deliberately not distinguished, so that probing preset names cannot be used to enumerate stored CIDs. | application/json | Error |
| 406 | The extension names a format this operator has not enabled. The enabled set is deployment configuration rather than a property of the stored image; | application/json | Error |
GET/i/{cid}/w{w}.{ext}
Retrieve the image resized to a target width, aspect preserved.
Parameters
| Name | In | Type | Notes |
|---|---|---|---|
| cid | path · required | Cid | |
| w | path · required | integer | Target width in pixels (height computed to preserve aspect). |
| ext | path · required | string | Output format file extension. avif and jxl are operator-gated (off by default). |
Responses
| Code | Meaning | Type | Shape |
|---|---|---|---|
| 200 | Image bytes. | image/jpeg, image/png, image/webp, image/avif | string (binary) |
| 400 | The requested dimensions are not on this operator's whitelist. Arbitrary sizes are refused deliberately: each distinct size is a distinct derivative to generate, store and replicate, so the whitelist is what stops a crawler minting unbounded work. Use a whitelisted size or a named preset. | application/json | Error |
| 404 | No object with that identifier is known to this coordinator. For content routes this means the CID has never been ingested here — content addressing makes it a statement about this federation, not about the bytes, so the same CID may well resolve on another one. Distinguish from | application/json | Error |
| 406 | The extension names a format this operator has not enabled. The enabled set is deployment configuration rather than a property of the stored image; | application/json | Error |
GET/i/{cid}/{w}x{h}.{ext}
Retrieve the image resized to fit a w x h box (cover crop).
Parameters
| Name | In | Type | Notes |
|---|---|---|---|
| cid | path · required | Cid | |
| w | path · required | integer | Target width in pixels (1..8192). |
| h | path · required | integer | Target height in pixels (1..8192). |
| ext | path · required | string | Output format file extension. avif and jxl are operator-gated (off by default). |
Responses
| Code | Meaning | Type | Shape |
|---|---|---|---|
| 200 | Image bytes. | image/jpeg, image/png, image/webp, image/avif | string (binary) |
| 400 | The requested dimensions are not on this operator's whitelist. Arbitrary sizes are refused deliberately: each distinct size is a distinct derivative to generate, store and replicate, so the whitelist is what stops a crawler minting unbounded work. Use a whitelisted size or a named preset. | application/json | Error |
| 404 | No object with that identifier is known to this coordinator. For content routes this means the CID has never been ingested here — content addressing makes it a statement about this federation, not about the bytes, so the same CID may well resolve on another one. Distinguish from | application/json | Error |
| 406 | The extension names a format this operator has not enabled. The enabled set is deployment configuration rather than a property of the stored image; | application/json | Error |
upload
Upload
Resumable (tus.io) and multipart upload endpoints. The M12 browser widget
(web/widget/) is the reference consumer of this surface; after the final
tus PATCH, it calls POST .../finalize. When NOVA_WIDGET_DIST_DIR is set
the coordinator also serves the widget bundle as static assets at /widget/*
(a non-API surface, not described here).
POST/api/v1/blobs
Upload a generic blob via multipart/form-data.
Requires authentication.
Request body (required)
multipart/form-data
file: <binary contents of report.pdf>
product: document
collection_id: 31818242-97c3-4cdf-8025-44ffcb3d0242
alt_text: Q3 availability report
caption: Generated from the coordinator's own metrics.Responses
| Code | Meaning | Type | Shape |
|---|---|---|---|
| 201 | Blob created. | application/json | UploadResult |
| 400 | The request could not be parsed or failed validation before any work was
attempted: a malformed CID, a body that is not valid JSON, a missing
required field, or a parameter outside its permitted range. The | application/json | Error |
| 401 | No usable credential was presented. Either the Note the difference from | application/json | Error |
| 413 | The declared or actual body length exceeds this operator's | application/json | Error |
Example response · 201
{
"cid": "bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi",
"byte_size": 204815,
"mime_type": "image/jpeg",
"product": "image",
"urls": {
"original": "https://nova.thebugreport.dev/i/bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi",
"json": "https://nova.thebugreport.dev/i/bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi.json",
"presets": {
"thumb": "https://nova.thebugreport.dev/i/bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi/p/thumb.webp",
"medium": "https://nova.thebugreport.dev/i/bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi/p/medium.webp"
}
}
}POST/api/v1/images
Upload an image with image-specific validation.
Identical to POST /api/v1/blobs with product=image, but
rejects non-image MIME types early and checks the CID against
the operator-curated CID blocklist so the caller learns
immediately if the upload was rejected on moderation grounds.
The blocklist is a direct indexed lookup; no perceptual scan is
performed at upload time (Phase-3 deferral).
Requires authentication.
Request body (required)
multipart/form-data
file: <binary contents of photo.jpg>
collection_id: 31818242-97c3-4cdf-8025-44ffcb3d0242
alt_text: A patch panel with sixteen cables terminated
caption: The rack as it stood in July.Responses
| Code | Meaning | Type | Shape |
|---|---|---|---|
| 201 | Image created. | application/json | UploadResult |
| 400 | The request could not be parsed or failed validation before any work was
attempted: a malformed CID, a body that is not valid JSON, a missing
required field, or a parameter outside its permitted range. The | application/json | Error |
| 401 | No usable credential was presented. Either the Note the difference from | application/json | Error |
| 415 | The uploaded file's detected media type is not one this coordinator accepts as an image. Detection is by content sniffing rather than by the supplied filename or | application/json | Error |
| 451 | The content hashes to a CID on this operator's blocklist and was refused at ingest. Because the address is derived from the bytes, an identical file will always be refused; nothing was stored. | application/json | Error |
Example response · 201
{
"cid": "bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi",
"byte_size": 204815,
"mime_type": "image/jpeg",
"product": "image",
"urls": {
"original": "https://nova.thebugreport.dev/i/bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi",
"json": "https://nova.thebugreport.dev/i/bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi.json",
"presets": {
"thumb": "https://nova.thebugreport.dev/i/bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi/p/thumb.webp",
"medium": "https://nova.thebugreport.dev/i/bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi/p/medium.webp"
}
}
}POST/api/v1/uploads
Create a resumable upload (tus).
Implements the tus.io resumable upload
protocol v1.0.0. The response Location header points at the
upload-specific endpoint that subsequent PATCH, HEAD, and
DELETE requests target.
After the final PATCH completes, call POST .../finalize to
commit the upload, compute the CID, and write the metadata
row.
Requires authentication.
Parameters
| Name | In | Type | Notes |
|---|---|---|---|
| Tus-Resumable | header · required | string | |
| Upload-Length | header · required | integer (int64) | |
| Upload-Metadata | header | string | tus metadata field. Nova recognizes: |
Request body
application/offset+octet-stream
# tus creation-with-upload: the body is the first chunk of raw bytes.
# Length and metadata travel in headers, not in the body:
# Upload-Length: 20481
# Upload-Metadata: filename cGhvdG8uanBn,product aW1hZ2U=
# Content-Type: application/offset+octet-stream
<20481 bytes of image/jpeg>
Responses
| Code | Meaning | Type | Shape |
|---|---|---|---|
| 201 | Upload created. | ||
| 400 | The request could not be parsed or failed validation before any work was
attempted: a malformed CID, a body that is not valid JSON, a missing
required field, or a parameter outside its permitted range. The | application/json | Error |
| 401 | No usable credential was presented. Either the Note the difference from | application/json | Error |
| 413 | The declared or actual body length exceeds this operator's | application/json | Error |
PATCH/api/v1/uploads/{id}
Append a chunk to an in-progress upload (tus PATCH).
Requires authentication.
Parameters
| Name | In | Type | Notes |
|---|---|---|---|
| id | path · required | string | |
| Upload-Offset | header · required | integer (int64) | |
| Tus-Resumable | header · required | string | |
| Content-Type | header · required | string |
Request body (required)
application/offset+octet-stream
# Each PATCH carries the next contiguous run of bytes. Upload-Offset must
# equal the server's current offset or the request is rejected with 409:
# Upload-Offset: 16384
# Content-Type: application/offset+octet-stream
<4097 bytes continuing the object>
Responses
| Code | Meaning | Type | Shape |
|---|---|---|---|
| 204 | Chunk accepted. | ||
| 404 | No object with that identifier is known to this coordinator. For content routes this means the CID has never been ingested here — content addressing makes it a statement about this federation, not about the bytes, so the same CID may well resolve on another one. Distinguish from | application/json | Error |
| 409 |
| application/json | Error |
DELETE/api/v1/uploads/{id}
Abandon an in-progress upload.
Requires authentication.
Parameters
| Name | In | Type | Notes |
|---|---|---|---|
| id | path · required | string |
Responses
| Code | Meaning | Type | Shape |
|---|---|---|---|
| 204 | Upload deleted. | ||
| 404 | No object with that identifier is known to this coordinator. For content routes this means the CID has never been ingested here — content addressing makes it a statement about this federation, not about the bytes, so the same CID may well resolve on another one. Distinguish from | application/json | Error |
HEAD/api/v1/uploads/{id}
Probe the current offset of an in-progress upload.
Requires authentication.
Parameters
| Name | In | Type | Notes |
|---|---|---|---|
| id | path · required | string |
Responses
| Code | Meaning | Type | Shape |
|---|---|---|---|
| 200 | Offset and metadata returned in headers. | ||
| 404 | No object with that identifier is known to this coordinator. For content routes this means the CID has never been ingested here — content addressing makes it a statement about this federation, not about the bytes, so the same CID may well resolve on another one. Distinguish from | application/json | Error |
POST/api/v1/uploads/{id}/finalize
Finalize an upload, compute its CID, and persist metadata.
Requires authentication.
Parameters
| Name | In | Type | Notes |
|---|---|---|---|
| id | path · required | string |
Responses
| Code | Meaning | Type | Shape |
|---|---|---|---|
| 200 | Upload finalized. | application/json | UploadResult |
| 404 | No object with that identifier is known to this coordinator. For content routes this means the CID has never been ingested here — content addressing makes it a statement about this federation, not about the bytes, so the same CID may well resolve on another one. Distinguish from | application/json | Error |
| 409 | Finalisation was requested before every byte arrived: the session's offset is short of the declared | application/json | Error |
metadata
Metadata
Read and update blob/image metadata.
GET/api/v1/blobs/{cid}
Authenticated metadata read for a blob (any state; owner or operator/moderator).
Returns the blob row in ANY state (unlike the public read path, which rejects non-active states). Authorization: the blob's owner, or an operator/moderator. Mounted in M11 (M6-deferred).
Requires authentication.
Parameters
| Name | In | Type | Notes |
|---|---|---|---|
| cid | path · required | Cid |
Responses
| Code | Meaning | Type | Shape |
|---|---|---|---|
| 200 | Blob metadata. | application/json | Blob |
| 401 | No usable credential was presented. Either the Note the difference from | application/json | Error |
| 403 | The caller was identified but is not permitted to perform this action.
Three distinct situations produce it, distinguishable by
Re-authenticating does not help. The credential is valid; the action is not permitted for it. | application/json | Error |
| 404 | No object with that identifier is known to this coordinator. For content routes this means the CID has never been ingested here — content addressing makes it a statement about this federation, not about the bytes, so the same CID may well resolve on another one. Distinguish from | application/json | Error |
Example response · 200
{
"cid": "bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi",
"mime_type": "image/jpeg",
"byte_size": 204815,
"uploaded_at": "2026-07-29T11:04:22Z",
"state": "active",
"product": "image",
"owner_id": "9a1d4e6f-2b77-4c1a-8f0e-5d3b2a7c9e01",
"urls": {
"bytes": "https://nova.thebugreport.dev/blob/bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi",
"json": "https://nova.thebugreport.dev/blob/bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi.json"
}
}PATCH/api/v1/blobs/{cid}
Update mutable owner-controlled fields on a blob.
Requires authentication.
Parameters
| Name | In | Type | Notes |
|---|---|---|---|
| cid | path · required | Cid |
Request body (required)
application/json
{
"alt_text": "Scanned page 14 of the 1987 service manual",
"caption": "Section 3.2, power supply rework."
}Responses
| Code | Meaning | Type | Shape |
|---|---|---|---|
| 200 | Blob metadata updated. | application/json | Blob |
| 403 | The caller was identified but is not permitted to perform this action.
Three distinct situations produce it, distinguishable by
Re-authenticating does not help. The credential is valid; the action is not permitted for it. | application/json | Error |
| 404 | No object with that identifier is known to this coordinator. For content routes this means the CID has never been ingested here — content addressing makes it a statement about this federation, not about the bytes, so the same CID may well resolve on another one. Distinguish from | application/json | Error |
Example response · 200
{
"cid": "bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi",
"mime_type": "image/jpeg",
"byte_size": 204815,
"uploaded_at": "2026-07-29T11:04:22Z",
"state": "active",
"product": "image",
"owner_id": "9a1d4e6f-2b77-4c1a-8f0e-5d3b2a7c9e01",
"urls": {
"bytes": "https://nova.thebugreport.dev/blob/bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi",
"json": "https://nova.thebugreport.dev/blob/bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi.json"
}
}DELETE/api/v1/blobs/{cid}
Soft-delete a blob (initiates crypto-shred and unpin broadcast).
The blob row enters state = 'soft_deleted' for the operator's
configured grace window, then transitions to tombstoned. On
tombstone the per-blob encryption key is zeroed and an unpin
broadcast is sent across the federation. Bytes on volunteer
disks may persist as unrecoverable ciphertext.
Requires authentication.
Parameters
| Name | In | Type | Notes |
|---|---|---|---|
| cid | path · required | Cid |
Responses
| Code | Meaning | Type | Shape |
|---|---|---|---|
| 204 | Soft-delete accepted (owner or operator). | ||
| 403 | The caller was identified but is not permitted to perform this action.
Three distinct situations produce it, distinguishable by
Re-authenticating does not help. The credential is valid; the action is not permitted for it. | application/json | Error |
| 404 | No object with that identifier is known to this coordinator. For content routes this means the CID has never been ingested here — content addressing makes it a statement about this federation, not about the bytes, so the same CID may well resolve on another one. Distinguish from | application/json | Error |
| 409 | The blob is not active (already soft-deleted, quarantined, or tombstoned). | application/json | Error |
GET/api/v1/images/{cid}
Authenticated metadata read for an image.
Requires authentication.
Parameters
| Name | In | Type | Notes |
|---|---|---|---|
| cid | path · required | Cid |
Responses
| Code | Meaning | Type | Shape |
|---|---|---|---|
| 200 | Image metadata. | application/json | Image |
| 404 | No object with that identifier is known to this coordinator. For content routes this means the CID has never been ingested here — content addressing makes it a statement about this federation, not about the bytes, so the same CID may well resolve on another one. Distinguish from | application/json | Error |
PATCH/api/v1/images/{cid}
Update mutable owner-controlled fields on an image.
Requires authentication.
Parameters
| Name | In | Type | Notes |
|---|---|---|---|
| cid | path · required | Cid |
Request body (required)
application/json
{
"alt_text": "A patch panel with sixteen cables terminated",
"caption": "The rack as it stood in July."
}Responses
| Code | Meaning | Type | Shape |
|---|---|---|---|
| 200 | Image metadata updated. | application/json | Image |
| 404 | No object with that identifier is known to this coordinator. For content routes this means the CID has never been ingested here — content addressing makes it a statement about this federation, not about the bytes, so the same CID may well resolve on another one. Distinguish from | application/json | Error |
DELETE/api/v1/images/{cid}
Soft-delete an image (alias for DELETE /api/v1/blobs/{cid}).
Requires authentication.
Parameters
| Name | In | Type | Notes |
|---|---|---|---|
| cid | path · required | Cid |
Responses
| Code | Meaning | Type | Shape |
|---|---|---|---|
| 204 | Deletion accepted. | ||
| 404 | No object with that identifier is known to this coordinator. For content routes this means the CID has never been ingested here — content addressing makes it a statement about this federation, not about the bytes, so the same CID may well resolve on another one. Distinguish from | application/json | Error |
GET/api/v1/users/me
Return the authenticated user's profile.
Requires authentication.
Responses
| Code | Meaning | Type | Shape |
|---|---|---|---|
| 200 | Current user. | application/json | User |
| 401 | No usable credential was presented. Either the Note the difference from | application/json | Error |
collections
Collections
Curated collections of blobs and images.
GET/api/v1/collections
List collections owned by the caller (or public ones).
Requires authentication.
Parameters
| Name | In | Type | Notes |
|---|---|---|---|
| page | query | integer | |
| per_page | query | integer | |
| visibility | query | string |
Responses
| Code | Meaning | Type | Shape |
|---|---|---|---|
| 200 | Collection list. | application/json | PaginatedCollections |
POST/api/v1/collections
Create a collection.
Requires authentication.
Request body (required)
application/json
{
"name": "Forum attachments",
"slug": "forum-attachments",
"visibility": "public"
}Responses
| Code | Meaning | Type | Shape |
|---|---|---|---|
| 201 | Collection created. | application/json | Collection |
Example response · 201
{
"id": "31818242-97c3-4cdf-8025-44ffcb3d0242",
"owner_id": "9a1d4e6f-2b77-4c1a-8f0e-5d3b2a7c9e01",
"name": "Forum attachments",
"slug": "forum-attachments",
"visibility": "public",
"created_at": "2026-07-28T09:12:00Z",
"item_count": 1284
}GET/api/v1/collections/{id}
Read a collection.
Parameters
| Name | In | Type | Notes |
|---|---|---|---|
| id | path · required | string (uuid) |
Responses
| Code | Meaning | Type | Shape |
|---|---|---|---|
| 200 | Collection. | application/json | Collection |
| 404 | No object with that identifier is known to this coordinator. For content routes this means the CID has never been ingested here — content addressing makes it a statement about this federation, not about the bytes, so the same CID may well resolve on another one. Distinguish from | application/json | Error |
Example response · 200
{
"id": "31818242-97c3-4cdf-8025-44ffcb3d0242",
"owner_id": "9a1d4e6f-2b77-4c1a-8f0e-5d3b2a7c9e01",
"name": "Forum attachments",
"slug": "forum-attachments",
"visibility": "public",
"created_at": "2026-07-28T09:12:00Z",
"item_count": 1284
}PATCH/api/v1/collections/{id}
Update collection metadata.
Requires authentication.
Parameters
| Name | In | Type | Notes |
|---|---|---|---|
| id | path · required | string (uuid) |
Request body (required)
application/json
{
"name": "Forum attachments (archived)",
"visibility": "unlisted"
}Responses
| Code | Meaning | Type | Shape |
|---|---|---|---|
| 200 | Updated collection. | application/json | Collection |
| 403 | The caller was identified but is not permitted to perform this action.
Three distinct situations produce it, distinguishable by
Re-authenticating does not help. The credential is valid; the action is not permitted for it. | application/json | Error |
| 404 | No object with that identifier is known to this coordinator. For content routes this means the CID has never been ingested here — content addressing makes it a statement about this federation, not about the bytes, so the same CID may well resolve on another one. Distinguish from | application/json | Error |
Example response · 200
{
"id": "31818242-97c3-4cdf-8025-44ffcb3d0242",
"owner_id": "9a1d4e6f-2b77-4c1a-8f0e-5d3b2a7c9e01",
"name": "Forum attachments",
"slug": "forum-attachments",
"visibility": "public",
"created_at": "2026-07-28T09:12:00Z",
"item_count": 1284
}DELETE/api/v1/collections/{id}
Delete a collection (does not delete its items).
Requires authentication.
Parameters
| Name | In | Type | Notes |
|---|---|---|---|
| id | path · required | string (uuid) |
Responses
| Code | Meaning | Type | Shape |
|---|---|---|---|
| 204 | Collection deleted. | ||
| 403 | The caller was identified but is not permitted to perform this action.
Three distinct situations produce it, distinguishable by
Re-authenticating does not help. The credential is valid; the action is not permitted for it. | application/json | Error |
| 404 | No object with that identifier is known to this coordinator. For content routes this means the CID has never been ingested here — content addressing makes it a statement about this federation, not about the bytes, so the same CID may well resolve on another one. Distinguish from | application/json | Error |
POST/api/v1/collections/{id}/items
Add a blob to a collection.
Requires authentication.
Parameters
| Name | In | Type | Notes |
|---|---|---|---|
| id | path · required | string (uuid) |
Request body (required)
application/json
{
"cid": "bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi",
"position": 3
}Responses
| Code | Meaning | Type | Shape |
|---|---|---|---|
| 201 | Item added. | application/json | CollectionItem |
| 404 | No object with that identifier is known to this coordinator. For content routes this means the CID has never been ingested here — content addressing makes it a statement about this federation, not about the bytes, so the same CID may well resolve on another one. Distinguish from | application/json | Error |
Example response · 201
{
"collection_id": "31818242-97c3-4cdf-8025-44ffcb3d0242",
"cid": "bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi",
"position": 3,
"added_at": "2026-07-30T12:00:00Z"
}DELETE/api/v1/collections/{id}/items/{cid}
Remove a blob from a collection.
Requires authentication.
Parameters
| Name | In | Type | Notes |
|---|---|---|---|
| id | path · required | string (uuid) | |
| cid | path · required | Cid |
Responses
| Code | Meaning | Type | Shape |
|---|---|---|---|
| 204 | Item removed. | ||
| 404 | No object with that identifier is known to this coordinator. For content routes this means the CID has never been ingested here — content addressing makes it a statement about this federation, not about the bytes, so the same CID may well resolve on another one. Distinguish from | application/json | Error |
search
Search
Search APIs (perceptual near-duplicate, etc.).
GET/api/v1/search/perceptual
Find images perceptually similar to a given CID.
Uses the in-memory BK-tree index over PDQ hashes. By default operator-only (deduplication tooling); operators may flip a config to expose to authenticated users.
Requires authentication.
Parameters
| Name | In | Type | Notes |
|---|---|---|---|
| cid | query · required | Cid | |
| max_distance | query | integer | Maximum Hamming distance over the 256-bit hash. |
| limit | query | integer |
Responses
| Code | Meaning | Type | Shape |
|---|---|---|---|
| 200 | Near-duplicate matches. | application/json | object |
admin
Admin
Operator and moderator endpoints (require role). Separately, the M13
first-run setup wizard exposes an ephemeral /setup/* surface
(internal/api/handlers/setup.go): it is mounted only while the
.bootstrap-complete sentinel is absent, binds loopback-only, and is
removed permanently once the wizard commits — /setup/* returns 404 in
normal operation. It is a bootstrap-only, non-API surface and is not
described here.
GET/api/v1/admin/audit-log
List operator action audit-log entries (newest-first).
Paginated listing of audit_log rows. Optional filters:
action (e.g. dmca.quarantined) and target_type (e.g. cid).
The audit log is append-only and monthly-partitioned; rows are never
deleted (recommended retention ≥ 7 years).
Requires authentication.
Parameters
| Name | In | Type | Notes |
|---|---|---|---|
| page | query | integer | |
| per_page | query | integer | |
| action | query | string | |
| target_type | query | string |
Responses
| Code | Meaning | Type | Shape |
|---|---|---|---|
| 200 | Audit log entries. | application/json | PaginatedAuditLogEntries |
| 400 | The request could not be parsed or failed validation before any work was
attempted: a malformed CID, a body that is not valid JSON, a missing
required field, or a parameter outside its permitted range. The | application/json | Error |
| 401 | No usable credential was presented. Either the Note the difference from | application/json | Error |
| 403 | The caller was identified but is not permitted to perform this action.
Three distinct situations produce it, distinguishable by
Re-authenticating does not help. The credential is valid; the action is not permitted for it. | application/json | Error |
GET/api/v1/admin/audits/integrity
List local integrity audit results.
Phase 1 local-fixity audits. See
docs/specs/INTEGRITY_AUDIT.md.
Requires authentication.
Parameters
| Name | In | Type | Notes |
|---|---|---|---|
| page | query | integer | |
| per_page | query | integer | |
| result | query | string | |
| audit_kind | query | string |
Responses
| Code | Meaning | Type | Shape |
|---|---|---|---|
| 200 | Integrity audit log entries. | application/json | PaginatedIntegrityAudits |
| 400 | The request could not be parsed or failed validation before any work was
attempted: a malformed CID, a body that is not valid JSON, a missing
required field, or a parameter outside its permitted range. The | application/json | Error |
| 401 | No usable credential was presented. Either the Note the difference from | application/json | Error |
| 403 | The caller was identified but is not permitted to perform this action.
Three distinct situations produce it, distinguishable by
Re-authenticating does not help. The credential is valid; the action is not permitted for it. | application/json | Error |
GET/api/v1/admin/audits/possession
List donor possession spot-check results.
Phase 2 donor possession audits. See
docs/specs/POSSESSION_AUDIT.md.
Requires authentication.
Parameters
| Name | In | Type | Notes |
|---|---|---|---|
| page | query | integer | |
| per_page | query | integer | |
| result | query | string | |
| node_id | query | string (uuid) |
Responses
| Code | Meaning | Type | Shape |
|---|---|---|---|
| 200 | Pin audit log entries. | application/json | PaginatedPossessionAudits |
GET/api/v1/admin/blobs
List blobs operator-wide (any state), paginated and filterable.
The M11 admin blob-management listing. Optional filters: state,
product, owner_id. The owner-facing detail view is
GET /api/v1/blobs/{cid}. Requires operator or moderator.
Requires authentication.
Parameters
| Name | In | Type | Notes |
|---|---|---|---|
| page | query | integer | |
| per_page | query | integer | |
| state | query | string | |
| product | query | string | |
| owner_id | query | string (uuid) |
Responses
| Code | Meaning | Type | Shape |
|---|---|---|---|
| 200 | Blob listing. | application/json | PaginatedBlobs |
| 400 | The request could not be parsed or failed validation before any work was
attempted: a malformed CID, a body that is not valid JSON, a missing
required field, or a parameter outside its permitted range. The | application/json | Error |
| 401 | No usable credential was presented. Either the Note the difference from | application/json | Error |
| 403 | The caller was identified but is not permitted to perform this action.
Three distinct situations produce it, distinguishable by
Re-authenticating does not help. The credential is valid; the action is not permitted for it. | application/json | Error |
GET/api/v1/admin/dmca
List DMCA cases.
Requires authentication.
Parameters
| Name | In | Type | Notes |
|---|---|---|---|
| page | query | integer | |
| per_page | query | integer | |
| status | query | string |
Responses
| Code | Meaning | Type | Shape |
|---|---|---|---|
| 200 | DMCA cases. | application/json | PaginatedDmcaCases |
| 401 | No usable credential was presented. Either the Note the difference from | application/json | Error |
| 403 | The caller was identified but is not permitted to perform this action.
Three distinct situations produce it, distinguishable by
Re-authenticating does not help. The credential is valid; the action is not permitted for it. | application/json | Error |
GET/api/v1/admin/dmca/{id}
Get a single DMCA case by id.
Requires authentication.
Parameters
| Name | In | Type | Notes |
|---|---|---|---|
| id | path · required | string (uuid) |
Responses
| Code | Meaning | Type | Shape |
|---|---|---|---|
| 200 | DMCA case detail. | application/json | DmcaCase |
| 401 | No usable credential was presented. Either the Note the difference from | application/json | Error |
| 403 | The caller was identified but is not permitted to perform this action.
Three distinct situations produce it, distinguishable by
Re-authenticating does not help. The credential is valid; the action is not permitted for it. | application/json | Error |
| 404 | No object with that identifier is known to this coordinator. For content routes this means the CID has never been ingested here — content addressing makes it a statement about this federation, not about the bytes, so the same CID may well resolve on another one. Distinguish from | application/json | Error |
GET/api/v1/admin/federation/version-census
Fleet version census (operator-only).
What every donor reports it is running, classified on orthogonal axes against this coordinator's compiled-in release catalog.
Operator-only. The /admin group admits moderators because they run
takedowns; this endpoint exposes the operator's supply-chain
expectations and every donor's self-reported build identity, which is
not moderation.
Axes are independent. A donor can be current, stale and digest-mismatched at the same time; one enum would force a precedence order onto facts that have none, and whichever fact lost would be the one the operator needed.
Reported values are untrusted claims. A process cannot discover its
own OCI manifest digest without a Docker socket, which Nova gives
neither coordinator nor doctor, so image_digest and
bundle_lock_digest are configured declarations. They never gate
protocol access, placement, durability counting, trust graduation,
drain or eviction. A mismatch against what the operator authorized via
novactl node rollout authorize is a supply-chain warning.
unknown is not unsupported. A donor that reports nothing is
judged by negotiated protocol and capabilities, which is the actual
interop contract. unavailable is different again: the applied
schema cannot answer that axis (schema_aware: false, which is what a
preflight run against schema 18 sees).
Rendered by the admin console in P2-M7.6.
Requires authentication.
Responses
| Code | Meaning | Type | Shape |
|---|---|---|---|
| 200 | The census. | application/json | object |
| 401 | No usable credential was presented. Either the Note the difference from | application/json | Error |
| 403 | The caller was identified but is not permitted to perform this action.
Three distinct situations produce it, distinguishable by
Re-authenticating does not help. The credential is valid; the action is not permitted for it. | application/json | Error |
GET/api/v1/admin/jobs
List background-queue jobs (read-only), paginated and filterable.
The M11 jobs view — stuck / failed / recent work with last_error.
Optional filters: state, kind. Read-only; requeue is a deliberate
M11 non-goal. Requires operator or moderator.
Requires authentication.
Parameters
| Name | In | Type | Notes |
|---|---|---|---|
| page | query | integer | |
| per_page | query | integer | |
| state | query | string | |
| kind | query | string |
Responses
| Code | Meaning | Type | Shape |
|---|---|---|---|
| 200 | Job listing. | application/json | PaginatedJobs |
| 400 | The request could not be parsed or failed validation before any work was
attempted: a malformed CID, a body that is not valid JSON, a missing
required field, or a parameter outside its permitted range. The | application/json | Error |
| 401 | No usable credential was presented. Either the Note the difference from | application/json | Error |
| 403 | The caller was identified but is not permitted to perform this action.
Three distinct situations produce it, distinguishable by
Re-authenticating does not help. The credential is valid; the action is not permitted for it. | application/json | Error |
POST/api/v1/admin/keys/rotate-master
Trigger a master-key rotation (operator-only).
Validates that to_version equals the currently-active master-key label,
marks the from_version as rotating, and starts the background worker
that re-wraps every DEK and non-shredded signing key from the old version
to the new one via atomic, version-guarded UPDATEs. Rotation is online;
reads work against either version during the drain.
Prerequisites. The operator must:
1. Deploy the new key to the secret mount (e.g. NOVA_MASTER_KEY_V2_FILE),
keeping the old key present.
2. Set NOVA_MASTER_KEY_ACTIVE=<to_version> and restart the coordinator.
3. Only then call this endpoint.
to_version MUST equal the active label at the time of the call or the
endpoint returns 400 to_not_active. See
docs/specs/ENCRYPTION_ENVELOPE.md § "Rotation procedure".
Requires authentication.
Request body (required)
application/json
{
"from_version": "v1",
"to_version": "v2"
}Responses
| Code | Meaning | Type | Shape |
|---|---|---|---|
| 202 | Rotation accepted. The | application/json | RotateMasterStarted |
| 400 | Validation failure. | application/json | Error |
| 401 | No usable credential was presented. Either the Note the difference from | application/json | Error |
| 403 | The caller was identified but is not permitted to perform this action.
Three distinct situations produce it, distinguishable by
Re-authenticating does not help. The credential is valid; the action is not permitted for it. | application/json | Error |
| 409 | A master-key rotation is already in progress. Only one may run at a time, because two concurrent rewrap passes over the same key material could leave envelopes wrapped under a version neither pass completes. Poll | application/json | Error |
POST/api/v1/admin/keys/rotate-signing
Rotate the HMAC signing key used for signed URLs.
Requires authentication.
Request body
application/json
{
"grace_seconds": 3600
}Responses
| Code | Meaning | Type | Shape |
|---|---|---|---|
| 201 | Key rotated; previous key remains valid until | application/json | object |
GET/api/v1/admin/keys/rotation-status
Get master-key rotation progress (operator-only).
Returns the active master-key label, the current in-progress rotation
(if any), and a per-version summary. When idle, in_progress is null.
The CLI (novactl keys status) uses this to poll until the rotation
completes or stalls. A stalled rotation means the from key was
removed before the drain finished; /readyz will also degrade.
Requires authentication.
Responses
| Code | Meaning | Type | Shape |
|---|---|---|---|
| 200 | Current rotation status. | application/json | RotationStatus |
| 401 | No usable credential was presented. Either the Note the difference from | application/json | Error |
| 403 | The caller was identified but is not permitted to perform this action.
Three distinct situations produce it, distinguishable by
Re-authenticating does not help. The credential is valid; the action is not permitted for it. | application/json | Error |
GET/api/v1/admin/moderation/blocklist
List operator-curated CID blocklist entries.
The Phase-1 blocklist is CID-based (operator-curated exact-CID deny). Perceptual-hash / visual re-upload prevention is a Phase-3 deferral.
Requires authentication.
Parameters
| Name | In | Type | Notes |
|---|---|---|---|
| page | query | integer | |
| per_page | query | integer |
Responses
| Code | Meaning | Type | Shape |
|---|---|---|---|
| 200 | Blocklist entries. | application/json | PaginatedBlocklistEntries |
| 401 | No usable credential was presented. Either the Note the difference from | application/json | Error |
| 403 | The caller was identified but is not permitted to perform this action.
Three distinct situations produce it, distinguishable by
Re-authenticating does not help. The credential is valid; the action is not permitted for it. | application/json | Error |
POST/api/v1/admin/moderation/blocklist
Add a CID to the operator-curated blocklist.
Adds the exact CID to the deny registry. Enforced at both the read
path (storage.Resolve → 451) and the import/commit path
(storage.Put → 451). The Phase-1 blocklist is CID-based;
perceptual-hash matching is Phase 3.
Requires authentication.
Request body (required)
application/json
{
"cid": "bafybeihq2n7hpx6djvsxaqzfhbmi2ftfqkrgqcxk3xrvmlmemkwr4ldwsm",
"reason": "Known-bad hash; refuse on ingest as well as on read."
}Responses
| Code | Meaning | Type | Shape |
|---|---|---|---|
| 201 | CID added to blocklist. | ||
| 400 | The request could not be parsed or failed validation before any work was
attempted: a malformed CID, a body that is not valid JSON, a missing
required field, or a parameter outside its permitted range. The | application/json | Error |
| 401 | No usable credential was presented. Either the Note the difference from | application/json | Error |
| 403 | The caller was identified but is not permitted to perform this action.
Three distinct situations produce it, distinguishable by
Re-authenticating does not help. The credential is valid; the action is not permitted for it. | application/json | Error |
DELETE/api/v1/admin/moderation/blocklist/{cid}
Remove a CID from the operator-curated blocklist.
Requires authentication.
Parameters
| Name | In | Type | Notes |
|---|---|---|---|
| cid | path · required | Cid |
Responses
| Code | Meaning | Type | Shape |
|---|---|---|---|
| 204 | CID removed from blocklist. | ||
| 401 | No usable credential was presented. Either the Note the difference from | application/json | Error |
| 403 | The caller was identified but is not permitted to perform this action.
Three distinct situations produce it, distinguishable by
Re-authenticating does not help. The credential is valid; the action is not permitted for it. | application/json | Error |
| 404 | No object with that identifier is known to this coordinator. For content routes this means the CID has never been ingested here — content addressing makes it a statement about this federation, not about the bytes, so the same CID may well resolve on another one. Distinguish from | application/json | Error |
POST/api/v1/admin/moderation/clear-legal-hold
Clear the legal-hold flag on a CID so the next sweep tombstones it (operator-only).
Clears data_encryption_keys.legal_hold for the parent and all
derivatives, sets scheduled_tombstone_at = now() on the originating
quarantine decision, and writes an audit row. The ≈1-minute sweep
will tombstone and shred on its next tick.
Operator role required — moderators receive 403.
Requires authentication.
Request body (required)
application/json
{
"cid": "bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi",
"case_ref": "DMCA-2026-0117",
"reason": "Matter closed; hold released so the scheduled shred can proceed."
}Responses
| Code | Meaning | Type | Shape |
|---|---|---|---|
| 200 | Legal hold cleared; tombstone scheduled. | ||
| 400 | The request could not be parsed or failed validation before any work was
attempted: a malformed CID, a body that is not valid JSON, a missing
required field, or a parameter outside its permitted range. The | application/json | Error |
| 401 | No usable credential was presented. Either the Note the difference from | application/json | Error |
| 403 | The caller was identified but is not permitted to perform this action.
Three distinct situations produce it, distinguishable by
Re-authenticating does not help. The credential is valid; the action is not permitted for it. | application/json | Error |
| 404 | No object with that identifier is known to this coordinator. For content routes this means the CID has never been ingested here — content addressing makes it a statement about this federation, not about the bytes, so the same CID may well resolve on another one. Distinguish from | application/json | Error |
POST/api/v1/admin/moderation/counter-notice
Record a counter-notice and clear the scheduled tombstone.
Clears scheduled_tombstone_at so the sweep will not auto-tombstone
during the counter-notice review window. The blob remains
quarantined; the operator later restores or lets a renewed schedule
proceed to tombstone. Appends notes to the associated DMCA case.
Requires operator or moderator role.
Requires authentication.
Request body (required)
application/json
{
"cid": "bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi",
"notes": "Uploader supplied a licence; forwarded to the claimant 2026-07-28."
}Responses
| Code | Meaning | Type | Shape |
|---|---|---|---|
| 200 | Counter-notice recorded; tombstone schedule cleared. | ||
| 400 | The request could not be parsed or failed validation before any work was
attempted: a malformed CID, a body that is not valid JSON, a missing
required field, or a parameter outside its permitted range. The | application/json | Error |
| 401 | No usable credential was presented. Either the Note the difference from | application/json | Error |
| 403 | The caller was identified but is not permitted to perform this action.
Three distinct situations produce it, distinguishable by
Re-authenticating does not help. The credential is valid; the action is not permitted for it. | application/json | Error |
| 404 | No object with that identifier is known to this coordinator. For content routes this means the CID has never been ingested here — content addressing makes it a statement about this federation, not about the bytes, so the same CID may well resolve on another one. Distinguish from | application/json | Error |
POST/api/v1/admin/moderation/quarantine
Quarantine a CID (blocks reads, preserves bytes for counter-notice window).
Sets the blob and its derivatives to quarantined, inserts a
signed-URL revocation, and schedules a tombstone after
tombstone_after (default 14 days). With legal_hold: true the
DEK(s) are pinned (legal_hold = true in data_encryption_keys)
and no scheduled_tombstone_at is set — crypto-shred is refused
at the DB layer until POST /moderation/clear-legal-hold.
Requires operator or moderator role.
Requires authentication.
Request body (required)
application/json
{
"cid": "bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi",
"rule": "dmca",
"case_id": "6c3f0a4e-6b1c-4a2f-9d55-1c0d9b7a2e11",
"reason": "DMCA notice received; holding bytes for the counter-notice window.",
"legal_hold": true
}Responses
| Code | Meaning | Type | Shape |
|---|---|---|---|
| 200 | CID quarantined. | application/json | object |
| 400 | The request could not be parsed or failed validation before any work was
attempted: a malformed CID, a body that is not valid JSON, a missing
required field, or a parameter outside its permitted range. The | application/json | Error |
| 401 | No usable credential was presented. Either the Note the difference from | application/json | Error |
| 403 | The caller was identified but is not permitted to perform this action.
Three distinct situations produce it, distinguishable by
Re-authenticating does not help. The credential is valid; the action is not permitted for it. | application/json | Error |
| 404 | No object with that identifier is known to this coordinator. For content routes this means the CID has never been ingested here — content addressing makes it a statement about this federation, not about the bytes, so the same CID may well resolve on another one. Distinguish from | application/json | Error |
GET/api/v1/admin/moderation/queue
List pending moderation decisions.
Requires authentication.
Parameters
| Name | In | Type | Notes |
|---|---|---|---|
| page | query | integer | |
| per_page | query | integer |
Responses
| Code | Meaning | Type | Shape |
|---|---|---|---|
| 200 | Moderation queue. | application/json | PaginatedModerationDecisions |
| 401 | No usable credential was presented. Either the Note the difference from | application/json | Error |
| 403 | The caller was identified but is not permitted to perform this action.
Three distinct situations produce it, distinguishable by
Re-authenticating does not help. The credential is valid; the action is not permitted for it. | application/json | Error |
POST/api/v1/admin/moderation/restore
Restore a quarantined CID to active (only valid from quarantined state).
Sets the blob and its derivatives back to active and clears
scheduled_tombstone_at. The (cid, cid) signed-URL revocation
is not removed (it is indistinguishable from a manual operator
revocation; re-mint new signed URLs after restore if needed).
Returns 409 if the blob is not in quarantined state.
Requires operator or moderator role.
Requires authentication.
Request body (required)
application/json
{
"cid": "bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi",
"reason": "Counter-notice accepted; quarantine lifted."
}Responses
| Code | Meaning | Type | Shape |
|---|---|---|---|
| 200 | CID restored to active. | ||
| 400 | The request could not be parsed or failed validation before any work was
attempted: a malformed CID, a body that is not valid JSON, a missing
required field, or a parameter outside its permitted range. The | application/json | Error |
| 401 | No usable credential was presented. Either the Note the difference from | application/json | Error |
| 403 | The caller was identified but is not permitted to perform this action.
Three distinct situations produce it, distinguishable by
Re-authenticating does not help. The credential is valid; the action is not permitted for it. | application/json | Error |
| 404 | No object with that identifier is known to this coordinator. For content routes this means the CID has never been ingested here — content addressing makes it a statement about this federation, not about the bytes, so the same CID may well resolve on another one. Distinguish from | application/json | Error |
| 409 | Restore applies only to a blob currently in | application/json | Error |
POST/api/v1/admin/moderation/takedown
Immediate tombstone + crypto-shred of a CID (local Kubo unpin after commit).
Bypasses the counter-notice window: tombstones and shreds the DEK
immediately. Returns 409 legal_hold if the DEK carries a
legal-hold flag (use clear-legal-hold first). The local Kubo
unpin is best-effort after commit; federation unpin broadcast is
Phase 2.
Requires operator or moderator role.
Requires authentication.
Request body (required)
application/json
{
"cid": "bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi",
"case_id": "6c3f0a4e-6b1c-4a2f-9d55-1c0d9b7a2e11",
"reason": "Uncontested DMCA notice; counter-notice window elapsed."
}Responses
| Code | Meaning | Type | Shape |
|---|---|---|---|
| 200 | CID tombstoned and DEK shredded. | ||
| 400 | The request could not be parsed or failed validation before any work was
attempted: a malformed CID, a body that is not valid JSON, a missing
required field, or a parameter outside its permitted range. The | application/json | Error |
| 401 | No usable credential was presented. Either the Note the difference from | application/json | Error |
| 403 | The caller was identified but is not permitted to perform this action.
Three distinct situations produce it, distinguishable by
Re-authenticating does not help. The credential is valid; the action is not permitted for it. | application/json | Error |
| 404 | No object with that identifier is known to this coordinator. For content routes this means the CID has never been ingested here — content addressing makes it a statement about this federation, not about the bytes, so the same CID may well resolve on another one. Distinguish from | application/json | Error |
| 409 | The blob's data encryption key carries a legal hold, and the database refuses to shred a held key. This is the intended interlock: it stops a takedown destroying evidence that a live matter requires. Clear the hold with | application/json | Error |
GET/api/v1/admin/nodes
List federated pinning nodes.
Requires authentication.
Responses
| Code | Meaning | Type | Shape |
|---|---|---|---|
| 200 | Node list. | application/json | object |
GET/api/v1/admin/nodes/{id}
Requires authentication.
Parameters
| Name | In | Type | Notes |
|---|---|---|---|
| id | path · required | string (uuid) |
Responses
| Code | Meaning | Type | Shape |
|---|---|---|---|
| 200 | Node detail. | application/json | Node |
| 404 | No object with that identifier is known to this coordinator. For content routes this means the CID has never been ingested here — content addressing makes it a statement about this federation, not about the bytes, so the same CID may well resolve on another one. Distinguish from | application/json | Error |
POST/api/v1/admin/nodes/{id}/revoke
Mark a node compromised; revoke its cert and re-replicate its pins.
The poison-pill protocol from FEDERATION_PROTOCOL.md.
On revocation the coordinator (a) revokes the node's Nebula
certificate, (b) tombstones the node's pin assignments,
(c) emits the federation.node_revoked webhook, and
(d) enqueues re-replication for any CIDs whose factor
dropped below target.
Requires authentication.
Parameters
| Name | In | Type | Notes |
|---|---|---|---|
| id | path · required | string (uuid) |
Request body
application/json
{
"reason": "Host reported compromised by its operator on 2026-07-29."
}Responses
| Code | Meaning | Type | Shape |
|---|---|---|---|
| 202 | Revocation accepted; healing dispatched asynchronously. | ||
| 404 | No object with that identifier is known to this coordinator. For content routes this means the CID has never been ingested here — content addressing makes it a statement about this federation, not about the bytes, so the same CID may well resolve on another one. Distinguish from | application/json | Error |
GET/api/v1/admin/pins
List pin assignments (filterable).
Requires authentication.
Parameters
| Name | In | Type | Notes |
|---|---|---|---|
| page | query | integer | |
| per_page | query | integer | |
| state | query | string | |
| node_id | query | string (uuid) | |
| cid | query | Cid |
Responses
| Code | Meaning | Type | Shape |
|---|---|---|---|
| 200 | Pin assignments. | application/json | PaginatedPinAssignments |
POST/api/v1/admin/signed-urls/revoke
Revoke signed URLs by structured (kind, value) tuple.
v2 revocation is structured, not prefix-based. See
docs/specs/SIGNED_URL_FORMAT.md § "Revocation".
Requires authentication.
Request body (required)
application/json
{
"kind": "aud",
"value": "https://forum.example"
}Responses
| Code | Meaning | Type | Shape |
|---|---|---|---|
| 201 | Revocation recorded (idempotent on the (kind, value) pair). | application/json | object |
| 400 | Unknown kind (must be cid, aud, kid, or path_prefix) or empty value. |
POST/api/v1/admin/signed-urls/sign
Mint a signed URL for a content path.
Server-side minting: the unwrapped HMAC signing key never leaves the
coordinator, so signed URLs cannot be minted client-side. The URL is
signed with the current active signing key, bound to aud, and valid
for ttl_seconds (clamped to the configured maximum). The matching
read endpoints (/blob/{cid}, /i/{cid}/...) verify it per
docs/specs/SIGNED_URL_FORMAT.md; a failed verification returns
403 with a signature_* code.
Requires authentication.
Request body (required)
application/json
{
"path": "/i/bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi/640x480.webp",
"ttl_seconds": 86400,
"aud": "https://forum.example"
}Responses
| Code | Meaning | Type | Shape |
|---|---|---|---|
| 201 | Signed URL minted. | application/json | object |
| 400 | The signing request was rejected before a URL was minted. One of: |
GET/api/v1/auth/config
Unauthenticated auth-mode discovery document for clients (SPA/CLI).
Always present in both modes. In local mode returns {mode: local}.
In external mode returns the IdP issuer_url (and client_id/scopes)
so the client can initiate the OIDC authorization-code + PKCE flow itself.
Responses
| Code | Meaning | Type | Shape |
|---|---|---|---|
| 200 | Auth discovery document. | application/json | AuthConfig |
GET/api/v1/auth/jwks.json
JSON Web Key Set for verifying local-issuer access tokens.
Responses
| Code | Meaning | Type | Shape |
|---|---|---|---|
| 200 | JWKS (OKP/Ed25519 signing key). | application/json | Jwks |
| 404 | This coordinator is configured to delegate authentication to an external OIDC provider, so its own token issuer is switched off and the route does not exist. Obtain a token from the configured provider instead; |
POST/api/v1/auth/login
Exchange username + password for an access + refresh token pair (local issuer).
Request body (required)
application/json
{
"username": "operator",
"password": "correct-horse-battery-staple"
}Responses
| Code | Meaning | Type | Shape |
|---|---|---|---|
| 200 | Token pair. | application/json | TokenResponse |
| 401 | Authentication failed. The response is deliberately identical for an unknown username and a wrong password so the endpoint cannot be used to enumerate accounts, and it is rate-limited per address. Repeated failures will begin returning | application/json | Error |
| 404 | This coordinator is configured to delegate authentication to an external OIDC provider, so its own token issuer is switched off and the route does not exist. Obtain a token from the configured provider instead; | application/json | Error |
| 503 | The login path is shedding load: either the per-address rate limit tripped or the bounded password-hashing pool is saturated. This is back-pressure rather than a failure — honour | application/json | Error |
Example response · 200
{
"access_token": "eyJhbGciOiJFZERTQSIsImtpZCI6InNpZy0yMDI2LTA3In0...",
"refresh_token": "v1.MEQCIB3q...c2VjcmV0.9f2b1a7c",
"token_type": "bearer",
"expires_in": 900,
"kid": "sig-2026-07"
}POST/api/v1/auth/logout
Revoke a refresh token.
Request body (required)
application/json
{
"refresh_token": "v1.MEQCIB3q...c2VjcmV0.9f2b1a7c"
}Responses
| Code | Meaning | Type | Shape |
|---|---|---|---|
| 204 | Logged out (idempotent). | ||
| 404 | This coordinator is configured to delegate authentication to an external OIDC provider, so its own token issuer is switched off and the route does not exist. Obtain a token from the configured provider instead; |
POST/api/v1/auth/refresh
Rotate a refresh token for a new pair (single-use; reuse revokes the family).
Request body (required)
application/json
{
"refresh_token": "v1.MEQCIB3q...c2VjcmV0.9f2b1a7c"
}Responses
| Code | Meaning | Type | Shape |
|---|---|---|---|
| 200 | New token pair. | application/json | TokenResponse |
| 401 | The refresh token was rejected. It was expired, already revoked, or — the case worth knowing about — presented a second time. Refresh tokens are single-use, and replay is treated as evidence of theft: the whole token family is revoked, which logs out every session descended from that login. Authenticate again with credentials. | application/json | Error |
| 404 | This coordinator is configured to delegate authentication to an external OIDC provider, so its own token issuer is switched off and the route does not exist. Obtain a token from the configured provider instead; |
Example response · 200
{
"access_token": "eyJhbGciOiJFZERTQSIsImtpZCI6InNpZy0yMDI2LTA3In0...",
"refresh_token": "v1.MEQCIFp7...c2VjcmV0.31d8c04a",
"token_type": "bearer",
"expires_in": 900,
"kid": "sig-2026-07"
}legal
Legal
Public DMCA receipt and DSAR endpoints.
POST/legal/dmca
Submit a DMCA takedown notice.
The receipt endpoint for DMCA Section 512 notices. Notices are
recorded in dmca_cases for the operator to review; this
endpoint does not auto-action. See docs/legal/DMCA_PROCEDURE.md.
Request body (required)
application/json
{
"claimant_name": "Jordan Reyes",
"claimant_email": "legal@example.com",
"sworn_statement": "I have a good faith belief that use of the material described above is not authorised by the copyright owner, its agent, or the law. I swear, under penalty of perjury, that this information is accurate and that I am the copyright owner or authorised to act on the owner's behalf.",
"target_cid": "bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi"
}Responses
| Code | Meaning | Type | Shape |
|---|---|---|---|
| 202 | Notice recorded; no action taken automatically. | application/json | object |
| 400 | The request could not be parsed or failed validation before any work was
attempted: a malformed CID, a body that is not valid JSON, a missing
required field, or a parameter outside its permitted range. The | application/json | Error |
GET/legal/dsar
Public information about how to submit a data-subject access request.
Responses
| Code | Meaning | Type | Shape |
|---|---|---|---|
| 200 | DSAR information. | application/json | object |
health
Health
Liveness probe.
GET/health
Liveness probe.
Always returns 200 if the coordinator is accepting traffic.
Responses
| Code | Meaning | Type | Shape |
|---|---|---|---|
| 200 | Healthy. | application/json | Health |
Example response · 200
{
"status": "ok",
"version": "0.2.0-beta.1",
"time": "2026-07-30T12:00:00Z"
}