api.file.kiwi v1
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
For the v2 API, see v2 API documentation.
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 |
files[].uploadUrls.head |
Base URL of the R2 endpoint |
files[].uploadUrls.tail |
Query string with AWS SigV4 parameters (without X-Amz-Signature) |
files[].uploadUrls.path |
Object key prefix for the file's chunks |
files[].uploadUrls.signatures |
Per-chunk X-Amz-Signature values, indexed by chunk number (0-based) |
files[].uploadUrls.headers |
Required request headers — must be sent verbatim with every chunk PUT (signature is bound to them) |
files[].uploadUrls.headers.x-amz-meta-folder_id |
WebFolder ID (same as top-level webfolderId) — stored as R2 object metadata |
files[].uploadUrls.headers.x-amz-meta-chunks |
Total chunk count as a string (same as files[].chunks) — stored as R2 object metadata |
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 (for example,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, then remaining chunks sequentially. This enables video preview on the receiver side for formats such as 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 mini@file.kiwi or chat.
| Limit | Value |
|---|---|
| Files per WebFolder | 10 |
| Max file size | 999 GiB |
| 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 |
| Download count (Korea) | 3 (details) |
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.