api.file.kiwi
Public API for file.kiwi — create WebFolders and upload E2E encrypted files programmatically.
Upload a file list to get download links instantly — uploads are processed asynchronously.
Base URL: https://api.file.kiwi
Overview
1. Generate encryption keys (client-side)
secretKey ← 16 random bytes (base64url)
ske ← secretKey encrypted via RFC 8188
2. POST /v1/delivery-webfolder
{ title, ske, files: [{ filename (encrypted), filesize }] }
→ webfolderId, apiAuth, files[].fid, files[].uploadUrls
3. Share
https://file.kiwi/{webfolderId}#{secretKey}
(secretKey is never sent to the server — URL fragment is client-only)
4. Upload chunks (client-side, encrypted via RFC 8188)
PUT {uploadUrls.head}{path}/{chunkNumber}?{tail}&X-Amz-Signature={sig}
Body: <encrypted chunk>
5. Verify upload
GET /v1/upload/check/{fid}?webfolderId=...&apiAuth=...
→ { complete: true/false, missing: [...] }
No authentication required. (and some limits)
SDK
- Node.js / JavaScript: github.com/file-kiwi/node
Endpoints
POST /v1/delivery-webfolder
Create a WebFolder and get presigned upload URLs for all files in a single call. All files are E2E encrypted — the server never sees plaintext data.
Request
POST https://api.file.kiwi/v1/delivery-webfolder
Content-Type: application/json
{
"title": "Project assets",
"ske": "<encrypted-secret-key>",
"files": [
{ "filename": "<encrypted-filename>", "filesize": 1048576 },
{ "filename": "<encrypted-filename>", "filesize": 5242880, "mimetype": "image/jpeg" }
]
}
| Field | Type | Required | Description |
|---|---|---|---|
title |
string | Yes | WebFolder title (max 200 characters) |
ske |
string | Yes | Encrypted secret key for E2E encryption (see E2E Encryption) |
files |
array | Yes | Files to upload (1–10 files) |
files[].filename |
string | Yes | Encrypted filename (base64) |
files[].filesize |
number | Yes | File size in bytes |
files[].mimetype |
string | No | MIME type |
Response 200 OK
{
"webfolderId": "a1b2c3d4",
"webfolderUrl": "https://file.kiwi/a1b2c3d4",
"retentionHours": 90,
"apiAuth": "a1b2c3d4e5f6g7h8",
"files": [
{
"filename": "<encrypted-filename>",
"fid": "e5f6g7h8",
"chunkSize": 10485760,
"chunks": 1,
"freeDownloadHours": 24,
"uploadUrls": {
"head": "https://filekiwi.xxx.r2.cloudflarestorage.com/",
"tail": "X-Amz-Algorithm=AWS4-HMAC-SHA256&...",
"path": "stbox/0328/e5f6g7h8",
"signatures": ["<signature_for_chunk_00001>"],
"headers": {
"x-amz-meta-folder_id": "a1b2c3d4",
"x-amz-meta-chunks": "1"
}
}
}
]
}
| Response Field | Description |
|---|---|
webfolderId |
Unique WebFolder identifier |
webfolderUrl |
Shareable URL (append #secretKey for decryption) |
retentionHours |
How long files are stored on the server (90 hours) |
apiAuth |
Authentication token for upload verification (keep secret) |
files[].fid |
Unique file identifier |
files[].chunkSize |
Size of each upload chunk in bytes |
files[].chunks |
Total number of chunks |
files[].freeDownloadHours |
Hours of free download availability after upload completes |
files[].uploadUrls |
Presigned URL components for chunk uploads |
Error Responses
| Status | Body | Cause |
|---|---|---|
400 |
{"error": "title is required"} |
Missing or empty title |
400 |
{"error": "title must be 200 characters or less"} |
Title too long |
400 |
{"error": "files array is required and must not be empty"} |
Missing files array |
400 |
{"error": "Maximum 10 files per request"} |
Too many files |
400 |
{"error": "files[N].filename is required"} |
Missing filename at index N |
400 |
{"error": "files[N].filesize must be a positive number"} |
Invalid filesize at index N |
500 |
{"error": "Failed to create webfolder"} |
Internal error creating WebFolder |
500 |
{"error": "Failed to register files"} |
Internal error registering files |
GET /v1/upload/check/:fid
Verify which chunks have been uploaded for a file.
When apiAuth is provided and matches the token returned from WebFolder creation, the server extends the file's expiration times if they have less than 2 hours remaining. This prevents files from expiring during long uploads.
Request
GET https://api.file.kiwi/v1/upload/check/e5f6g7h8?webfolderId=a1b2c3d4&apiAuth=a1b2c3d4e5f6g7h8
| Parameter | Type | Required | Description |
|---|---|---|---|
:fid |
string | Yes | File ID (URL path parameter) |
webfolderId |
string | Yes | WebFolder ID |
apiAuth |
string | No | Auth token from WebFolder creation. Extends expiration on completion if < 2h remaining. |
Response 200 OK
Upload complete:
{
"complete": true,
"missing": []
}
Upload incomplete:
{
"complete": false,
"missing": [2, 5]
}
Error Responses
| Status | Body | Cause |
|---|---|---|
400 |
{"error": "fid is required"} |
Missing file ID |
400 |
{"error": "webfolderId is required"} |
Missing WebFolder ID |
404 |
{"error": "File not found"} |
File metadata not found in database |
500 |
{"error": "Failed to check upload status"} |
R2 listing failed |
Upload Flow
1. Create WebFolder
POST /v1/delivery-webfolder
-> webfolderId, files[].uploadUrls
2. Share
Share the WebFolder URL with recipients:
https://file.kiwi/{webfolderId}#{secretKey}
The #secretKey is the decryption key. It is never sent to the server (URL hash fragments are client-only).
3. Upload Chunks
For each file, upload each chunk via presigned PUT URL:
PUT {head}{path}/{chunkNumber}?{tail}&X-Amz-Signature={signatures[i]}
Headers: (all headers from uploadUrls.headers)
Body: <encrypted chunk data>
chunkNumberis zero-padded to 5 digits (e.g.,00001,00002)- All headers from
uploadUrls.headersmust be included in the PUT request - Presigned URLs expire after 36 hours
- Recommended upload order: first chunk → last chunk → remaining chunks sequentially. This enables video preview on the receiver's side, as the first chunk contains the file header and the last chunk contains the moov atom (for MP4).
4. Verify Upload
GET /v1/upload/check/{fid}?webfolderId=WEBFOLDER_ID&apiAuth=TOKEN
-> { complete: true/false, missing: [...] }
When complete: true, the file is marked as ready for download and progress tracking data is cleaned up.
Limits
Default limits for unauthenticated requests. To increase limits with an API key, contact [email protected] or chat.
| Limit | Value |
|---|---|
| Files per WebFolder | 10 |
| Max file size | Unlimited |
| Max title length | 200 characters |
| File retention | 90 hours |
| Presigned URL validity | 36 hours |
| Concurrent chunk uploads | 1 (upload one chunk at a time) |
| Rate limit | Undisclosed |
Free Download Hours by File Size
| File Size | Free Download Hours |
|---|---|
| < 10 GB | 24 hours |
| 10 GB – 20 GB | 12 hours |
| ≥ 20 GB | 6 hours |
These hours differ from the web version.
E2E Encryption
All files are encrypted client-side before upload using RFC 8188 — Encrypted Content-Encoding for HTTP (AES-128-GCM). The server never sees plaintext data — it stores only ciphertext.
ske (Encrypted Secret Key)
ske is the secret key encrypted via RFC 8188, sent to the server for ownership verification. It is generated as follows:
- Generate a Keychain —
new Keychain(null, null)creates a random 16-byte key and 16-byte salt usingcrypto.getRandomValues(). - Extract
secretKey— the 16-byte key is encoded as base64url (keychain.keyB64). This becomes the URL fragment#secretKey. - Encrypt
secretKeyintoske— the base64url string is UTF-8 encoded, then encrypted usingkeychain.encryptStream():- The key is derived via HKDF-SHA-256 (using the Keychain's salt) into an AES-128-GCM content encryption key and nonce.
- The data is encrypted per RFC 8188 (21-byte header + AES-128-GCM ciphertext with authentication tag).
- The resulting bytes are base64 encoded to produce the
skestring.
- Share via URL fragment — the original
secretKeyis placed in the URL hash (#{secretKey}), which is never sent to the server.
The
#secretKeyin the URL is the only way to decrypt the files. If it is lost, the files cannot be recovered.