UploadKit
Getting Started

API Only

Use the UploadKit REST API directly from any language or framework.

The UploadKit REST API lets you manage uploads from any backend language or framework — Python, Go, Ruby, PHP, or plain curl. No SDK required.

All file uploads follow the presigned URL flow: your server requests a short-lived upload URL, your client uploads directly to storage, then your server confirms completion.

Authentication

Pass your API key as a Bearer token in the Authorization header on every request:

curl https://api.uploadkit.dev/api/v1/upload/request \
  -H "Authorization: Bearer uk_live_xxxxxxxxxxxxxxxxxxxxx"

Never expose your API key in client-side JavaScript. For browser uploads, use the React SDK which handles key security for you.

Request a presigned URL

Before uploading, request a presigned PUT URL from the UploadKit API. This validates your API key, checks file type and size against your route configuration, and generates a temporary upload URL (valid for 15 minutes).

POST /api/v1/upload/request

Request body:

FieldTypeRequiredDescription
fileNamestringYesOriginal file name (1–255 chars)
fileSizenumberYesFile size in bytes (positive integer)
contentTypestringYesMIME type (e.g. image/jpeg)
routeSlugstringYesYour file route name (1–100 chars)
metadataobjectNoArbitrary key/value pairs stored with the file and passed to your webhook
curl -X POST https://api.uploadkit.dev/api/v1/upload/request \
  -H "Authorization: Bearer uk_live_xxxxxxxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "fileName": "profile.jpg",
    "fileSize": 204800,
    "contentType": "image/jpeg",
    "routeSlug": "imageUploader"
  }'
const response = await fetch('https://api.uploadkit.dev/api/v1/upload/request', {
  method: 'POST',
  headers: {
    Authorization: 'Bearer uk_live_xxxxxxxxxxxxxxxxxxxxx',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    fileName: 'profile.jpg',
    fileSize: 204800,
    contentType: 'image/jpeg',
    routeSlug: 'imageUploader',
  }),
});

const { fileId, uploadUrl } = await response.json();
import requests

response = requests.post(
    'https://api.uploadkit.dev/api/v1/upload/request',
    headers={
        'Authorization': 'Bearer uk_live_xxxxxxxxxxxxxxxxxxxxx',
        'Content-Type': 'application/json',
    },
    json={
        'fileName': 'profile.jpg',
        'fileSize': 204800,
        'contentType': 'image/jpeg',
        'routeSlug': 'imageUploader',
    },
)

data = response.json()
file_id = data['fileId']
upload_url = data['uploadUrl']

Response:

{
  "fileId": "65f1a2b3c4d5e6f7a8b9c0d1",
  "uploadUrl": "https://<account>.r2.cloudflarestorage.com/<key>?X-Amz-Signature=...",
  "key": "65f0.../imageUploader/abc123.../profile.jpg",
  "cdnUrl": "https://cdn.uploadkit.dev/65f0.../imageUploader/abc123.../profile.jpg"
}

Upload to storage

Use the presigned URL to PUT the file directly to R2/S3. You must include the exact Content-Type that was used when requesting the URL — it's locked into the signature.

curl -X PUT "<uploadUrl>" \
  -H "Content-Type: image/jpeg" \
  --data-binary @profile.jpg
// fileBuffer is an ArrayBuffer, Blob, or ReadableStream
await fetch(uploadUrl, {
  method: 'PUT',
  headers: { 'Content-Type': 'image/jpeg' },
  body: fileBuffer,
});
with open('profile.jpg', 'rb') as f:
    requests.put(
        upload_url,
        headers={'Content-Type': 'image/jpeg'},
        data=f,
    )

Confirm the upload

After the PUT succeeds, notify UploadKit that the upload is complete. This triggers your webhook (if configured) and marks the file as available.

POST /api/v1/upload/complete

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"}'
const result = await fetch('https://api.uploadkit.dev/api/v1/upload/complete', {
  method: 'POST',
  headers: {
    Authorization: 'Bearer uk_live_xxxxxxxxxxxxxxxxxxxxx',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ fileId }),
});

const { file } = await result.json();
// file.url — permanent CDN URL for the uploaded file
result = requests.post(
    'https://api.uploadkit.dev/api/v1/upload/complete',
    headers={
        'Authorization': 'Bearer uk_live_xxxxxxxxxxxxxxxxxxxxx',
        'Content-Type': 'application/json',
    },
    json={'fileId': file_id},
)

