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 singlePUT. 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:
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 ROOTKey now reads the assembled file and checks its SHA-256 against the one you declared. Only then does it anchor the hash.
202: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 amultipartupload, sign the part again. For asingleupload, abort the upload and start a new one. - Uploads expire. An upload you never complete expires (
expiresAtin the response, 30 minutes for asingleupload and 24 hours for amultipartone) and what was uploaded for it is deleted. Completing an expired upload getsfile_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_mismatchandfile_upload_size_mismatchmean 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
500withcreation_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.
