Eazip
Eazip.jsPricingSign inStart free
Guides

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_bytes turns 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_in to 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: false keeps one bad object from blocking a 700-photo delivery; failures land in errors so 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.

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.