file = result.json()['file']
print(file['url'])  # permanent CDN URL

Response:

{
  "file": {
    "id": "65f1a2b3c4d5e6f7a8b9c0d1",
    "key": "65f0.../imageUploader/abc123.../profile.jpg",
    "name": "profile.jpg",
    "size": 204800,
    "type": "image/jpeg",
    "url": "https://cdn.uploadkit.dev/65f0.../imageUploader/abc123.../profile.jpg",
    "status": "UPLOADED",
    "createdAt": "2026-04-07T11:59:30Z"
  }
}

List files

GET /api/v1/files

Returns paginated files for the current project, newest first.

curl "https://api.uploadkit.dev/api/v1/files?limit=20" \
  -H "Authorization: Bearer uk_live_xxxxxxxxxxxxxxxxxxxxx"
const response = await fetch('https://api.uploadkit.dev/api/v1/files?limit=20', {
  headers: { Authorization: 'Bearer uk_live_xxxxxxxxxxxxxxxxxxxxx' },
});
const { files, nextCursor, hasMore } = await response.json();
response = requests.get(
    'https://api.uploadkit.dev/api/v1/files',
    headers={'Authorization': 'Bearer uk_live_xxxxxxxxxxxxxxxxxxxxx'},
    params={'limit': 20},
)
data = response.json()
files = data['files']

Query parameters:

ParameterTypeDefaultDescription
limitnumber50Results per page (1–100)
cursorstringPagination cursor from previous response

Delete a file

DELETE /api/v1/files/:key

Permanently removes the file from storage and marks it deleted in the database. The key path parameter must be URL-encoded.

curl -X DELETE \
  "https://api.uploadkit.dev/api/v1/files/65f0...%2FimageUploader%2Fabc123...%2Fprofile.jpg" \
  -H "Authorization: Bearer uk_live_xxxxxxxxxxxxxxxxxxxxx"
const key = '65f0.../imageUploader/abc123.../profile.jpg';
await fetch(`https://api.uploadkit.dev/api/v1/files/${encodeURIComponent(key)}`, {
  method: 'DELETE',
  headers: { Authorization: 'Bearer uk_live_xxxxxxxxxxxxxxxxxxxxx' },
});
from urllib.parse import quote

key = '65f0.../imageUploader/abc123.../profile.jpg'
requests.delete(
    f'https://api.uploadkit.dev/api/v1/files/{quote(key, safe="")}',
    headers={'Authorization': 'Bearer uk_live_xxxxxxxxxxxxxxxxxxxxx'},
)

Response: { "ok": true }.

Delete multiple files

DELETE /api/v1/files

Bulk delete up to 100 files in one request. Prefer this endpoint when your app needs to remove several UploadKit files at once.

curl -X DELETE "https://api.uploadkit.dev/api/v1/files" \
  -H "Authorization: Bearer uk_live_xxxxxxxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{"keys":["65f0.../imageUploader/abc123.../profile.jpg"]}'
const res = await fetch('https://api.uploadkit.dev/api/v1/files', {
  method: 'DELETE',
  headers: {
    Authorization: 'Bearer uk_live_xxxxxxxxxxxxxxxxxxxxx',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ keys }),
});

const result = await res.json();
response = requests.delete(
    'https://api.uploadkit.dev/api/v1/files',
    headers={
        'Authorization': 'Bearer uk_live_xxxxxxxxxxxxxxxxxxxxx',
        'Content-Type': 'application/json',
    },
    json={'keys': keys},
)
result = response.json()

Error responses

All errors follow a consistent shape — see the Error Codes reference for the full list.

{
  "error": {
    "type": "invalid_request",
    "code": "TIER_LIMIT_EXCEEDED",
    "message": "You have exceeded your file size limit",
    "suggestion": "Upgrade your plan at app.uploadkit.dev/billing"
  }
}
HTTP statusTypical codeWhen
400VALIDATION_ERROR, INVALID_FILE_TYPEInvalid request body or file type rejected
401UNAUTHORIZEDMissing or invalid API key
403TIER_LIMIT_EXCEEDEDFile size, quota, or project/key count exceeded
404NOT_FOUNDResource not found in this project
409DUPLICATE_SLUGA file router with that slug already exists
422FILE_NOT_IN_STORAGEConfirm called but PUT did not complete
429RATE_LIMITEDRate limit exceeded
500INTERNAL_ERRORServer-side error

On this page