pexafy-python
Python client for the Pexafy image search API. Search a catalogue of free stock photos by describing what you want, or by handing it an image to match.
pip install pexafy
Getting started
from pexafy import Client
client = Client("your-api-key")
for photo in client.search("a quiet street in the rain", per_page=5):
print(photo.urls.regular, "-", photo.alt_text)
The key comes from your dashboard; the free
tier does not ask for a card. If you would rather not put it in the code, the
client picks up PEXAFY_API_KEY from the environment.
Writing queries
Search runs on meaning, not keywords, so full sentences work better than a pile
of nouns. two people hiking on a ridge at dawn finds what you would expect;
hiking dawn people gives you a worse ranking, because you have thrown away
the relationships between the words.
Filters narrow the result set after the semantic match:
result = client.search(
"an empty office at night",
orientation="landscape",
color_name="blue",
source=["Pexels", "Unsplash"],
per_page=20,
)
print(len(result), "photos in", result.took_ms, "ms")
orientation, source and license_type take several values, as a list or as a comma
separated string. color_name takes one: the API filters on a single colour, and passing
more raises rather than quietly filtering on whichever one arrived last.
Paging
A single call returns one page. iter_search follows the cursor for you and
yields photos until the results run out or you have seen enough:
for photo in client.iter_search("vintage typewriter", max_results=200):
download(photo.urls.large)
Search by image
Pass a path, raw bytes, or an open file:
similar = client.search_by_image("moodboard/reference.jpg", per_page=12)
If you already have a photo id, client.similar(photo_id) is cheaper — the
image does not have to be uploaded and encoded again.
Async
The same surface, awaitable:
import asyncio
from pexafy import AsyncClient
async def main():
async with AsyncClient() as client:
pages = await asyncio.gather(
client.search("desert road"),
client.search("snow covered pines"),
)
for page in pages:
print(len(page))
asyncio.run(main())
Errors
Everything raised inherits from PexafyError. The ones worth catching
separately:
from pexafy import errors
try:
client.search("...")
except errors.RateLimitError as exc:
time.sleep(exc.retry_after or 60)
except errors.AuthenticationError:
... # key is missing, malformed or revoked
except errors.APIError as exc:
print(exc.status_code, exc.code, exc.request_id)
request_id is worth logging. It is the fastest way to get an answer if you
need to ask about a specific call.
Timeouts and 5xx responses are retried twice with backoff, and Retry-After is
honoured when the server sends it. Set max_retries=0 if you would rather
handle that yourself.
Command line
$ pexafy search "morning fog over pine trees" -n 3
0.847 019e0eb8-b028-73cb-9296-dfa70f557bc9 4000x2667 green Pexels https://...
0.812 019e4c9b-3022-7660-b43d-e730b8435f24 6000x4000 green Unsplash https://...
0.798 019e4f39-66d4-7ef2-bc9b-eb5340fd243e 3648x5472 grey Pexels https://...
pexafy photo <id>, pexafy similar <id> and pexafy usage are also there.
Add --json to any of them for the raw response.
Attribution
Photos come from several providers with different licence terms. Every photo
carries an attribution object with a ready made credit line:
photo.attribution.plain # Photo by J. Doe
photo.attribution.html # <a href="...">J. Doe</a>
Check photo.license_type if your use depends on it.
Reference
| Method | What it does |
|---|---|
search(q, **filters) |
one page of results |
iter_search(q, max_results=None, **filters) |
every page, cursor handled |
search_by_image(image, **filters) |
match an image you supply |
get_photo(photo_id, fields=None) |
one photo by id |
similar(photo_id, **filters) |
photos close to an existing one |
colors() sources() orientations() licenses() |
filter values you can use |
suggest_photographers(q) photographer(username) |
photographer lookup |
collections() create_collection(name) collection(id) delete_collection(id) |
saved sets |
add_to_collection(id, photo_id) remove_from_collection(id, photo_id) |
photos in a set |
usage() usage_daily(days=None) usage_monthly(months=None) usage_by_key() |
where you are against your quota |
AsyncClient has every one of these methods, awaitable. Full API documentation is
at docs.pexafy.com; the limits of each plan are at
docs.pexafy.com/plans.
Versioning
The package follows semantic versioning. Within 1.x, an upgrade does not break code written against 1.0: methods and their parameters stay, and new ones are added alongside. Every change is listed in the changelog.
Requirements
Python 3.9 or newer. The only dependency is httpx.
Licence
MIT.
Metadata
Release files for pexafy 1.0.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| pexafy-1.0.0.tar.gz | 19.8 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| pexafy-1.0.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 35.0 kB
Release files / pexafy-1.0.0.tar.gz
| Download URL | pexafy-1.0.0.tar.gz |
|---|---|
| Size | 19.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
1cf89c9330c08cd74d95d19c4bfdbcce36a894b95e1a2939582db7e6826c45b4
|
|
BLAKE2b-256 checksum How to use checksums |
f430a86729d8ffaaa050a24c2cfeeac73979df46808f7b09e3d057dc7e179631
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 18, 2026.
Transparency logRelease files / pexafy-1.0.0-py3-none-any.whl
| Download URL | pexafy-1.0.0-py3-none-any.whl |
|---|---|
| Size | 15.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
d40c05bdeab5a4fd9fae1ca4e6cd754ea2a3d54fbf43de62540328b813410e2d
|
|
BLAKE2b-256 checksum How to use checksums |
f4263bd015930be61ce94827903b05a181bc2f5e754e1baaad7629056fab2d1f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 18, 2026.
Transparency log