Cloudflare R2 storage

This guide explains how to use Uploadcare as an upload and processing layer while storing files in your own Cloudflare R2 bucket. This is not a native integration — Uploadcare does not write to R2 directly. Instead, your backend listens for webhook events, downloads the file from Uploadcare, and uploads it to R2.

This pattern suits teams that:

  • Already manage infrastructure on Cloudflare and want to keep files in R2.
  • Need full control over where files are stored and how they are served.
  • Want to apply Uploadcare processing (resize, format conversion, etc.) before committing a file to long-term storage.

Once a file is removed from Uploadcare, CDN delivery and URL API transformations on the original file URL are no longer available. Files can still be processed via Proxy if they remain accessible at a public URL, but this triggers a new ingest rather than continuing from the original file. After the transfer, serve files directly from your R2 bucket or through a CDN in front of it.

Prerequisites

Before you start, make sure you have:

  • An active Uploadcare account with a project and its public and secret API keys.
  • A Cloudflare account with R2 enabled (R2 is not enabled by default — activate it in the Cloudflare dashboard).
  • An R2 bucket created under R2 Object Storage in the Cloudflare dashboard.
  • An R2 API token with Object Read & Write permissions, generated under R2 → Manage R2 API Tokens.
  • Your Cloudflare Account ID, visible in the right sidebar of the Cloudflare dashboard.
  • A publicly reachable backend server capable of receiving HTTPS POST requests from Uploadcare.

How it works

The flow is driven by Uploadcare webhooks:

  1. A file is uploaded via the File Uploader or Upload API.
  2. Uploadcare processes the file and emits a file.uploaded webhook event to your endpoint.
  3. Your backend receives the event, downloads the file from Uploadcare using the URL in the payload, and uploads it to your R2 bucket.
  4. The file is deleted from Uploadcare or left to expire automatically.

Once the transfer is complete, your R2 bucket is the authoritative location for the file.

Step 1: Upload a file

Use the File Uploader for browser-based uploads or the Upload API for server-side or programmatic uploads. At this stage, Uploadcare handles ingestion, validation, and any transformations you have configured.

If you want to minimize storage costs and avoid persisting files in Uploadcare longer than necessary, disable autostore for the upload. Non-stored files expire automatically after a retention period, which gives your backend a generous window to complete the transfer.

To disable autostore on a per-upload basis, pass UPLOADCARE_STORE=0 when using the Upload API, or set store: false in the File Uploader configuration. To disable it project-wide, go to Project settings → Storage in the Dashboard.

Step 2: Receive the webhook event

Configure a webhook in your Uploadcare Dashboard under Project settings → Webhooks. Set the event type to file.uploaded and provide the URL of your backend endpoint.

When a file is uploaded, Uploadcare sends a POST request with a JSON payload:

1{
2 "id": 24877,
3 "file": {
4 "uuid": "eb7830fa-c59d-4ebb-ba2f-29b7c452ce84",
5 "original_file_url": "https://ucarecdn.com/eb7830fa-c59d-4ebb-ba2f-29b7c452ce84/DSCN4715.JPG",
6 "is_stored": false,
7 "mime_type": "image/jpeg",
8 "filename": "DSCN4715.JPG",
9 "size": 7096467
10 }
11}

The host in original_file_url depends on your project’s CDN configuration. It may be ucarecdn.com, a *.ucarecd.net subdomain, or a custom CDN domain. Use the URL as-is — do not hardcode the host in your backend logic.

Your endpoint should:

  1. Validate the request using the webhook signature (see below).
  2. Extract file.uuid, file.original_file_url, file.filename, and file.mime_type from the payload.
  3. Proceed to transfer the file to R2.

Uploadcare signs webhook requests using a secret you configure in the Dashboard. In production, verify the X-Uc-Signature header before processing any payload. See Webhooks for signature verification details.

Step 3: Transfer the file to R2

R2 exposes an S3-compatible API, so you can use the AWS SDK v3 S3 client without any Cloudflare-specific library. Examples use Node.js — refer to the Cloudflare R2 SDK documentation for other languages.

$npm install @aws-sdk/client-s3

Configure the client with your R2 credentials and Cloudflare Account ID. The region must be set to auto — R2 does not use named regions:

1import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3'
2
3const r2 = new S3Client({
4 region: 'auto',
5 endpoint: `https://${process.env.CLOUDFLARE_ACCOUNT_ID}.r2.cloudflarestorage.com`,
6 credentials: {
7 accessKeyId: process.env.R2_ACCESS_KEY_ID,
8 secretAccessKey: process.env.R2_SECRET_ACCESS_KEY,
9 },
10})

Implement the transfer function. The example below fetches the file from Uploadcare and writes it to R2 in one pass:

