pexels-stock-images
Fetch real, license-free stock photos AND videos from the Pexels API for storefront/website builds on this multi-tenant content platform — hero/banner images, product photos, blog featured images, content-record images, section backgrounds, team/about photos, and cinematic looping hero/background VIDEOS (search_stock_videos). Use whenever a build needs real imagery instead of broken links, placeholders, or grey boxes (e.g. "the store images are blank", "add real product photos", "find a hero image", "make the hero a video"). Pairs with the shop MCP's upload_image / upload_product_image (and the hero video_url setting) to re-host the asset as a durable, store-owned URL. ALWAYS pick media that match the design's subject + treatment (see shop-core-rules §B).
Real photos beat placeholders. When building or redesigning a store/site, source genuine imagery, then re-host it through the shop MCP so the store owns a stable URL (external CDN links can change; self-hosted survives).
Read
shop-core-rules§C first — it covers image sourcing (Pexels stock is the DEFAULT source for every slot), the "needs a key → STOP and ask" rule, and the "re-host, never hotlink + the server auto-enforces it" rule. This skill is the Pexels path — the default. (For the store's own media / Unsplash / placeholders, skip to "Re-host through the MCP".)
API key — load from the gitignored .env (never hardcode in a committed file)
The key lives in the repo-root .env (gitignored as .env*), as PEXELS_API_KEY=…. Load it per command:
PEXELS_API_KEY=$(grep -E '^PEXELS_API_KEY=' .env | cut -d= -f2-)
If .env has no PEXELS_API_KEY, ask the user for a key (https://www.pexels.com/api/) and add the line to .env. Do not paste the key into a skill/seeder/source file that gets committed.
Search the API
GET https://api.pexels.com/v1/search with header Authorization: <key>:
curl -s -H "Authorization: $PEXELS_API_KEY" \
"https://api.pexels.com/v1/search?query=modern%20living%20room&per_page=5&orientation=landscape&size=large"
Params: query (be specific — "scandinavian oak dining table" beats "table"), per_page (≤80), page, orientation (landscape/portrait/square — match the slot: hero→landscape, PDP→portrait/square, card→square), size (large/medium/small = minimum resolution), color (e.g. #F3EDE3 or a name — useful to match the design palette).
Response shape (per photo):
photos[].id, .width, .height, .avg_color, .photographer, .alt
photos[].src.{ original, large2x, large, medium, small, portrait, landscape, tiny }
Pick the right src size for the slot (don't ship 7000px originals):
- hero / full-bleed banner →
src.large2x(≈1880px) orsrc.original - product / card / gallery →
src.large(≈940px) orsrc.medium - thumbnail / avatar →
src.medium/src.small
Other endpoints: /v1/curated?per_page=… (editor's picks), /v1/photos/{id} (a specific photo). For VIDEO (cinematic hero loops) use the search_stock_videos tool — see "Video heroes" below.
Re-host through the MCP (durable) — don't link the CDN directly
The server auto-enforces self-hosting. Any third-party image URL you pass to a write tool (product image_urls/image attrs, collection image_url, content values, blog featured_image_url + HTML body, section/global theme settings, upsert_theme_file, even graphql_mutation variables) is fetched and re-hosted to /storage/... automatically — and if that source URL is dead / 403 / times out, the whole write fails with an error telling you to pick a reachable image. So you can hand it a Pexels URL directly, but you must hand it a working one. Calling upload_image yourself first is still the clearest path and lets you reuse the hosted URL many times.
Pass the chosen src URL to the shop MCP so it fetches + re-hosts:
- Section / hero / blog / content image →
upload_image { url: "<src.large2x>", folder: "products|blog|content|general", alt: "<photos[].alt>" }→ returns a store-hosted URL. Use that URL in the section setting / page /featured_image_url. - Product image →
upload_product_image { product_id, url: "<src.large>", is_primary: true, alt }(attaches to the gallery). Or passsrcURLs increate_product { image_urls: [...] }.
Re-hosting matters for the storefront listing too: collection/search/related GRID thumbnails render from the product's image/small_image/thumbnail attributes (the listing index), NOT the gallery. You no longer set those by hand — create_product { image_urls } (first url = primary) and upload_product_image { is_primary: true } now auto-populate them from the primary image, so the same self-hosted URL feeds both the PDP gallery AND the grid thumbnail. Pass explicit attributes:{ image, small_image, thumbnail } only to override.
Choose images that match the design (not just any photo)
Per core §B, the design spec defines the imagery subject and treatment (e.g. airy minimalist interiors, warm-toned, lots of negative space). Honor it:
- Match subject to the section (a pet-furniture hero needs a cat/dog with furniture, not a generic couch).
- Match tone/palette — use
color=<spec hex>and eyeballavg_colorso photos sit in the palette. - Consistency: pull a set from the same style/photographer for a cohesive look, not a random grab bag.
- Verify the re-hosted image actually renders in the screenshot-verify gate (core §D). A blank/grey slot = fail; re-pick.
Video heroes (cinematic background loops) — search_stock_videos
Pexels also has video, and a looping hero/section background can look stunning — but it is design-dependent, not every site. Use it ONLY for a cinematic / bold / motion-led look (fashion lookbook, travel, restaurant ambience, agency reel, premium launch); a clean minimal/utility store stays on a still photo.
search_stock_videos { query, per_page, orientation }→ candidates each withurl(a single mp4 rendition ~720–1080p, kept small) +poster(a still frame) +width/height/duration. Same Pexels key as photos. Useorientation:"landscape"for a hero.- Wire it into the hero, don't hand-bake a
<video>: both base heroes (web-heroon content sites,heroon shops) already havevideo_url+video_postersettings. Write the returnedurlintovideo_urlandposterintovideo_poster(viaset_section_settings/ the section template). The hero then renders a full-bleed<video autoplay muted loop playsinline poster>with a dark overlay; leavevideo_urlblank and it falls back to the still image — so it's a drop-in, design-dependent toggle. - Auto re-hosted, same as images (core §C): the mp4 URL you write into
video_urlis fetched and re-hosted to/storage/...on save (videos.pexels.com+.mp4are recognized). Big files count against the org's media quota — that's why the search returns a moderate ~720–1080p rendition, not the 4K original. Upload cap is 64 MiB; if a clip is rejected as too large, pick a shorter/lower-res result. - In a draw-first MOCKUP (shop-design-concept), hotlink the Pexels CDN mp4 directly in the
<video>— the preview plays it (no CSP block) and the real re-host happens at build time. Keep it to one or two clips per page, hero/section-background only — never cards or icons.
Quotas & etiquette
- Free tier ≈ 200 requests/hour, 20,000/month (videos share the same quota). One search per concept; reuse the returned list (don't re-query per image). Response headers
X-Ratelimit-Remaining/X-Ratelimit-Resetshow your budget. - Pexels license = free for commercial use, no attribution required — but the response carries
photographer/photographer_urlif you want to credit (nice for blog/editorial). - Don't hotlink dozens of CDN URLs into the theme long-term; re-host via
upload_image.
Quick end-to-end
PEXELS_API_KEY=$(grep -E '^PEXELS_API_KEY=' .env | cut -d= -f2-)
# 1. search
curl -s -H "Authorization: $PEXELS_API_KEY" \
"https://api.pexels.com/v1/search?query=cat%20tree%20furniture&per_page=10&orientation=square&size=large" \
| python3 -c "import sys,json;[print(p['id'],p['src']['large'],'|',p['alt']) for p in json.load(sys.stdin)['photos']]"
# 2. pick a good one → re-host through MCP upload_product_image { product_id, url: <large>, is_primary:true }
# 3. screenshot-verify the PDP/grid shows it; re-pick if blank or off-brand.
