Creating a photography portfolio comes down to four problems, and Filestack solves each one with a task in a URL. Mixed aspect ratios have to become a tidy grid. Phone photographs arrive rotated. The full size view has to load fast on a phone. And the work usually wants a signature on it.
You upload each photograph once. Every version below is produced from that one file at request time, so nothing is exported, resized in an editor or stored twice.
Key takeaways
- Requesting
fit:cropreturns every photograph at identical dimensions, so portrait and landscape originals form one tidy grid. - Use
rotate=deg:exifon phone photographs, becauserotate=deg:90leaves the orientation tag set and the image sideways. - The watermark task composites a signature handle over a photograph and scales the mark with the width you serve.
- Never request a width larger than the original, since upscaling a 500 pixel photograph to 1200 tripled its file size.
- Face detection runs on the free plan, so
blur_facescan mask a subject in the published version only.
Getting the shoot uploaded
One POST per photograph, or the picker if you want a drag and drop interface:
curl -X POST -F "fileUpload=@dsc_0142.jpg" \
"https://www.filestackapi.com/api/store/S3?key=YOUR_API_KEY"
What comes back is a 20 character handle. Keep those in whatever holds your portfolio metadata, next to the caption and the shoot date. Handles never change, so they survive every redesign, while URLs do not.
Every URL below ends in a handle, shown as HANDLE. Drop your own in at the end and the same URL runs against your photograph, and the images beside each one show what comes back.
A grid that stays a grid
Portfolios look wrong the moment one thumbnail is portrait and the next is landscape. Fixing that in an editor means exporting a second copy of everything. Fixing it in the URL means one task:
https://cdn.filestackcontent.com/resize=width:280,height:280,fit:crop/output=format:webp/HANDLE
fit:crop fills the square and discards the overflow, so a portrait, a landscape and a square all come out at exactly 280 by 280. The four photographs below are different shapes in the archive and identical in the grid:
Measured on 6 August 2026, four photographs at that size weighed between 9,452 and 33,968 bytes each, so a page of thirty thumbnails is under a megabyte. The other fit values are worth knowing before you settle on crop, since clip and max preserve the aspect ratio and leave you with the ragged grid you were trying to avoid, and scale distorts to fill. The full set of options is in the image editing api guide.
When the crop cuts the subject out, reach for crop=dim:[x,y,width,height] on that one photograph and go back to the template for the rest.
The one that catches phone photographs
A photograph shot in portrait on a phone is usually stored landscape, with an EXIF orientation tag telling the viewer to turn it. Some software reads that tag and some does not, which is why a gallery ends up with one image on its side.
There is a task for it, and the parameter is not a number:
https://cdn.filestackcontent.com/rotate=deg:exif/HANDLE
That handle is a phone photograph carrying orientation tag 6, and here it is with the tag baked into the pixels:
rotate=deg:exif, upright in every viewer because the orientation tag is reset to 1
Measured on that file:
| Request | Pixels returned | Orientation tag |
|---|---|---|
| The stored original | 1536 by 1024 | 6 |
rotate=deg:exif |
1024 by 1536 | 1 |
rotate=deg:90 |
1024 by 1536 | 6 |
deg:exif reads the tag, turns the actual pixels, and resets the tag to 1. The image is then upright everywhere, because there is no instruction left for anything to act on.
deg:90 is the trap. It turns the pixels the same way but leaves the tag at 6, so any viewer that honours EXIF turns it a second time and you are back where you started. Put rotate=deg:exif in front of every photograph coming off a phone and the problem disappears for good.
Signing the work
watermark composites one stored file over another, so upload your signature or logo as a transparent PNG once and reference its handle. The first handle in this URL is the signature, the last is the photograph:
https://cdn.filestackcontent.com/watermark=file:SIGNATURE_HANDLE,size:35,position:[bottom,right]/resize=width:800/HANDLE
size is a percentage of the base image width, so the mark scales with whatever size you serve rather than looming over the thumbnails. Percent encode the square brackets around position as %5B and %5D when you send this from curl; a browser does it for you.
Put the watermark before the resize in the chain and it is applied to the full size image, then scaled down with it. That is usually what you want, since the alternative composites a fixed size mark onto an already small image.
The original stays unmarked. Serve the clean version to yourself and the marked version to the public, from the same handle, by changing the URL.
The full size view
The lightbox wants the largest sensible width and nothing more:
https://cdn.filestackcontent.com/resize=width:1600/output=format:webp/HANDLE
Adding the format conversion after the resize is worth roughly a fifth of the bytes. The reasoning behind picking a format at all is in the guide on when to convert to webp.
One rule matters more than the rest here. Never ask for a width larger than the file you uploaded. A 500 pixel wide photograph requested at 1200 pixels came back at 363,715 bytes against the 107,013 byte original, which is a file three times the size and no extra detail in it. Cap the requested width at your smallest original, or serve the untouched handle when you are not sure.
A contact sheet in one URL
collage composites several handles into one image, which is the fastest way to build a proof sheet or a social preview:
https://cdn.filestackcontent.com/collage=files:[HANDLE_B,HANDLE_C,HANDLE_D],margin:10,width:900,height:600,color:white,fit:crop/HANDLE_A
The handle at the end is the first image in the sheet and the rest go in files, in order. zip works the same way, zip/[HANDLE_A,HANDLE_B], which gives a client gallery a download button without any server code behind it.
Faces, when the portfolio is street or event work
Face detection runs on the free plan, which matters when you shoot in public and somebody asks to be taken out. blur_faces, pixelate_faces and crop_faces are tasks like any other, so you can mask a face in the published version and keep the original untouched. There is a walkthrough of building on that in the guide to blur faces with Node and React.
The operations that read and interpret an image, such as tagging, captioning and text recognition, run on the higher plans. Automatic keywords for a searchable archive is a Start feature rather than a free one, so plan that in when your catalogue outgrows manual captions.
What a portfolio costs on the free plan
The quotas are 500 uploads, 1 GB of storage, 1,000 transformations and 1 GB of bandwidth a month, checked on the free plan page on 6 August 2026.
For a portfolio, three of those are comfortable and one needs thought.
- Uploads and storage count originals only. The grid crops, the watermarked versions and the full size views are all generated on request and none of them is stored. Two hundred photographs at 3 MB each is 600 MB of the 1 GB.
- Transformations count distinct URLs, not visits. A grid crop, a watermarked full size view and a lightbox width per photograph is three transformations each, so 200 photographs is 600 of the 1,000.
- Bandwidth is the one to watch. Thumbnails are cheap, at roughly 89,000 requests for a 320 pixel WebP inside 1 GB. Full size views are not, at around 4,000 requests for a 1600 pixel one.
The Filestack CDN caches each variant after the first request, with a default cache-control: public, max-age=2667950 on the response, so repeat viewers cost you bandwidth but not transformations. How the edges hold those files is covered in the guide to global delivery from the edge.
A portfolio that starts getting real traffic hits the bandwidth line first, and Start raises it to 75 GB. Until then the highest value change is capping your lightbox width, which is a single number in a URL you already have, and folding the whole gallery into a considered file delivery workflow.
Joshua is a web developer with over 4 years of experience building responsive, high-performance websites and web applications. Currently working as an AI Automation Specialist, he combines modern web development with automation to create efficient, scalable digital solutions. He shares practical insights on WordPress, web development, and emerging technologies.
