Skip to main content
With a direct upload, the file’s bytes never pass through the ROOTKey API. You declare the file and its SHA-256, ROOTKey answers with a signed URL, you send the bytes straight to the storage, and then you complete the upload so ROOTKey can anchor the hash and create the file. The API only handles small JSON requests, so uploads stay fast for large files and keep up when you send many files at once. Create file, which sends the file in the request itself, stays available. Use a direct upload when the file is large, or when you register many files, and keep Create file for the simple case.
Direct uploads are not enabled on every deployment. If Start an upload answers 404 with the errorCode presigned_file_upload_disabled, use Create file instead.

How it works

The storage itself checks the bytes against the SHA-256, the length and the media type you declared, so a file can only be stored if it is the file you said it is. Nothing is anchored until you complete the upload, and a hash that was not verified is never anchored. ROOTKey picks the mode when you start the upload, and your client follows what the response says:

Upload a file

1

Hash the file

Compute the SHA-256 of the whole file, as 64 hexadecimal characters. This is the hash that gets anchored.
2

Start the upload

Start an upload with the vault, the file name, its size in bytes, the hash and the media type.
The fileId is the id the file will have once the upload is completed. To upload a new version of an existing file, add its parentId to the request.
3

Send the bytes

PUT the file to upload.url. Send exactly the headers in upload.headers: they are part of the signature, and the storage refuses a request whose headers, length or bytes differ from what you declared. Your HTTP client sets Content-Length itself.This request goes to the storage, not to the ROOTKey API, so it carries no x-api-key header.
4

Complete the upload

Complete the upload. It takes no body.
This is the same answer as Create file, plus the fileId. As with every write, the anchor is confirmed asynchronously: see the API overview.

Example in Node.js

This example uploads one file that goes in a single PUT. It needs Node.js 18 or later, and reads the whole file into memory, which is fine for the sizes that use single mode.

Large files: multipart uploads

When Start an upload answers "mode": "multipart", it does not return a URL. It returns a plan instead:
Split the file into partCount parts of partSize bytes each; the last part is the remainder. Always follow the partSize ROOTKey returns instead of choosing your own.
1

Sign the parts you are about to send

Sign part URLs for up to 100 parts per call, and send the SHA-256 of each part with its number. Each URL lives for a few minutes, so ask for them shortly before you send the parts, a few at a time, instead of all at once.
Each entry in the answer has its own url and headers, signed for that part’s exact length.
2

Send each part

PUT each part to its URL with exactly the headers returned for it (the object may be empty). Parts can be sent in parallel. If a part fails, send that part again; you do not have to start over.
3

Complete the upload

Complete the upload. For a multipart upload the answer is 202:
ROOTKey now reads the assembled file and checks its SHA-256 against the one you declared. Only then does it anchor the hash.
4

Wait for the result

Poll Get upload status until status is COMPLETED, which carries the chainId, transactionHash and fileId, or FAILED, whose failureCode says why. Polling every few seconds is enough.
Always sending each part’s sha256, and always sending the headers returned for it, works on every deployment. Where the storage can check a part as it arrives, a corrupted part is refused at its PUT and only that part is sent again. Where it cannot, the sha256 is ignored, and a corrupted file is caught when the whole file is verified: the upload then fails with file_upload_checksum_mismatch and you start a new one.

Retries and expiry

  • Completing is safe to repeat. If you lose the answer to Complete, call it again: a completed upload answers with its file, and an upload that is being completed is never anchored twice. If the answer is file_upload_finalizing, the upload is being completed; check its status and try again later.
  • Signed URLs are short-lived. A URL must be used within a few minutes (upload.expiresAt). For a multipart upload, sign the part again. For a single upload, abort the upload and start a new one.
  • Uploads expire. An upload you never complete expires (expiresAt in the response, 30 minutes for a single upload and 24 hours for a multipart one) and what was uploaded for it is deleted. Completing an expired upload gets file_upload_expired.
  • Abort what you will not finish. An open upload counts against the limits below until it ends. Abort it when you give up, and use List open uploads to find the ones your client lost track of.
  • A refused upload is not repeated. file_upload_checksum_mismatch and file_upload_size_mismatch mean that what was stored is not what you declared, for example because the file changed while it was uploading. The upload fails; hash the file again and start a new upload.
  • An unknown outcome is never retried by ROOTKey. If Complete answers 500 with creation_outcome_unknown, the anchor may exist. Do not upload the file again before you have checked the vault, as that error describes.

Limits and usage

Going over either open-upload limit gets a 429 with too_many_open_file_uploads or too_many_open_file_upload_bytes, and nothing is created. These limits count every upload your organization has open, whichever API key started it, and an upload stops counting when it is completed, aborted, expired or failed. Only completing an upload uses your plan: one file and one API request, the same as Create file. Starting, signing parts, polling and aborting consume nothing. Your plan’s file and storage limits are checked when you start the upload, so you hear about them before you send any bytes, and again when you complete it. Media type. ROOTKey keeps the contentType you declare for types that browsers do not execute (PDF, images, plain text, CSV, JSON, archives, office documents, audio and video), and stores anything else as application/octet-stream. The Content-Type in upload.headers is the one to send.