UploadKit
API Reference

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:

FieldTypeRequiredDescription
fileNamestringYesOriginal file name (1–255 chars)
fileSizenumberYesFile size in bytes (positive integer)
contentTypestringYesMIME type (e.g., image/jpeg)
routeSlugstringYesFile route slug defined in your file router (1–100 chars)
metadataobjectNoArbitrary 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.jpg

Error cases:

CodeHTTPCause
UNAUTHORIZED401Invalid or missing API key
VALIDATION_ERROR400Required field missing or wrong type
NOT_FOUND404routeSlug does not exist in your project
INVALID_FILE_TYPE400contentType not in the route's allowedTypes
TIER_LIMIT_EXCEEDED403File 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:

FieldTypeRequiredDescription
fileIdstringYesfileId returned from /upload/request
metadataobjectNoOptional 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:

CodeHTTPCause
NOT_FOUND404fileId does not exist, belongs to another project, or is no longer in UPLOADING state
FILE_NOT_IN_STORAGE422R2 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:

FieldTypeRequiredDescription
fileNamestringYesOriginal file name
fileSizenumberYesTotal file size in bytes (must be > 10 MB)
contentTypestringYesMIME type
routeSlugstringYesFile route slug
metadataobjectNoStored 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:

CodeHTTPCause
FILE_TOO_SMALL_FOR_MULTIPART400fileSize is ≤ 10 MB — use /upload/request instead
INVALID_FILE_TYPE400contentType not in the route's allowedTypes
TIER_LIMIT_EXCEEDED403File size, storage, or upload quota exceeded
NOT_FOUND404routeSlug 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:

FieldTypeRequiredDescription
fileIdstringYesfileId from /multipart/init
uploadIdstringYesuploadId from /multipart/init
partsarrayYesArray of { partNumber, etag } — ETags from each part PUT response
metadataobjectNoOptional 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:

CodeHTTPCause
NOT_FOUND404fileId + 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:

FieldTypeRequiredDescription
fileIdstringYesfileId from /multipart/init
uploadIdstringYesuploadId 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.

On this page