Skip to content

API overview ​

The stable client, lifecycle, and node-health contract is described here with worked examples. The generated per-operation tables are on Endpoint reference, and the machine-readable document is openapi.yaml.

Examples use https://ipfs.example.org, which stands for a reverse proxy in front of the node. The process itself serves plain HTTP; see Security and privacy.

Contract and versioning ​

docs/openapi.yaml covers the routes an application and an operator depend on. Low-level Helia and libp2p routes and the optional debug route are deliberately outside it — they are operator tools whose shape may change with the runtime.

The version field in GET /api/node/health, GET /api/node/info, and GET /api/node/details is the version from package.json. It is the software version, not the contract version, and it is unrelated to the libp2p replication protocol version reported by GET /api/storage/metrics.

Authentication ​

There is one credential: the administrative key, sent as the x-api-key header and compared in constant time against adminApiKey.

ClassRoutesRequirement
PublicGET /, GET /api/node/health, GET /api/node/infoNone
Public file transferPOST /api/file/upload, GET /api/file/:cidNone; rate, size, and admission limits apply
Public storage stateGET /api/file/:cid/status, GET /api/storage/metrics, GET /api/storage/policyNone
AdministrativeGET /api/node/details, POST /api/file/:cid/confirm, POST /api/file/:cid/unpin, POST /api/storage/gc, POST /api/storage/repair, all /api/helia/*, all /api/libp2p/*x-api-key
Disabled by defaultGET /api/debug/autopeeringenableDebugApi: true and x-api-key

A missing or wrong key answers 401. An unset adminApiKey answers 503 Service not configured, so administrative routes fail closed rather than open. There is no end-user identity or session layer.

GET / answers with the plain text IPFS node.

Upload a file ​

Send one or more multipart parts named files. Text fields are rejected with 400.

bash
curl --fail-with-body \
  --form 'files=@file.txt' \
  https://ipfs.example.org/api/file/upload
json
{
  "filesNames": ["file.txt"],
  "cids": ["bafkreif7v2d2wdyh6pz5y2pwmrpegfpdgh5u7n5vomxnbofraqhuk2wapm"],
  "files": [
    {
      "cid": "bafkreif7v2d2wdyh6pz5y2pwmrpegfpdgh5u7n5vomxnbofraqhuk2wapm",
      "name": "file.txt",
      "state": "confirmed",
      "expiresAt": null,
      "replication": {
        "mode": "quorum",
        "desiredCopies": 4,
        "copies": 3,
        "required": 1,
        "acknowledged": 1,
        "replicaCount": 0,
        "cachedCount": 0,
        "satisfied": true,
        "networkTooSmall": true,
        "failedAttemptCount": 0,
        "attempts": []
      }
    }
  ],
  "replication": {
    "mode": "quorum",
    "desiredCopies": 4,
    "copies": 3,
    "required": 1,
    "acknowledged": 1,
    "replicaCount": 0,
    "cachedCount": 0,
    "satisfied": true,
    "networkTooSmall": true,
    "failedAttemptCount": 0,
    "attempts": []
  }
}

Placement is decided per CID, so each entry of files carries its own report. The top-level replication object repeats the first file's report for clients written against the earlier shape; new clients should read the per-file field.

The report is counts and per-attempt outcomes only. It never carries node names, peer ids, or peer error text.

StatuscodeMeaning
200Every file was stored and pinned
400no_fileNo file was sent
400too_many_filesMore files than maxFileCount
400file_too_largeOne file exceeded uploadLimitSizeBytes
400multipart_fieldsA non-file text field was sent
400invalid_multipartThe multipart body could not be parsed
413request_too_largeThe combined size exceeded storage.maxRequestSizeBytes
429rate_limitedThe upload rate window was exceeded. Retry-After and RateLimit are set
429upload_concurrencyThe concurrent upload limit was exceeded. Retry-After is 5
503replication_quorumA required replication quorum could not be reached
507insufficient_storageStoring the request would consume storage.diskReserveBytes
500Storage or replica settlement failed; do not treat it as a clean rejection

An interrupted upload leaves no blocks behind: each request owns a session that removes exactly the blocks it created. See the storage lifecycle.

Download a file ​

bash
curl --fail-with-body \
  --output file.bin \
  https://ipfs.example.org/api/file/bafkreif7v2d2wdyh6pz5y2pwmrpegfpdgh5u7n5vomxnbofraqhuk2wapm

Responses are the complete representation, served as application/octet-stream with Content-Disposition: attachment; filename="<cid>" and X-Content-Type-Options: nosniff. A successful response carries an ETag holding the quoted CID, Cache-Control: private, max-age=3600, must-revalidate, and Accept-Ranges: none. A matching If-None-Match answers 304 after availability has been checked.

StatuscodeMeaning
200The response contains the requested file as an attachment
304The If-None-Match validator matched
400The CID is invalid. The body is {"error":"Invalid CID"} and has no code
408file_timeoutThe file could not be found or retrieved before the configured timeout
429rate_limitedThe read rate window was exceeded. Retry-After and RateLimit are set
429download_concurrencyThe global download slot limit was exceeded. Retry-After is 5
429download_client_concurrencyThis client address holds its download share. Retry-After is 5
500An unexpected internal failure occurred before streaming started

Range headers are ignored. Discovery, idle transfer time, and the size-aware complete transfer all have bounded deadlines and all end in 408 with code file_timeout, so a failed request may spend more than one findFileTimeout before answering. A client disconnect cancels the underlying retrieval. If an error occurs after response bytes have started, the server terminates the incomplete response, because a status and a JSON body can no longer be sent safely.

The node never answers 404 for a CID it cannot retrieve: it cannot know whether the content exists somewhere it cannot see.

File status ​

bash
curl --fail-with-body \
  https://ipfs.example.org/api/file/bafkreif7v2d2wdyh6pz5y2pwmrpegfpdgh5u7n5vomxnbofraqhuk2wapm/status
json
{
  "cid": "bafkreif7v2d2wdyh6pz5y2pwmrpegfpdgh5u7n5vomxnbofraqhuk2wapm",
  "state": "confirmed",
  "pinned": true,
  "createdAt": 1720614998797,
  "expiresAt": null,
  "confirmedAt": 1720614998797,
  "replication": { "acknowledged": 1, "required": 1, "heldLocally": true }
}

state is temporary, confirmed, or expired. An unknown CID answers 404. The response carries no filename and no peer identity.

Storage metrics and policy ​

bash
curl --fail-with-body https://ipfs.example.org/api/storage/metrics
curl --fail-with-body https://ipfs.example.org/api/storage/policy

metrics reports pinned, reclaimable, available, and reserved bytes, the lifecycle counters, the state of the background jobs, and the libp2p replication protocol this node speaks. Nodes on different protocol versions cannot place copies on each other, so a mixed deployment is visible here. Values refresh on diskUsageScanPeriod, not per request.

policy reports the limits and the durability mode a client should expect before uploading: maxFileCount, uploadLimitSizeBytes, maxRequestSizeBytes, the concurrency limits, confirmationRequired, temporaryTtlMs, and a durability object holding mode (quorum or best-effort), placement, and ackQuorum.

Both routes are public and carry no filenames, CID lists, or peer identity.

Node health ​

bash
curl --fail-with-body https://ipfs.example.org/api/node/health

This route always returns HTTP 200. Consumers must read state, which is starting, ready, stale, or degraded.

height is a persisted, monotonic Unix-millisecond checkpoint at the start of a fixed round. It advances only when startup reconciliation, storage freshness and reserve, repair freshness, and the configured peer attestations all pass, and it freezes on failure. Repair freshness requires a complete cycle inside health.repairMaxAgeMs with no backlog, unless health.repairBacklogGraceCycles still covers that many consecutive unsuccessful complete cycles.

State changes are asymmetric. The response is served from the last checkpoint, and reading it recomputes only what elapsed time can decide, so a node may be downgraded to degraded or stale between checkpoints. Returning to ready requires a successful checkpoint, so expect up to health.checkpointIntervalMs of lag after the underlying fault clears.

evaluatedAt dates the last checkpoint attempt, which may have failed. checkpoint.observedAt dates the last attempt that succeeded. checks.checkpointFresh, checks.storageFresh, and checks.repairFresh are recomputed at read time; every other entry in checks, and membership and startup, describe evaluatedAt.

membership.version identifies the configured peer-set epoch. Changing the node list resets the persisted checkpoint for that node, so height stays 0 until the first valid checkpoint under the new membership. Compare heights only between nodes reporting the same membership version, and treat a version change as a new epoch.

A full annotated example and the alerting rules are in Monitoring and health.

Legacy node information ​

bash
curl --fail-with-body https://ipfs.example.org/api/node/info

This public compatibility route retains version, timestamp, heliaStatus, blockstoreSizeMb, datastoreSizeMb, and availableSizeInMb for the current PWA and iOS application. It exposes no peer identity and no topology.

Administrative node details ​

bash
curl --fail-with-body \
  --header 'x-api-key: your-generated-key' \
  https://ipfs.example.org/api/node/details

The authenticated response adds peerId, multiAddresses, byte-accurate storage figures (pinnedBytes, reclaimableBytes, availableBytes, reservedBytes), the complete health object, bounded HTTP counters under http, and admission-limiter occupancy under concurrency. Counters accumulate since process start and carry no paths, CIDs, IP addresses, or user-controlled labels.

Garbage collection ​

bash
curl --fail-with-body --request POST \
  --header 'x-api-key: your-generated-key' \
  'https://ipfs.example.org/api/storage/gc?dryRun=true'

?dryRun=true reports the exact CIDs that would be released and retained without touching a block, which is the supported way to review a deletion policy before enabling the scheduled collector. ?force=true collects even when the blockstore is below the high watermark. A run already in progress answers 409.

The report carries trigger, blockstoreBytesBefore, estimatedBytesAfter, releasedCids, retainedCids, demoted, removedBlocks, removedCids, repairedPins, unprotected, and errors. demoted lists files whose local copy was handed over to their designated holders.

Replication repair ​

bash
curl --fail-with-body --request POST \
  --header 'x-api-key: your-generated-key' \
  https://ipfs.example.org/api/storage/repair

Runs the next bounded repair pass and reports its progress and the cycle state. A run already in progress answers 409. Repair places copies of any confirmed file whose designated holders have not all acknowledged.

Administrative and debug routes ​

These are operator tools. They are not part of docs/openapi.yaml and may change with the runtime.

RoutePurpose
GET /api/helia/pinsList the pin set
POST /api/helia/pin/:cidPin and register arbitrary content
GET /api/helia/pins/isPinned/:cidReport whether a CID is pinned
GET /api/libp2p/statusReport libp2p status
GET /api/libp2p/peersList connected peers
GET /api/libp2p/connectionsList connections
GET /api/libp2p/peerStoreRead peer-store data
GET /api/libp2p/peerInfoRead information about one peer
GET /api/libp2p/dialDial a peer on request
GET /api/libp2p/services/pingPing a peer
GET /api/debug/autopeeringRun one peering pass on request

GET /api/helia/routing/findProviders/:cid no longer exists: provider lookup needs content routing, and this node registers none. /api/debug/* is not mounted at all unless enableDebugApi is true, and it still requires the key.

Error format ​

Errors are JSON objects with an error string. Operational upload and read failures also set code to a stable identifier. Clients should branch on code when it is present. Responses that are not in the table below omit code, including validation messages such as Invalid CID, authentication failures, and 500 Internal Server Error.

codeStatusMeaning
no_file400The upload contained no files part
too_many_files400The upload contained more parts than maxFileCount
file_too_large400One part exceeded uploadLimitSizeBytes
multipart_fields400The multipart body contained a text field
invalid_multipart400The multipart body was rejected for another parser limit
request_too_large413The declared or streamed size exceeded storage.maxRequestSizeBytes
rate_limited429The fixed window for this route was exceeded
upload_concurrency429No upload slot was free
download_concurrency429No download slot was free
download_client_concurrency429This client address holds its download share
file_timeout408Discovery or retrieval hit its deadline
lifecycle_busy409A confirm, release, or replication step found the file mid-transition
replication_quorum503requireQuorumOnUpload rejected the upload
insufficient_storage507Admitting the upload would consume storage.diskReserveBytes

A 429 with code rate_limited is the window limiter. It sets Retry-After to the seconds left in the window and sends draft-8 RateLimit and RateLimit-Policy headers. An admission 429 sets Retry-After: 5 and does not add its own rate-window headers. POST /api/file/upload and GET /api/file/:cid run the window limiter first, so an admission refusal can still carry the RateLimit headers that limiter already wrote. Identify the refusal by code. x-powered-by is disabled, and an unmatched path answers 404 from the not-found handler.

Machine-readable specification ​

The OpenAPI 3.1 document is published at openapi.yaml and lives in the repository at docs/openapi.yaml.

Neither the node nor this site serves an interactive API explorer. There is no /swagger endpoint and no Swagger UI; use the specification file with a client generator or an editor of your choice.

Released under the GPL-3.0 License.