DEV Community

DevFixel
DevFixel

Posted on

Sourcing Free Stock Photos Programmatically Without Accidentally Grabbing Unsplash+ Premium Images

If you've ever scripted stock photo sourcing — pulling images for a content pipeline, a CMS seed script, a batch of location pages, whatever — you've probably hit the same trap I kept hitting: a big chunk of what shows up in a normal Unsplash search is actually Unsplash+, a paid tier, and nothing about the URL or the search result makes that obvious until you're looking at the actual photo page.

Grab enough images automatically and you will download a premium photo you don't have a license for, without realizing it, unless you check for it explicitly.

The problem in practice

A typical workflow looks like: search for something, pick a promising result, construct an images.unsplash.com URL, download it. That last step almost always "succeeds" — you get a valid image file back — regardless of whether the photo is free or Unsplash+. The CDN doesn't refuse the request. Nothing errors. You just end up with a file you don't actually have rights to use, sitting in your project looking exactly like every other downloaded image.

The only reliable tell is on the photo's own page: free photos serve from images.unsplash.com, Unsplash+ photos serve from plus.unsplash.com. If you're constructing download URLs from a search result without visiting the actual page, you can miss this entirely.

Checking programmatically instead of by eye

Unsplash's own site uses an internal API you can query directly for search results, and — usefully — each result includes a plus boolean:

curl -s "https://unsplash.com/napi/search/photos?query=warehouse+logistics&per_page=10" \
  | python3 -c "
import json, sys
data = json.load(sys.stdin)
for r in data['results']:
    print(r['id'], '|', r['alt_description'], '| plus:', r['plus'])
"
Enter fullscreen mode Exit fullscreen mode

That gives you a clean list to eyeball — plus: False is safe, plus: True isn't — before you commit to downloading anything. You can filter it in the same script instead of eyeballing it:

free_results = [r for r in data['results'] if not r['plus']]
Enter fullscreen mode Exit fullscreen mode

For a single candidate photo you already have the ID for, you can check it directly the same way:

curl -s "https://unsplash.com/napi/photos/PHOTO_ID" \
  | python3 -c "import json,sys; d=json.load(sys.stdin); print(d['plus'], d['urls']['raw'])"
Enter fullscreen mode Exit fullscreen mode

This also hands you the actual photo description and location metadata, which is worth checking too — I've had search results come back for a photo whose alt text vaguely matched what I was looking for, but which turned out (per its own location data) to be a completely different city than intended.

Downloading and actually looking at what you got

Once you've confirmed a photo is free, constructing a sized download URL is straightforward:

https://images.unsplash.com/photo-XXXXXXXX?w=1600&auto=format&fit=crop&q=80
Enter fullscreen mode Exit fullscreen mode

The step people skip here is the same one that matters most: open the downloaded file and actually look at it before using it. Alt text and descriptions are written by photographers and occasionally wrong, misleading, or missing context — I've pulled a photo tagged for one city that turned out, on inspection, to have an odd framing issue (an out-of-focus object blocking half the shot) that no amount of metadata would have warned me about. A quick visual check catches that in seconds; skipping it means finding out after it's already live somewhere.

The full loop

  1. Search via the API, filter for plus: false
  2. For your shortlisted candidates, fetch the individual photo record and confirm plus again (belt and suspenders — I've seen search results and individual lookups disagree)
  3. Construct the sized download URL and pull the file
  4. Actually open and look at the image before using it
  5. Only then wire it into whatever you're building

It's a few extra steps over "grab the first result that looks right," but it's the difference between a defensible content-sourcing process and quietly shipping an unlicensed image because the download URL didn't complain.


Built this into a content pipeline for DevFixel. If Unsplash's public API behavior changes or there's a more official way to filter by license, I'd genuinely like to know — this was reverse-engineered from what the site itself calls, not from documented API behavior.

Top comments (0)