Deliver Event Photos to Clients as a ZIP
Turn a finished gallery into one expiring download link — hundreds of full-resolution photos, delivered without building ZIP infrastructure or paying bandwidth per download.
You shot the event, culled and edited, and uploaded the finals to your storage. Now the client wants everything — not a web gallery to click through one photo at a time, but their files, on their disk. This guide turns a finished gallery into a single expiring download link you can put in the delivery email.
The shape of the problem is what makes galleries annoying to deliver:
- Full-resolution photo sets are heavy. A wedding delivery of 800 edited JPEGs at 15–25 MB each is 12–20 GB — past what browser-based ZIP downloads handle reliably, and past what many servers want to proxy.
- Clients download more than once. The couple, both families, the planner. If your delivery pipeline pays bandwidth per download, one gallery gets expensive; if it re-zips per download, it gets slow.
- Delivery should expire. Galleries typically stay downloadable for a window (30–90 days), then age out.
The flow
One job per gallery: list the gallery's files, presign them, create the job, and email the link the webhook hands back.
// 1. Presign the gallery's objects (S3 shown; R2 and Supabase guides below)
const files = await listPrefixAsSignedUrls('deliveries', `galleries/${galleryId}/`);
// 2. One job per gallery
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: 'smith-wedding-2026.zip',
expires_in: 60 * 60 * 24 * 60, // the 60-day delivery window
max_zip_size_bytes: 10 * 1024 ** 3, // split into ~10 GB parts
metadata: { gallery_id: galleryId },
}),
});When the job.completed webhook arrives, metadata.gallery_id tells you
which gallery finished and download_url (or zips[] for split
deliveries) is what goes in the client email. The listing-and-presigning
helper is the platform-specific part — see
S3, R2, or
Supabase Storage for that code and each
platform's cost math.
Decisions that matter for galleries
- Split large deliveries.
max_zip_size_bytesturns a 20 GB wedding into two or three parts with individual links — friendlier to hotel Wi-Fi and to clients' disks than one monolith, and each part supports resume (Range) if a connection drops at 95%. - Match
expires_into your delivery window, not to a default. Plans cap retention (24 hours on free, up to 90 days on the largest plan); a gallery that must outlive the window gets a fresh job when the client asks again — the sources are still in your bucket. - Repeat downloads cost you nothing extra. The archive is prepared once (your storage pays egress once — or zero on R2) and then served from zero-egress storage. Six downloads by the extended family are six downloads of Eazip's copy, not six passes through your bucket.
fail_on_url_error: falsekeeps one bad object from blocking a 700-photo delivery; failures land inerrorsso you can fix and re-run.
When you don't need this
An engagement-shoot delivery of 40 photos fits comfortably in a browser-side ZIP — the client clicks, the browser fetches and packages, no job involved. Eazip.js does that with no account. The API path earns its keep when galleries are multi-gigabyte, downloaded repeatedly, or delivered by email rather than while the client waits on the page.
FAQ
Can the client resume a huge download that fails midway?
Yes — download links support Range requests, so browsers and download managers resume instead of restarting. Splitting the delivery into parts bounds the damage further.
What happens after the link expires?
The archive is deleted and the link stops working. Your originals are untouched; create a new job from the same gallery when a client returns after the window.
Do I need to re-zip when the client downloads again?
No. Stored mode (the default) prepares the archive once; every download after that serves the same prepared ZIP.
Can I brand the delivery?
The ZIP filename is yours (zip_filename), and the link goes out in your
own email. Inside the archive, filenames are whatever you presigned —
{ url, filename } lets you rename on the way in.
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.
Add "Download All" to a Client Portal
Give portal users one button that turns their documents into a ZIP — authorized per client, built as a background job, and delivered as an expiring link that survives the page.