Listing photos and documents for marketplaces

Dreep stores each listing photo once and serves every size your marketplace needs from URL parameters or saved presets: thumbnails, cards, galleries, zoom and mobile. Sellers upload straight from the browser with presigned URLs, duplicate photos are skipped by content hash, files land in a folder per listing, and ownership documents sit in signed folders only your reviewers can open.

How a listing photo moves through Dreep

The same path works for a flat, a used car, a holiday let or a pair of trainers. Your backend holds the API key; the seller's browser only ever sees a single-use upload URL.

  1. Seller picks 20 photos

    Browser hashes each file (SHA-256)

  2. Your backend presigns

    folder: listings/{id}, contentHash

  3. Parallel PUTs

    4 at a time, duplicates skipped

  4. Confirm

    Asset becomes ready

  5. Presets on delivery

    Thumb, card, gallery, zoom, mobile

Upload URLs are single use and valid for 10 minutes. Until confirm, an asset is pending and excluded from listings.

One original, five listing sizes

Save each slot in your UI as a preset, then request it with ?p=listing_card. Change a preset's recipe later and every listing picks it up without a code change. Free includes 3 presets, Starter 10 and Pro 50.

SlotPreset keyParametersWhere it is used
Thumbnaillisting_thumbwidth=256, height=256, fit=cover, gravity=attention, format=webp, quality=65Search results, seller dashboard
Cardlisting_cardwidth=512, height=384, fit=cover, gravity=attention, format=webp, quality=75Grid cards; add dpr=2 for high-density screens
Gallerylisting_gallerywidth=1440, fit=inside, format=avif, quality=75Main carousel on the listing page
Zoomlisting_zoomwidth=2560, fit=inside, format=webp, quality=85Full-screen zoom on floor plans, car interiors, product detail
Mobilelisting_mobilewidth=828, fit=inside, format=avif, quality=65Detail screen in your iOS or Android app
gravity=attention picks a salient focal point when cropping, so a car is not cut in half by a 4:3 card. fit=inside never crops, which matters for floor plans and documents.

A listing card with srcset

Store the asset URL you get back at confirm time, then build every size from it. The card uses a preset with dpr for density; the gallery uses width descriptors and serves AVIF with a WebP fallback.

// listing.coverUrl is the asset URL saved when the upload was confirmed
const ListingCard = ({ listing }) => (
  <a href={"/listings/" + listing.id}>
    <img
      src={listing.coverUrl + "?p=listing_card"}
      srcSet={
        listing.coverUrl + "?p=listing_card 1x, " +
        listing.coverUrl + "?p=listing_card&dpr=2 2x"
      }
      width={512}
      height={384}
      loading="lazy"
      alt={listing.title}
    />
    <h3>{listing.title}</h3>
    <p>{listing.price}</p>
  </a>
);

export default ListingCard;

Bulk uploads for sellers

Sellers rarely upload one photo. A dealer lists 40 cars on a Monday; a letting agent re-lists the same flat every year with the same photos. The presign flow handles both.

Your backend checks that the seller owns the listing, then asks Dreep for one presigned URL per file with folder set to listings/{listingId}. Missing folders are created on the fly, so there is no setup per listing. The browser PUTs the bytes directly, a few at a time, and your backend confirms each one. The API key stays on your server and Dreep checks that Content-Type and Content-Length match what was presigned.

If the browser sends a SHA-256 contentHash and Dreep already stores identical bytes, the presign response comes back with alreadyExists and no upload URL. The browser skips the PUT and goes straight to confirm, which saves the seller's bandwidth on re-listings and accidental double drops.

// Browser: hash, presign, PUT and confirm, 4 photos at a time.
const sha256 = async (file) => {
  const digest = await crypto.subtle.digest("SHA-256", await file.arrayBuffer());
  return Array.from(new Uint8Array(digest), (b) => b.toString(16).padStart(2, "0")).join("");
};

const uploadPhoto = async ({ listingId, file }) => {
  const presign = await fetch("/api/listings/" + listingId + "/photos/presign", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      filename: file.name,
      contentType: file.type,
      sizeBytes: file.size,
      contentHash: await sha256(file),
    }),
  }).then((res) => res.json());

  if (!presign.alreadyExists) {
    await fetch(presign.uploadUrl, {
      method: "PUT",
      headers: { "Content-Type": file.type },
      body: file,
    });
  }

  return fetch("/api/photos/" + presign.id + "/confirm", { method: "POST" })
    .then((res) => res.json());
};

export const uploadListingPhotos = async ({ listingId, files, concurrency = 4 }) => {
  const queue = Array.from(files);
  const results = [];
  const worker = async () => {
    while (queue.length) {
      results.push(await uploadPhoto({ listingId, file: queue.shift() }));
    }
  };
  await Promise.all(Array.from({ length: concurrency }, worker));
  return results;
};

The backend side: presign and confirm

Two small endpoints on your server. Authorise the seller against the listing before presigning; Dreep validates the file, you decide who may upload where.

import { Dreep } from "dreep";

const dreep = new Dreep({ apiKey: process.env.DREEP_API_KEY });