1async function transferToR2(uuid, fileUrl, filename, mimeType) {
2 const response = await fetch(fileUrl)
3
4 if (!response.ok) {
5 throw new Error(`Failed to fetch file from Uploadcare: ${response.status}`)
6 }
7
8 const buffer = await response.arrayBuffer()
9
10 // Use the UUID in the key to avoid collisions when multiple users
11 // upload files with the same filename.
12 const key = `uploads/${uuid}/${filename}`
13
14 await r2.send(
15 new PutObjectCommand({
16 Bucket: process.env.R2_BUCKET_NAME,
17 Key: key,
18 Body: Buffer.from(buffer),
19 ContentType: mimeType,
20 })
21 )
22
23 return key
24}

Wire it into a minimal Express.js webhook handler:

1import express from 'express'
2
3const app = express()
4app.use(express.json())
5
6app.post('/webhooks/uploadcare', async (req, res) => {
7 const { file } = req.body
8
9 if (!file) {
10 return res.status(400).json({ error: 'Missing file payload' })
11 }
12
13 const { uuid, original_file_url, filename, mime_type } = file
14
15 try {
16 const key = await transferToR2(uuid, original_file_url, filename, mime_type)
17 console.log(`Transferred ${uuid} to R2 at key: ${key}`)
18 // Respond 200 so Uploadcare does not retry this event.
19 res.status(200).json({ ok: true })
20 } catch (err) {
21 console.error(`Transfer failed for ${uuid}:`, err)
22 // A non-2xx response causes Uploadcare to retry the webhook automatically.
23 res.status(500).json({ error: 'Transfer failed' })
24 }
25})
26
27app.listen(3000)

Returning a non-2xx status from your endpoint signals a failure to Uploadcare, which will retry delivery. This means transient errors (network blips, R2 throttling) are retried automatically without any additional queue infrastructure.

Step 4: Handle file lifecycle

After a successful transfer, decide what to do with the original file in Uploadcare.

Option A — Delete immediately via REST API

Remove the file as soon as the transfer completes to minimize the window during which it is accessible via Uploadcare’s CDN and to avoid storage charges on your Uploadcare plan:

1async function deleteFromUploadcare(uuid) {
2 const response = await fetch(`https://api.uploadcare.com/files/${uuid}/`, {
3 method: 'DELETE',
4 headers: {
5 Authorization: `Uploadcare.Simple ${process.env.UPLOADCARE_PUBLIC_KEY}:${process.env.UPLOADCARE_SECRET_KEY}`,
6 Accept: 'application/vnd.uploadcare-v0.7+json',
7 },
8 })
9
10 if (!response.ok) {
11 throw new Error(`Failed to delete file ${uuid}: ${response.status}`)
12 }
13}

Call deleteFromUploadcare(uuid) after a successful transferToR2 call in your webhook handler.

Option B — Rely on auto-expiration

If is_stored is false in the webhook payload, the file was uploaded without autostore and Uploadcare will remove it automatically after a retention period. No explicit deletion call is needed if your backend completes the transfer within that window.

Choose Option A when you want immediate control over file presence and cost. Choose Option B when simpler backend logic is more important and the 24-hour window is acceptable.

Important considerations

Autostore behavior

Uploadcare projects have autostore enabled by default, meaning uploaded files are retained indefinitely unless deleted. To rely on auto-expiration (Option B), either disable autostore at the project level or set UPLOADCARE_STORE=0 via the Upload API. Autostore can be toggled under Project settings → Storage in the Dashboard.

Idempotency

Because Uploadcare retries webhook delivery on failures, your handler may be called more than once for the same file. The transfer function above is safe to run multiple times — PutObjectCommand overwrites the existing R2 object without error. If you are also writing records to a database, make sure those writes are idempotent or guarded by a check on the UUID.

R2 object key strategy

The example uses uploads/{uuid}/{filename} as the R2 key. Prefixing with the UUID prevents collisions between files that share a name and gives you a stable key you can reconstruct from Uploadcare metadata later. Adjust the prefix structure to match your application’s access patterns.

Feature availability after file deletion

Uploadcare’s CDN delivery, URL API image transformations, and adaptive bitrate streaming all require the file to be present in Uploadcare. Once the file is deleted or expires, URLs served via Uploadcare’s CDN for that file will return errors. Plan your delivery strategy — R2 public bucket access, Cloudflare CDN, or pre-signed URLs — before removing files from Uploadcare.

Summary

This flow lets you use Uploadcare as an upload and processing layer while keeping files in Cloudflare R2. Uploadcare handles ingestion and emits a webhook; your backend downloads the file, transfers it to R2, and manages the lifecycle from there. CDN delivery and URL API transformations are only available while the file remains in Uploadcare, so finalize your delivery strategy before deleting or expiring files on the Uploadcare side.

For teams using Amazon S3, see Direct uploads to S3 and Copy files to S3.