Welcome to the R2Upload developer reference. Integrate direct chunked file uploads, remote URL streams, bulk URL ingestion, BYOB multi-cloud S3 storage, and authenticated webhooks into your application.
Get up and running in less than 3 minutes.
Sign up for a free developer account with 500 requests/month and $0 egress.
Create a live secret key in your dashboard with granular permissions.
Send direct multipart uploads or stream from remote URLs via REST API.
# Upload your first file via cURL
curl -X POST https://api.r2upload.io/v1/external/files/upload \
-H "X-API-Key: r2u_live_your_api_key_here" \
-F "file=@/path/to/invoice.pdf" \
-F "is_public=true"All external API requests must include your secret API key either in the X-API-Key request header or as a Bearer token in the Authorization header:
| Environment | Base URL | Notes |
|---|---|---|
| Production API | https://api.r2upload.io/v1 | Anycast edge with TLS 1.3 |
| CDN Upload & Delivery | https://cdn.r2upload.io | Global CDN caching & $0 egress |
High-throughput chunked multipart uploads, remote URL streams, and bulk batch ingestion.
/v1/external/files/uploadScope: uploadUpload a binary file directly to storage via standard multipart/form-data. Supports chunked streams up to 5 TB with automatic SHA-256 integrity verification and magic-byte MIME validation.
| Field | In | Type | Required | Description |
|---|---|---|---|---|
| file | body | file | Yes | Binary file payload (multipart/form-data) |
| filename | body | string | Optional | Custom target filename (extension is auto-preserved) |
| folder | body | string | Optional | Virtual destination directory (e.g. /invoices/2026) |
| is_public | body | boolean | Optional | true to generate public CDN link; false for signed presigned URL |
| storage_connection_id | body | integer | Optional | ID of BYOB bucket to store in (defaults to default bucket) |
curl -X POST https://api.r2upload.io/v1/external/files/upload \
-H "X-API-Key: r2u_live_your_api_key_here" \
-F "file=@document.pdf" \
-F "folder=/documents" \
-F "is_public=true"{
"success": true,
"data": {
"uuid": "upl_9f8e7d6c-5b4a-3210-fedc-ba9876543210",
"original_name": "invoice.pdf",
"storage_path": "uploads/2026/09/invoice-9f8e.pdf",
"size_bytes": 1048576,
"mime_type": "application/pdf",
"sha256_hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"is_public": true,
"public_url": "https://cdn.r2upload.io/uploads/2026/09/invoice-9f8e.pdf",
"created_at": "2026-09-04T07:30:00Z"
}
}/v1/external/files/upload-from-urlScope: uploadInstruct R2Upload to fetch a file from any public HTTP/HTTPS URL and stream it directly into your storage. Includes full SSRF protection blocking private subnets (127.0.0.1, 10.0.0.0/8, 169.254.169.254).
| Field | In | Type | Required | Description |
|---|---|---|---|---|
| url | body | string | Yes | Publicly accessible URL to fetch |
| filename | body | string | Optional | Optional filename override |
| folder | body | string | Optional | Destination folder |
| async | body | boolean | Optional | Set true to process in background and receive webhook |
curl -X POST https://api.r2upload.io/v1/external/files/upload-from-url \
-H "X-API-Key: r2u_live_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/assets/banner.png",
"folder": "/banners",
"is_public": true
}'{
"success": true,
"data": {
"uuid": "upl_1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
"original_name": "product-image.webp",
"size_bytes": 524288,
"mime_type": "image/webp",
"public_url": "https://cdn.r2upload.io/files/product-image.webp"
}
}/v1/external/files/bulk-upload-from-urlsScope: uploadSubmit up to 100 remote URLs in a single API call for parallel asynchronous ingestion. Ideal for migrating entire media libraries or syncing CDN assets.
| Field | In | Type | Required | Description |
|---|---|---|---|---|
| urls | body | array | Yes | Array of URL objects (max 100 per call) |
| urls[].url | body | string | Yes | Source URL |
| urls[].filename | body | string | Optional | Target filename |
curl -X POST https://api.r2upload.io/v1/external/files/bulk-upload-from-urls \
-H "X-API-Key: r2u_live_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"urls": [
{"url": "https://example.com/img1.jpg", "folder": "/catalog"},
{"url": "https://example.com/img2.jpg", "folder": "/catalog"}
]
}'{
"success": true,
"data": {
"batch_id": "batch_99887766-5544-3322-1100-aabbccddeeff",
"queued_count": 48,
"status": "processing",
"webhook_enabled": true
}
}Add drag-and-drop file uploads to any website with a single line of code. Features automatic chunked multipart uploads, progress tracking, and client-side MIME validation.
<!-- Place anywhere in your HTML <body> -->
<div id="r2u-uploader"></div>
<script
src="https://cdn.r2upload.io/widget/v1.js"
data-api-key="r2u_live_your_public_scoped_key"
data-folder="/user-avatars"
data-max-size="50MB"
data-on-success="handleUploadComplete"
async>
</script>Search, inspect, update metadata, and safely recover or purge files.
/v1/external/filesScope: readReturns a paginated list of your stored files. Supports case-insensitive text search (ILIKE), folder filtering, MIME type filtering, and sorting.
| Field | In | Type | Required | Description |
|---|---|---|---|---|
| page | query | integer | Optional | Page number (default: 1) |
| per_page | query | integer | Optional | Items per page (max 100, default: 25) |
| folder | query | string | Optional | Filter by directory path |
| search | query | string | Optional | Search filename or original name |
curl -G https://api.r2upload.io/v1/external/files \
-H "X-API-Key: r2u_live_your_api_key_here" \
-d "per_page=10" \
-d "folder=/invoices"{
"success": true,
"data": [
{
"uuid": "upl_9f8e7d6c-5b4a-3210-fedc-ba9876543210",
"original_name": "invoice.pdf",
"size_bytes": 1048576,
"mime_type": "application/pdf",
"public_url": "https://cdn.r2upload.io/files/invoice.pdf",
"created_at": "2026-09-04T07:30:00Z"
}
],
"meta": {
"current_page": 1,
"total": 142,
"last_page": 6
}
}/v1/external/files/{uuid}Scope: readRetrieve comprehensive technical metadata for a file including true MIME type, SHA-256 integrity checksum, download counters, and presigned access URL.
| Field | In | Type | Required | Description |
|---|---|---|---|---|
| uuid | path | string | Yes | Unique file UUID |
curl https://api.r2upload.io/v1/external/files/upl_9f8e7d6c-5b4a-3210-fedc-ba9876543210 \
-H "X-API-Key: r2u_live_your_api_key_here"{
"success": true,
"data": {
"uuid": "upl_9f8e7d6c-5b4a-3210-fedc-ba9876543210",
"original_name": "design-system.fig",
"size_bytes": 84920194,
"mime_type": "application/octet-stream",
"sha256_hash": "3f79bb7b435b05321651daefd374cd681b6101e479e7f68f4e9ad16ff426046e",
"storage_connection": { "id": 1, "provider": "cloudflare_r2", "bucket": "prod-media" },
"download_count": 34,
"is_public": true
}
}/v1/external/files/{uuid}Scope: uploadRename a file, move it to a different virtual folder, or toggle public CDN access.
| Field | In | Type | Required | Description |
|---|---|---|---|---|
| uuid | path | string | Yes | File UUID |
| filename | body | string | Optional | New filename |
| folder | body | string | Optional | New destination folder |
| is_public | body | boolean | Optional | Toggle public vs signed URL access |
curl -X PATCH https://api.r2upload.io/v1/external/files/upl_9f8e7d6c-5b4a-3210-fedc-ba9876543210 \
-H "X-API-Key: r2u_live_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{"filename": "updated-invoice.pdf", "folder": "/archive/2026"}'{
"success": true,
"data": {
"uuid": "upl_9f8e7d6c-5b4a-3210-fedc-ba9876543210",
"filename": "updated-invoice.pdf",
"folder": "/archive/2026",
"is_public": false
}
}/v1/external/files/{uuid}Scope: deleteSafely moves a file to the Trash. The file remains recoverable for 30 days before automatic purge.
| Field | In | Type | Required | Description |
|---|---|---|---|---|
| uuid | path | string | Yes | File UUID |
curl -X DELETE https://api.r2upload.io/v1/external/files/upl_9f8e7d6c-5b4a-3210-fedc-ba9876543210 \
-H "X-API-Key: r2u_live_your_api_key_here"{
"success": true,
"message": "File moved to trash. Recoverable for 30 days."
}/v1/external/files/{uuid}/restoreScope: uploadRecovers a soft-deleted file from the Trash and restores it to its original folder.
| Field | In | Type | Required | Description |
|---|---|---|---|---|
| uuid | path | string | Yes | File UUID |
curl -X POST https://api.r2upload.io/v1/external/files/upl_9f8e7d6c-5b4a-3210-fedc-ba9876543210/restore \
-H "X-API-Key: r2u_live_your_api_key_here"{
"success": true,
"message": "File successfully restored from trash."
}/v1/external/files/{uuid}/forceScope: deletePermanently removes the file from underlying S3/R2 storage and purges all database records. This action cannot be reversed.
| Field | In | Type | Required | Description |
|---|---|---|---|---|
| uuid | path | string | Yes | File UUID |
curl -X DELETE https://api.r2upload.io/v1/external/files/upl_9f8e7d6c-5b4a-3210-fedc-ba9876543210/force \
-H "X-API-Key: r2u_live_your_api_key_here"{
"success": true,
"message": "File permanently erased from storage."
}Bring Your Own Bucket: Connect any S3-compatible cloud storage provider.
With BYOB, your storage credentials (Access Key ID and Secret Access Key) are encrypted at rest using AES-256-GCM via Crypt::encryptString(). Files are streamed directly to your bucket without intermediate storage on R2Upload servers.
Because R2Upload abstracts storage using the Amazon S3 REST protocol (AWS Signature Version 4 and path-style addressing), the exact same API request format connects and manages any provider. The API executes an immediate live handshake validation (ListObjectsV2) upon submission, ensuring your credentials and bucket permissions are verified before saving.
| Provider | provider Value | Endpoint Format | Region | Notes |
|---|---|---|---|---|
| Cloudflare R2 | cloudflare_r2 | https://<account_id>.r2.cloudflarestorage.com | auto | $0 egress, global edge |
| Amazon AWS S3 | aws_s3 | https://s3.<region>.amazonaws.com | us-east-1 | Standard AWS SigV4 |
| Wasabi Cloud | wasabi | https://s3.<region>.wasabisys.com | us-east-1 | $0 egress object storage |
| Backblaze B2 | backblaze_b2 | https://s3.<region>.backblazeb2.com | us-west-004 | S3-compatible B2 endpoint |
| DigitalOcean Spaces | digitalocean_spaces | https://<region>.digitaloceanspaces.com | nyc3 | Built-in edge CDN |
| MinIO Self-Hosted | minio | https://minio.yourdomain.com | us-east-1 | Self-hosted private cloud |
| Custom S3 | custom | https://s3.custom-provider.com | custom | Any S3-compatible cluster |
/v1/storage-connectionsScope: accountProgrammatically connect and verify an external bucket from any S3-compatible provider. R2Upload performs an immediate live handshake validation (listObjectsV2) before encrypting your keys with AES-256-GCM. Once connected, all subsequent upload operations stream directly to your bucket.
| Field | In | Type | Required | Description |
|---|---|---|---|---|
| name | body | string | Yes | Descriptive connection name (e.g. "Production R2 Bucket") |
| provider | body | string | Yes | Provider identifier: cloudflare_r2, aws_s3, wasabi, backblaze_b2, digitalocean_spaces, minio, custom |
| endpoint | body | string | Yes | Full S3 API endpoint URL (e.g. https://<account_id>.r2.cloudflarestorage.com) |
| bucket | body | string | Yes | Destination S3 bucket name |
| region | body | string | Yes | Storage region string (e.g. "auto", "us-east-1", "nyc3") |
| access_key | body | string | Yes | S3 Access Key ID (encrypted at rest with AES-256-GCM) |
| secret_key | body | string | Yes | S3 Secret Access Key (encrypted at rest with AES-256-GCM) |
| is_default | body | boolean | Optional | Set this connection as the active default destination for new uploads |
| custom_domain | body | string | Optional | Custom domain or public CDN domain for direct downloads (e.g. cdn.yourdomain.com) |
| path_prefix | body | string | Optional | Optional folder path prefix inside the bucket (e.g. uploads/) |
curl -X POST https://api.r2upload.io/v1/storage-connections \
-H "Authorization: Bearer YOUR_ACCOUNT_TOKEN" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{
"name": "Production Cloudflare R2",
"provider": "cloudflare_r2",
"endpoint": "https://a1b2c3d4e5f6.r2.cloudflarestorage.com",
"bucket": "my-company-assets",
"region": "auto",
"access_key": "your_access_key_id",
"secret_key": "your_secret_access_key",
"is_default": true,
"custom_domain": "cdn.yourdomain.com",
"path_prefix": "uploads/"
}'{
"success": true,
"data": {
"uuid": "sc_8e7d6c5b-4a32-10fe-dcba-987654321098",
"name": "Production Cloudflare R2",
"provider": "cloudflare_r2",
"endpoint": "https://a1b2c3d4e5f6.r2.cloudflarestorage.com",
"bucket": "my-company-assets",
"region": "auto",
"path_prefix": "uploads/",
"custom_domain": "cdn.yourdomain.com",
"is_active": true,
"is_default": true,
"last_connected_at": "2026-09-04T06:30:00Z",
"created_at": "2026-09-04T06:30:00Z"
}
}To enable direct browser multipart uploads to your BYOB bucket, add the following CORS rule in your cloud provider console:
[
{
"AllowedHeaders": ["*"],
"AllowedMethods": ["GET", "PUT", "POST", "DELETE", "HEAD"],
"AllowedOrigins": ["https://*.r2upload.io", "https://yourdomain.com"],
"ExposeHeaders": ["ETag", "x-amz-server-side-encryption"],
"MaxAgeSeconds": 3600
}
]/v1/external/upload-linksScope: uploadGenerate secure, temporary upload URLs for clients, guests, or external partners. Guests upload files directly into your connected bucket without creating an account or needing an API key.
| Field | In | Type | Required | Description |
|---|---|---|---|---|
| name | body | string | Yes | Title for the upload drop |
| folder | body | string | Optional | Target destination folder |
| max_downloads | body | integer | Optional | Maximum allowed uploads (e.g. 5) |
| expires_in_hours | body | integer | Optional | Link validity duration (default: 48) |
| password | body | string | Optional | Optional password protection |
curl -X POST https://api.r2upload.io/v1/external/upload-links \
-H "X-API-Key: r2u_live_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"name": "Tax Documents 2026",
"folder": "/clients/acme",
"max_downloads": 3,
"expires_in_hours": 72
}'{
"success": true,
"data": {
"token": "drop_4f3e2d1c0b9a8f7e",
"upload_url": "https://r2upload.io/upload/drop_4f3e2d1c0b9a8f7e",
"expires_at": "2026-09-06T07:30:00Z",
"max_uploads": 5
}
}Receive instant HTTP POST notifications when uploads complete, fail, or links expire.
| Event Name | Trigger Condition | Key Data Fields |
|---|---|---|
| upload.completed | Direct or remote upload verified and stored | uuid, original_name, size_bytes, public_url |
| upload.failed | Remote URL fetch error or quota exceeded | url, error_code, reason |
| upload.deleted | File moved to trash or permanently erased | uuid, deleted_at, is_permanent |
| upload_link.used | Client completed upload via magic link | token, file_uuid, submitter_ip |
Prevent replay attacks and verify every incoming webhook originated from R2Upload servers.
Each webhook request includes an X-R2U-Signature header. Calculate the HMAC hash of the raw HTTP request body using your webhook secret and verify using a timing-safe equality check:
# Send a test webhook simulation payload using curl:
curl -X POST https://your-server.com/webhooks/r2upload \
-H "Content-Type: application/json" \
-H "X-R2U-Signature: 8f9b4c2e6d1a5b7c8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d" \
-d '{
"event": "upload.completed",
"timestamp": 1725436800,
"data": {
"uuid": "upl_9f8e7d6c-5b4a-3210-fedc-ba9876543210",
"filename": "database-backup.tar.gz",
"size_bytes": 4294967296,
"public_url": "https://cdn.r2upload.io/files/database-backup.tar.gz"
}
}'crypto.timingSafeEqual or hash_equals) to protect against timing side-channel vulnerabilities.Predictable error handling with standard HTTP status codes and machine-readable error envelopes.
{
"success": false,
"error": {
"code": "VALIDATION_ERROR",
"message": "The given data was invalid.",
"details": {
"file": ["The file must be a valid document or image."]
}
}
}| Code | HTTP Status | Description |
|---|---|---|
| UNAUTHORIZED | 401 | Missing or invalid X-API-Key |
| FORBIDDEN | 403 | API key lacks required permission scope |
| VALIDATION_ERROR | 422 | Invalid input parameters or MIME type mismatch |
| QUOTA_EXCEEDED | 402 | Storage quota reached or insufficient wallet balance |
| RATE_LIMITED | 429 | Request rate limit exceeded for plan tier |
| NOT_FOUND | 404 | File or bucket resource does not exist |
Sliding 60-second window enforced per API Key or Bearer Token
All external API endpoints evaluate rate limits on a sliding 60-second window based on your API key. Inspect response headers X-RateLimit-Limit and X-RateLimit-Remaining on every response:
Maximum requests allowed in the sliding 60s window
Number of available requests remaining in the current window
Seconds to wait before retrying when HTTP 429 is received