# Pexels stock adapter fails on every request — missing `User-Agent` header (Cloudflare 401/403)
Summary
Summary
tools/video/stock_sources/pexels.py sends requests to the Pexels API and CDN with no User-Agent header. The Python default (python-requests/x.y.z) is blocked by Pexels' edge (Cloudflare), so every search returns 401 Unauthorized / 403 Forbidden even with a valid PEXELS_API_KEY. Adding a browser-like UA makes the identical key return 200.
Impact
direct_clip_searchandcorpus_builderreturn zero clips whenever Pexels is a source.- Preflight (
provider_menu_summary()) reports Pexels asconfiguredbecause the env var is present, so the failure only surfaces mid-run at the asset stage. - Blocks the
documentary-montagepipeline (Pexels is its primary source) and any pipeline relying on Pexels stock.
Repro
- Set a valid
PEXELS_API_KEYin.env. - Run any Pexels-backed search, e.g.:
from tools.tool_registry import registry registry.discover() registry._tools["direct_clip_search"].execute({ "output_dir": "/tmp/probe", "queries": [{"query": "rain city street"}], "sources": ["pexels"], }) - Result:
errors: [{"phase": "search", "source": "pexels", "error": "HTTPError: 401 Client Error: Unauthorized for url: https://api.pexels.com/videos/search?..."}]
Minimal confirmation:
import os, urllib.request
key = os.environ["PEXELS_API_KEY"]
req = urllib.request.Request("https://api.pexels.com/videos/search?query=rain&per_page=2",
headers={"Authorization": key})
urllib.request.urlopen(req) # -> HTTPError 403
req.add_header("User-Agent", "Mozilla/5.0 ... Chrome/122.0 Safari/537.36")
urllib.request.urlopen(req) # -> 200 OKCause
_headers() returns only {"Authorization": key}; the download path calls requests.get(...) with no headers at all. Neither sends a UA.
Fix
Send a browser-like User-Agent on both the API calls (via _headers(), used by _search_videos and _search_images) and the file download.
File: tools/video/stock_sources/pexels.py (against cd9f3c1)
@@ -87,7 +87,16 @@ class PexelsSource:
out_path.parent.mkdir(parents=True, exist_ok=True)
with requests.get(
- candidate.download_url, stream=True, timeout=120
+ candidate.download_url,
+ stream=True,
+ timeout=120,
+ headers={
+ "User-Agent": (
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
+ "AppleWebKit/537.36 (KHTML, like Gecko) "
+ "Chrome/122.0 Safari/537.36"
+ )
+ },
) as r:
r.raise_for_status()
with open(out_path, "wb") as f:
@@ -107,7 +116,16 @@ class PexelsSource:
"PEXELS_API_KEY not set. Get a free key at "
"https://www.pexels.com/api/ and add it to .env."
)
- return {"Authorization": key}
+ # Pexels' edge (Cloudflare) 403s the default `python-requests/x`
+ # User-Agent, so send a browser-like UA alongside the API key.
+ return {
+ "Authorization": key,
+ "User-Agent": (
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
+ "AppleWebKit/537.36 (KHTML, like Gecko) "
+ "Chrome/122.0 Safari/537.36"
+ ),
+ }Follow-ups (not in this diff)
- Consider a shared UA constant so other keyless/scraper adapters (
unsplash,wikimedia,mixkit,coverr, …) don't rot the same way. - Preflight could do a live 1-request auth probe for Pexels so a blocked/invalid key shows as
degradedinstead ofconfigured.
Operating system
macOS Tahoe 26.6.2
Pipeline
short documentary
Runtime / renderer
No response
Steps to reproduce
Summary
tools/video/stock_sources/pexels.py sends requests to the Pexels API and CDN with no User-Agent header. The Python default (python-requests/x.y.z) is blocked by Pexels' edge (Cloudflare), so every search returns 401 Unauthorized / 403 Forbidden even with a valid PEXELS_API_KEY. Adding a browser-like UA makes the identical key return 200.
Impact
direct_clip_searchandcorpus_builderreturn zero clips whenever Pexels is a source.- Preflight (
provider_menu_summary()) reports Pexels asconfiguredbecause the env var is present, so the failure only surfaces mid-run at the asset stage. - Blocks the
documentary-montagepipeline (Pexels is its primary source) and any pipeline relying on Pexels stock.
Repro
- Set a valid
PEXELS_API_KEYin.env. - Run any Pexels-backed search, e.g.:
from tools.tool_registry import registry registry.discover() registry._tools["direct_clip_search"].execute({ "output_dir": "/tmp/probe", "queries": [{"query": "rain city street"}], "sources": ["pexels"], }) - Result:
errors: [{"phase": "search", "source": "pexels", "error": "HTTPError: 401 Client Error: Unauthorized for url: https://api.pexels.com/videos/search?..."}]
Minimal confirmation:
import os, urllib.request
key = os.environ["PEXELS_API_KEY"]
req = urllib.request.Request("https://api.pexels.com/videos/search?query=rain&per_page=2",
headers={"Authorization": key})
urllib.request.urlopen(req) # -> HTTPError 403
req.add_header("User-Agent", "Mozilla/5.0 ... Chrome/122.0 Safari/537.36")
urllib.request.urlopen(req) # -> 200 OKCause
_headers() returns only {"Authorization": key}; the download path calls requests.get(...) with no headers at all. Neither sends a UA.
Fix
Send a browser-like User-Agent on both the API calls (via _headers(), used by _search_videos and _search_images) and the file download.
File: tools/video/stock_sources/pexels.py (against cd9f3c1)
@@ -87,7 +87,16 @@ class PexelsSource:
out_path.parent.mkdir(parents=True, exist_ok=True)
with requests.get(
- candidate.download_url, stream=True, timeout=120
+ candidate.download_url,
+ stream=True,
+ timeout=120,
+ headers={
+ "User-Agent": (
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
+ "AppleWebKit/537.36 (KHTML, like Gecko) "
+ "Chrome/122.0 Safari/537.36"
+ )
+ },
) as r:
r.raise_for_status()
with open(out_path, "wb") as f:
@@ -107,7 +116,16 @@ class PexelsSource:
"PEXELS_API_KEY not set. Get a free key at "
"https://www.pexels.com/api/ and add it to .env."
)
- return {"Authorization": key}
+ # Pexels' edge (Cloudflare) 403s the default `python-requests/x`
+ # User-Agent, so send a browser-like UA alongside the API key.
+ return {
+ "Authorization": key,
+ "User-Agent": (
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
+ "AppleWebKit/537.36 (KHTML, like Gecko) "
+ "Chrome/122.0 Safari/537.36"
+ ),
+ }Follow-ups (not in this diff)
- Consider a shared UA constant so other keyless/scraper adapters (
unsplash,wikimedia,mixkit,coverr, …) don't rot the same way. - Preflight could do a live 1-request auth probe for Pexels so a blocked/invalid key shows as
degradedinstead ofconfigured.
Expected behavior
Summary
tools/video/stock_sources/pexels.py sends requests to the Pexels API and CDN with no User-Agent header. The Python default (python-requests/x.y.z) is blocked by Pexels' edge (Cloudflare), so every search returns 401 Unauthorized / 403 Forbidden even with a valid PEXELS_API_KEY. Adding a browser-like UA makes the identical key return 200.
Impact
direct_clip_searchandcorpus_builderreturn zero clips whenever Pexels is a source.- Preflight (
provider_menu_summary()) reports Pexels asconfiguredbecause the env var is present, so the failure only surfaces mid-run at the asset stage. - Blocks the
documentary-montagepipeline (Pexels is its primary source) and any pipeline relying on Pexels stock.
Repro
- Set a valid
PEXELS_API_KEYin.env. - Run any Pexels-backed search, e.g.:
from tools.tool_registry import registry registry.discover() registry._tools["direct_clip_search"].execute({ "output_dir": "/tmp/probe", "queries": [{"query": "rain city street"}], "sources": ["pexels"], }) - Result:
errors: [{"phase": "search", "source": "pexels", "error": "HTTPError: 401 Client Error: Unauthorized for url: https://api.pexels.com/videos/search?..."}]
Minimal confirmation:
import os, urllib.request
key = os.environ["PEXELS_API_KEY"]
req = urllib.request.Request("https://api.pexels.com/videos/search?query=rain&per_page=2",
headers={"Authorization": key})
urllib.request.urlopen(req) # -> HTTPError 403
req.add_header("User-Agent", "Mozilla/5.0 ... Chrome/122.0 Safari/537.36")
urllib.request.urlopen(req) # -> 200 OKCause
_headers() returns only {"Authorization": key}; the download path calls requests.get(...) with no headers at all. Neither sends a UA.
Fix
Send a browser-like User-Agent on both the API calls (via _headers(), used by _search_videos and _search_images) and the file download.
File: tools/video/stock_sources/pexels.py (against cd9f3c1)
@@ -87,7 +87,16 @@ class PexelsSource:
out_path.parent.mkdir(parents=True, exist_ok=True)
with requests.get(
- candidate.download_url, stream=True, timeout=120
+ candidate.download_url,
+ stream=True,
+ timeout=120,
+ headers={
+ "User-Agent": (
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
+ "AppleWebKit/537.36 (KHTML, like Gecko) "
+ "Chrome/122.0 Safari/537.36"
+ )
+ },
) as r:
r.raise_for_status()
with open(out_path, "wb") as f:
@@ -107,7 +116,16 @@ class PexelsSource:
"PEXELS_API_KEY not set. Get a free key at "
"https://www.pexels.com/api/ and add it to .env."
)
- return {"Authorization": key}
+ # Pexels' edge (Cloudflare) 403s the default `python-requests/x`
+ # User-Agent, so send a browser-like UA alongside the API key.
+ return {
+ "Authorization": key,
+ "User-Agent": (
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
+ "AppleWebKit/537.36 (KHTML, like Gecko) "
+ "Chrome/122.0 Safari/537.36"
+ ),
+ }Follow-ups (not in this diff)
- Consider a shared UA constant so other keyless/scraper adapters (
unsplash,wikimedia,mixkit,coverr, …) don't rot the same way. - Preflight could do a live 1-request auth probe for Pexels so a blocked/invalid key shows as
degradedinstead ofconfigured.
Actual behavior
Summary
tools/video/stock_sources/pexels.py sends requests to the Pexels API and CDN with no User-Agent header. The Python default (python-requests/x.y.z) is blocked by Pexels' edge (Cloudflare), so every search returns 401 Unauthorized / 403 Forbidden even with a valid PEXELS_API_KEY. Adding a browser-like UA makes the identical key return 200.
Impact
direct_clip_searchandcorpus_builderreturn zero clips whenever Pexels is a source.- Preflight (
provider_menu_summary()) reports Pexels asconfiguredbecause the env var is present, so the failure only surfaces mid-run at the asset stage. - Blocks the
documentary-montagepipeline (Pexels is its primary source) and any pipeline relying on Pexels stock.
Repro
- Set a valid
PEXELS_API_KEYin.env. - Run any Pexels-backed search, e.g.:
from tools.tool_registry import registry registry.discover() registry._tools["direct_clip_search"].execute({ "output_dir": "/tmp/probe", "queries": [{"query": "rain city street"}], "sources": ["pexels"], }) - Result:
errors: [{"phase": "search", "source": "pexels", "error": "HTTPError: 401 Client Error: Unauthorized for url: https://api.pexels.com/videos/search?..."}]
Minimal confirmation:
import os, urllib.request
key = os.environ["PEXELS_API_KEY"]
req = urllib.request.Request("https://api.pexels.com/videos/search?query=rain&per_page=2",
headers={"Authorization": key})
urllib.request.urlopen(req) # -> HTTPError 403
req.add_header("User-Agent", "Mozilla/5.0 ... Chrome/122.0 Safari/537.36")
urllib.request.urlopen(req) # -> 200 OKCause
_headers() returns only {"Authorization": key}; the download path calls requests.get(...) with no headers at all. Neither sends a UA.
Fix
Send a browser-like User-Agent on both the API calls (via _headers(), used by _search_videos and _search_images) and the file download.
File: tools/video/stock_sources/pexels.py (against cd9f3c1)
@@ -87,7 +87,16 @@ class PexelsSource:
out_path.parent.mkdir(parents=True, exist_ok=True)
with requests.get(
- candidate.download_url, stream=True, timeout=120
+ candidate.download_url,
+ stream=True,
+ timeout=120,
+ headers={
+ "User-Agent": (
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
+ "AppleWebKit/537.36 (KHTML, like Gecko) "
+ "Chrome/122.0 Safari/537.36"
+ )
+ },
) as r:
r.raise_for_status()
with open(out_path, "wb") as f:
@@ -107,7 +116,16 @@ class PexelsSource:
"PEXELS_API_KEY not set. Get a free key at "
"https://www.pexels.com/api/ and add it to .env."
)
- return {"Authorization": key}
+ # Pexels' edge (Cloudflare) 403s the default `python-requests/x`
+ # User-Agent, so send a browser-like UA alongside the API key.
+ return {
+ "Authorization": key,
+ "User-Agent": (
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
+ "AppleWebKit/537.36 (KHTML, like Gecko) "
+ "Chrome/122.0 Safari/537.36"
+ ),
+ }Follow-ups (not in this diff)
- Consider a shared UA constant so other keyless/scraper adapters (
unsplash,wikimedia,mixkit,coverr, …) don't rot the same way. - Preflight could do a live 1-request auth probe for Pexels so a blocked/invalid key shows as
degradedinstead ofconfigured.
Relevant logs or error output
Source: calesthio/OpenMontage