// POST /api/listings/:listingId/photos/presign
export const presignListingPhoto = async ({ listingId, filename, contentType, sizeBytes, contentHash }) => {
  // Check here that the signed-in seller owns listingId.
  const upload = await dreep.presignUpload({
    filename,
    contentType,
    sizeBytes,
    contentHash,
    folder: "listings/" + listingId,
  });

  return { id: upload.id, uploadUrl: upload.uploadUrl, alreadyExists: upload.alreadyExists };
};

// POST /api/photos/:id/confirm
export const confirmListingPhoto = async ({ id }) => {
  const asset = await dreep.confirmUpload({ id });

  return {
    id: asset.id,
    url: asset.url,
    thumb: dreep.url(asset, { preset: "listing_thumb" }),
    card: dreep.url(asset, { preset: "listing_card" }),
  };
};

Private documents: ID and proof of ownership

Verification files should never share a public folder with listing photos. Create a verification folder once with access set to Signed; every verification/{listingId} subfolder created by later uploads inherits Signed, so a new folder is never silently public. PDFs, Word files and images are accepted; PDFs and Office files are stored as-is.

When a reviewer opens a case, your backend generates a signed URL with a short expiry, for example five minutes. Signing happens locally in the SDK with your signing secret, so it costs no API call. Anyone holding the link can open it until it expires, so treat it as a bearer token and do not log it.

If documents should only ever be viewed by your own staff inside the Dreep dashboard, use a Private folder instead. Private assets cannot be fetched through the delivery URL at all, signed or not.

Worked example: a mid-sized property marketplace

Plan maths for one month, using list prices at the time of writing (October 2026). Change the assumptions to match your own listing volume.

ItemAssumptionEffect
Live listings3,000 listings × 10 photos, average original 1 MB30 GB of originals
Cached variantsAbout 0.5 MB of generated sizes per photo (not every photo is zoomed)Roughly 15 GB; cached variants are stored and count towards storage
Verification documents3,000 listings × 1 PDF of 0.5 MB1.5 GB
New listings400 per month × 10 photos × 5 presetsUp to 20,000 image transformations if every size is requested
PlanPro: 50 GB storage, 50,000 image transformations, 50 presets$5.99/month
Headroom+10 GB storage add-on, recurring$1.99/month
TotalPro plus one storage add-on$7.98/month
Assumes each variant counts once, when it is first generated. Check the real figures with GET /usage during your first month. Starter (10 GB, 10,000 transformations) is too small for this catalogue; Pro yearly is $59. Hitting a limit returns LIMIT_EXCEEDED rather than an overage bill.

What each feature does for a marketplace

Presets per UI slot

Designers change the card size in one place. No code change, no re-upload, no batch job over the whole catalogue.

Content-hash dedupe

Re-listing the same property or car skips the upload entirely, which is the slowest part for sellers on mobile data.

Folder per listing

listings/{id} keeps a listing's photos together, so you can list or delete them as a set when a listing is withdrawn.

Signed verification folders

Documents inherit Signed access from their parent folder and only open through short-lived links your backend issues.

Who this is for

  • Property, vehicle, rental and product marketplaces where sellers upload their own photos.
  • Teams that want one original per photo and every display size from presets.
  • Marketplaces that collect ownership or ID documents and need them out of public reach.
  • Small teams who want a predictable monthly plan rather than per-request metering.

When Dreep is not the right fit

  • You need automatic content moderation or malware scanning of seller uploads. Dreep does neither; add your own step.
  • You need watermarks or text overlays burned into listing photos. Dreep has no overlay feature.
  • You want to import an existing catalogue from S3 or R2. Only Cloudinary import exists today.

Frequently asked questions

Should I resize listing photos on upload or on delivery?

On delivery, for most marketplaces. Keep the original and request sizes through presets, so you can add a new card size next year without re-uploading anything. Transform on upload only when you want to cap storage, because the original is not kept.

How do I stop sellers uploading to someone else's listing?

Authorise on your backend before you call presign. Dreep validates the file type, the 500 MB size limit and that the PUT matches the presigned Content-Type and Content-Length, but it does not know which of your users owns which listing.

What happens to photos when a listing is deleted?

List the assets in listings/{id} and delete them. Deleting an asset removes the original and every cached transform of it, and the space is credited back against your plan.

Can sellers upload iPhone HEIC photos?

Yes. HEIC and HEIF are accepted inputs. Request format=webp or format=avif on delivery and buyers get a format their browser can display.

What happens if we run out of transformations mid-month?

Requests for new variants return LIMIT_EXCEEDED. There is no automatic overage billing: buy a one-time pack (+10,000 for $1.99) or upgrade.

Can listing photos live in our own S3 or R2 bucket?

Yes, on Starter or Pro, with Bring Your Own Bucket. Connect the bucket while the project is still empty; it cannot be changed afterwards.

Related

Set up your listing presets on the free plan

Create a project, save a thumbnail, card and gallery preset, and upload a test listing from your browser. Upgrade when your catalogue outgrows 1 GB.