Zip R2 Files into a Download Link via API
Presign any set of Cloudflare R2 objects, POST the list to Eazip, and hand out an expiring ZIP download link. R2 charges no egress, which makes this the cheapest storage-to-ZIP setup available.
This guide turns any set of Cloudflare R2 objects — a prefix, a customer's
folder, or a hand-picked list — into a ZIP download link from your backend.
Presign the objects, POST them as one job, and give the link to whoever
needs it. No bucket CORS configuration, no archive bytes through your
server, no browser tab involved.
R2 has one property that changes the economics of this entire feature: zero egress fees. With S3, every export pays ~$0.09/GB to leave the bucket. With R2, Eazip reads the objects for free, and Eazip adds no bandwidth fee of its own — the whole pipeline from bucket to the user's download has no per-gigabyte bandwidth cost at all.
Building a button the user clicks in your web app instead? See the integration decision guide. Still comparing approaches? Start with 3 Ways to Download Cloudflare R2 Files as a ZIP.
Sign the objects
R2 speaks the S3 API, so the AWS SDK presigns against your account endpoint with an R2 API token (create one in the Cloudflare dashboard under R2 → Manage API Tokens):
import {
S3Client,
ListObjectsV2Command,
GetObjectCommand,
} from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
const r2 = new S3Client({
region: 'auto',
endpoint: `https://${process.env.R2_ACCOUNT_ID}.r2.cloudflarestorage.com`,
credentials: {
accessKeyId: process.env.R2_ACCESS_KEY_ID,
secretAccessKey: process.env.R2_SECRET_ACCESS_KEY,
},
});
export async function listPrefixAsSignedUrls(bucket: string, prefix: string) {
const sources: { url: string; filename: string }[] = [];
let continuationToken: string | undefined;
do {
const page = await r2.send(
new ListObjectsV2Command({
Bucket: bucket,
Prefix: prefix,
ContinuationToken: continuationToken,
}),
);
for (const object of page.Contents ?? []) {
if (!object.Key || object.Key.endsWith('/')) continue;
const url = await getSignedUrl(
r2,
new GetObjectCommand({ Bucket: bucket, Key: object.Key }),
{ expiresIn: 3600 },
);
sources.push({ url, filename: object.Key.slice(prefix.length) });
}
continuationToken = page.NextContinuationToken;
} while (continuationToken);
return sources;
}Listing a prefix is the common shape; files is just an array, so keys
from your own database work the same way. If the bucket is public (an
r2.dev subdomain or a custom domain), you can skip presigning entirely
and pass the public URLs.
Create the job
const files = await listPrefixAsSignedUrls('your-bucket', 'exports/2026-08/');
const response = await fetch('https://api.eazip.io/jobs', {
method: 'POST',
headers: {
'X-API-Key': process.env.EAZIP_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
files,
zip_filename: 'export.zip',
expires_in: 172800,
}),
});
const { job_id } = await response.json();A webhook or a GET /jobs/:id poll returns the expiring download link when
the job completes. The full flow, delivery options, and webhook payloads
live in
Create a ZIP from URLs with One API Call;
this page stays on what is specific to R2.
What an export costs you on R2
This is where R2 stands apart:
- Egress: $0. R2 charges nothing for data leaving the bucket, regardless of destination. A 100 GB export costs the same $0 in bandwidth as a 1 GB one. (The same export from S3 would cost about $9 in egress.)
- Reads are Class B operations at $0.36 per million (after the 10-million monthly free allowance) — a 10,000-object export is about $0.004. Listing is Class A ($4.50/million after 1 million free), still noise at export scale.
- Repeat downloads stay free. In Eazip's default stored mode the archive is prepared once and served from zero-egress storage; Eazip adds no outbound-bandwidth fee. End to end, bucket → archive → five user downloads involves no per-GB bandwidth charge anywhere.
R2-specific limits and gotchas
- Presigned URLs cap at 7 days (SigV4, same as S3). Sign shortly before creating the job and keep lifetimes to what the job needs.
- Infrequent Access storage class objects can be read directly — no Glacier-style restore step exists on R2 — but each read bills a retrieval fee ($0.01/GB), so a large export of IA objects has a real per-GB cost where standard-class R2 has none.
- Workers are not required. A common misconception is that reading R2 from outside Cloudflare needs a Worker; the S3-compatible endpoint works from any backend with an R2 API token. (If you do want to build ZIPs inside a Worker instead, mind the Worker memory limit of 128 MB — that path is compared honestly in the R2 roundup.)
- Jurisdiction-restricted buckets (EU, FedRAMP) use a jurisdiction
endpoint (
{account}.eu.r2.cloudflarestorage.com); presigned URLs made against the wrong endpoint fail. - Archives cap at 50 GB each; set
max_zip_size_bytesto auto-split larger exports, up to 500 GB of total output per job on the largest plan.
FAQ
Does the export flow through my server?
No. Your server sends only the signed URL list. Eazip fetches the objects from R2 directly and serves the download itself.
Do I really pay no bandwidth for a 100 GB export?
For the bucket-to-archive leg, yes — R2 charges no egress and Eazip adds no bandwidth fee. What you do pay: R2 read operations (cents per million), Eazip's plan quota (stored GB-days or streamed GB), and the one-time archive preparation.
Can I use my bucket's public URLs instead of presigning?
Yes. If the bucket is exposed via r2.dev or a custom domain, pass those
URLs directly in files. Presigning is only needed for private buckets.
My export is bigger than 50 GB — can this still work?
Yes. 50 GB is the per-archive cap. Set max_zip_size_bytes and the job
splits into numbered ZIPs, each with its own download link.
Why not just zip inside a Cloudflare Worker?
Workers cap at 128 MB of memory and are billed on CPU time, which makes large in-Worker archives impractical — that's exactly the gap this API path fills. For small archives a Worker can work; the roundup compares both honestly.
Zip S3 Files into a Download Link via API
Presign any set of S3 objects — a prefix, a customer's folder, or a hand-picked list — POST it to Eazip, and hand out an expiring ZIP download link. With the real S3 numbers on egress cost, presign lifetimes, and Glacier gotchas.
Zip Supabase Storage Files into a Download Link via API
Create signed URLs for any set of Supabase Storage objects with one createSignedUrls call, POST the list to Eazip, and hand out an expiring ZIP download link — with the egress-quota math that decides the approach.