Upload Endpoints
Request presigned URLs, confirm uploads, and manage multipart uploads via the REST API.
The upload flow is a two-step process: request a presigned URL, then confirm after the client uploads directly to storage. Files over 10 MB use the multipart endpoints automatically when using the SDK, but you can drive the multipart flow manually via these endpoints.
POST /api/v1/upload/request
Request a presigned URL for a single-file upload. Presigned URLs are valid for 15 minutes.
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
fileName | string | Yes | Original file name (1–255 chars) |
fileSize | number | Yes | File size in bytes (positive integer) |
contentType | string | Yes | MIME type (e.g., image/jpeg) |
routeSlug | string | Yes | File route slug defined in your file router (1–100 chars) |
metadata | object | No | Arbitrary key-value pairs stored with the file |
Response:
{
"fileId": "65f1a2b3c4d5e6f7a8b9c0d1",
"uploadUrl": "https://<account>.r2.cloudflarestorage.com/<key>?X-Amz-Signature=...",
"key": "65f0.../imageUploader/abc123.../photo.jpg",
"cdnUrl": "https://cdn.uploadkit.dev/65f0.../imageUploader/abc123.../photo.jpg"
}curl example:
curl -X POST https://api.uploadkit.dev/api/v1/upload/request \
-H "Authorization: Bearer uk_live_xxxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"fileName": "photo.jpg",
"fileSize": 204800,
"contentType": "image/jpeg",
"routeSlug": "imageUploader",
"metadata": { "userId": "user_123" }
}'After receiving the uploadUrl, the client performs a PUT directly to that URL. The Content-Type header must match the contentType sent to /upload/request — it is locked into the signature.
curl -X PUT "<uploadUrl>" \
-H "Content-Type: image/jpeg" \
--data-binary @photo.jpgError cases:
| Code | HTTP | Cause |
|---|---|---|
UNAUTHORIZED | 401 | Invalid or missing API key |
VALIDATION_ERROR | 400 | Required field missing or wrong type |
NOT_FOUND | 404 | routeSlug does not exist in your project |
INVALID_FILE_TYPE | 400 | contentType not in the route's allowedTypes |
TIER_LIMIT_EXCEEDED | 403 | File size exceeds route/tier limit, or monthly upload/storage quota reached (FREE tier) |
POST /api/v1/upload/complete
Notify the API that the client-side PUT to the presigned URL has finished. This triggers verification against R2 and fires the onUploadComplete webhook.
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
fileId | string | Yes | fileId returned from /upload/request |
metadata | object | No | Optional metadata update (replaces existing metadata if provided) |
Response:
{
"file": {
"id": "65f1a2b3c4d5e6f7a8b9c0d1",
"key": "65f0.../imageUploader/abc123.../photo.jpg",
"name": "photo.jpg",
"size": 204800,
"type": "image/jpeg",
"url": "https://cdn.uploadkit.dev/65f0.../imageUploader/abc123.../photo.jpg",
"status": "UPLOADED",
"metadata": { "userId": "user_123" },
"createdAt": "2026-04-08T12:00:00.000Z"
}
}curl example:
curl -X POST https://api.uploadkit.dev/api/v1/upload/complete \
-H "Authorization: Bearer uk_live_xxxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"fileId": "65f1a2b3c4d5e6f7a8b9c0d1"
}'Error cases:
| Code | HTTP | Cause |
|---|---|---|
NOT_FOUND | 404 | fileId does not exist, belongs to another project, or is no longer in UPLOADING state |
FILE_NOT_IN_STORAGE | 422 | R2 object not found — the client-side PUT may have failed |
POST /api/v1/upload/multipart/init
Initialize a multipart upload for files over 10 MB. Returns presigned URLs for each 5 MB part. Part URLs are valid for 1 hour.
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
fileName | string | Yes | Original file name |
fileSize | number | Yes | Total file size in bytes (must be > 10 MB) |
contentType | string | Yes | MIME type |
routeSlug | string | Yes | File route slug |
metadata | object | No | Stored with the file |
Response:
{
"fileId": "65f1a2b3c4d5e6f7a8b9c0d1",
"uploadId": "mpu_xyz789",
"key": "65f0.../videoUploader/abc123.../large-video.mp4",
"parts": [
{ "partNumber": 1, "uploadUrl": "https://<account>.r2.cloudflarestorage.com/...?partNumber=1&uploadId=mpu_xyz789&..." },
{ "partNumber": 2, "uploadUrl": "https://<account>.r2.cloudflarestorage.com/...?partNumber=2&uploadId=mpu_xyz789&..." },
{ "partNumber": 3, "uploadUrl": "https://<account>.r2.cloudflarestorage.com/...?partNumber=3&uploadId=mpu_xyz789&..." }
]
}Each part URL is for a PUT request with a 5 MB chunk of the file. The last part may be smaller than 5 MB.
curl example:
curl -X POST https://api.uploadkit.dev/api/v1/upload/multipart/init \
-H "Authorization: Bearer uk_live_xxxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"fileName": "large-video.mp4",
"fileSize": 104857600,
"contentType": "video/mp4",
"routeSlug": "videoUploader"
}'Error cases:
| Code | HTTP | Cause |
|---|---|---|
FILE_TOO_SMALL_FOR_MULTIPART | 400 | fileSize is ≤ 10 MB — use /upload/request instead |
INVALID_FILE_TYPE | 400 | contentType not in the route's allowedTypes |
TIER_LIMIT_EXCEEDED | 403 | File size, storage, or upload quota exceeded |
NOT_FOUND | 404 | routeSlug does not exist |
POST /api/v1/upload/multipart/complete
Signal that all parts have been uploaded. R2 assembles the parts into the final object.
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
fileId | string | Yes | fileId from /multipart/init |
uploadId | string | Yes | uploadId from /multipart/init |
parts | array | Yes | Array of { partNumber, etag } — ETags from each part PUT response |
metadata | object | No | Optional metadata update |
curl example:
curl -X POST https://api.uploadkit.dev/api/v1/upload/multipart/complete \
-H "Authorization: Bearer uk_live_xxxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"fileId": "65f1a2b3c4d5e6f7a8b9c0d1",
"uploadId": "mpu_xyz789",
"parts": [
{ "partNumber": 1, "etag": "\"etag-value-1\"" },
{ "partNumber": 2, "etag": "\"etag-value-2\"" },
{ "partNumber": 3, "etag": "\"etag-value-3\"" }
]
}'The response is the same { "file": { ... } } shape returned by /upload/complete.
Error cases:
| Code | HTTP | Cause |
|---|---|---|
NOT_FOUND | 404 | fileId + uploadId pair not found or no longer in UPLOADING state |
POST /api/v1/upload/multipart/abort
Abort an in-progress multipart upload and clean up all uploaded parts. Also removes the pending file record.
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
fileId | string | Yes | fileId from /multipart/init |
uploadId | string | Yes | uploadId from /multipart/init |
curl example:
curl -X POST https://api.uploadkit.dev/api/v1/upload/multipart/abort \
-H "Authorization: Bearer uk_live_xxxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"fileId": "65f1a2b3c4d5e6f7a8b9c0d1",
"uploadId": "mpu_xyz789"
}'Response:
{ "ok": true }When using the @uploadkitdev/core or @uploadkitdev/react SDK, multipart uploads are handled automatically for files over 10 MB. You do not need to call these endpoints manually.