Every API here is available over the APIs.io API and to AI agents over MCP.
openapi: 3.2.0
info:
title: Pexafy Search API
x-refined-note:
- x-logo differs across the merged source definitions and was not carried
version: '1.0'
description: 'Operations tagged search across 2 of this provider''s published API definitions: pexafy-openapi-v1.3.0.json, pexafy-openapi.json. Each path carries the servers of the definition it was published in.'
servers:
- url: https://api.pexafy.com/api/v1
description: Production
- url: https://api.pexafy.com
description: Production
tags:
- name: search
description: Finding photos.
paths:
/search/photos:
get:
operationId: searchPhotos
tags:
- search
summary: Search photos by describing them
description: 'Ranking is semantic, not keyword based, so a full sentence outranks a pile of nouns: ''two people hiking on a ridge at dawn'' carries the relationships between the words and ''hiking dawn people'' throws them away.
Filters are applied after the semantic match, which means a narrow filter on a broad query returns fewer results than the same filter on a narrow query. There is no total count and no page number; follow pagination.next_cursor.'
parameters:
- name: q
in: query
description: What you are looking for, in words.
schema:
type: string
minLength: 1
maxLength: 400
required: true
- name: color_name
in: query
description: Restrict to one dominant colour. The API filters on a single colour; passing several is rejected rather than silently using the last one.
schema:
$ref: '#/components/schemas/ColorName'
- name: color_hex
in: query
description: Restrict to photos near one hex colour, used with color_tolerance.
schema:
type: string
pattern: ^#?[0-9a-fA-F]{6}$
- name: color_tolerance
in: query
description: How far from color_hex a photo may sit, in the same units as the colour distance the indexer stores.
schema:
type: integer
minimum: 0
maximum: 100
default: 20
- name: orientation
in: query
description: One or more orientations. Repeat the parameter or pass a comma separated list.
schema:
type: array
items:
$ref: '#/components/schemas/Orientation'
maxItems: 3
- name: source
in: query
description: Restrict to one or more upstream libraries.
schema:
type: array
items:
$ref: '#/components/schemas/Source'
maxItems: 8
- name: license_type
in: query
description: Restrict to one or more licence families.
schema:
type: array
items:
$ref: '#/components/schemas/LicenseType'
maxItems: 8
- name: photographer
in: query
description: Restrict to one photographer, by username.
schema:
type: string
maxLength: 120
- name: per_page
in: query
description: Results per page.
schema:
type: integer
minimum: 1
maximum: 80
default: 20
- name: limit
in: query
description: Hard ceiling on results across all pages of this query.
schema:
type: integer
minimum: 1
maximum: 200
- name: score_threshold
in: query
description: Drop results whose relevance score is below this. Search is semantic, so every query returns something; this is how you decide it returned nothing useful.
schema:
type: number
minimum: 0
maximum: 1
- name: cursor
in: query
description: Opaque cursor from pagination.next_cursor. Cursors are tied to the query that produced them and expire; treat them as single use.
schema:
type: string
maxLength: 512
- name: fields
in: query
description: Comma separated list of Photo fields to return. Trims the payload, does not change ranking.
schema:
type: string
- name: after_date
in: query
description: Only photos uploaded on or after this date.
schema:
type: string
format: date
- name: sort_by
in: query
description: Ordering. Defaults to relevance.
schema:
$ref: '#/components/schemas/SortBy'
responses:
'200':
description: One page of ranked results.
headers:
X-Request-Id:
description: Echoed in error payloads; quote it in a support request.
schema:
type: string
X-RateLimit-Remaining:
description: Calls left in the current window.
schema:
type: integer
content:
application/json:
schema:
$ref: '#/components/schemas/SearchEnvelope'
example:
success: true
data:
- photo_id: 0192f3a1-6c1e-7a3b-9f2d-4c8e1b5a7d20
image_url: https://cdn.pexafy.com/p/0192f3a1/regular.jpg
urls:
thumb: https://cdn.pexafy.com/p/0192f3a1/thumb.jpg
small: https://cdn.pexafy.com/p/0192f3a1/small.jpg
regular: https://cdn.pexafy.com/p/0192f3a1/regular.jpg
large: https://cdn.pexafy.com/p/0192f3a1/large.jpg
full: https://cdn.pexafy.com/p/0192f3a1/full.jpg
width: 5472
height: 3648
orientation: landscape
color_name: blue
color_hex: '#2f4f6f'
photographer_username: a-photographer
source: unsplash
license_type: unsplash
alt_description: an empty office lit only by monitors
relevance_score: 0.71
pagination:
next_cursor: c2NvcmU6MC42MQ
per_page: 20
has_more: true
meta:
request_id: req_01J8Z3
took_ms: 61.4
'400':
description: The request could not be parsed.
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
example:
success: false
error:
code: bad_request
message: The request could not be parsed.
request_id: req_01J8Z3
'401':
description: The API key is missing, malformed or revoked.
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
example:
success: false
error:
code: unauthenticated
message: The API key is missing, malformed or revoked.
request_id: req_01J8Z3
'403':
description: The key is valid but lacks the scope for this call.
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
example:
success: false
error:
code: forbidden
message: The key is valid but lacks the scope for this call.
request_id: req_01J8Z3
'422':
description: A parameter was rejected before the search ran.
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
example:
success: false
error:
code: validation_failed
message: A parameter was rejected before the search ran.
request_id: req_01J8Z3
'429':
description: Too many requests, or the plan quota is exhausted.
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
example:
success: false
error:
code: rate_limited
message: Too many requests, or the plan quota is exhausted.
request_id: req_01J8Z3
'500':
description: Something broke on our side.
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
example:
success: false
error:
code: internal_error
message: Something broke on our side.
request_id: req_01J8Z3
security:
- apiKey: []
servers:
- url: https://api.pexafy.com/api/v1
description: Production
/search/photos/by-image:
post:
operationId: searchPhotosByImage
tags:
- search
summary: Search photos by handing over an image
description: 'The image is embedded and matched against the same vector space as a text query, so the two are interchangeable as far as ranking is concerned. The image is not stored.
Send either a file or a URL, not both. Anything over 10 MB is rejected before it reaches the encoder.'
requestBody:
required: true
content:
multipart/form-data:
schema:
type: object
properties:
image:
type: string
format: binary
image_url:
type: string
format: uri
per_page:
type: integer
minimum: 1
maximum: 80
default: 20
orientation:
type: array
items:
$ref: '#/components/schemas/Orientation'
source:
type: array
items:
$ref: '#/components/schemas/Source'
score_threshold:
type: number
minimum: 0
maximum: 1
responses:
'200':
description: One page of results ranked against the image.
headers:
X-Request-Id:
description: Echoed in error payloads; quote it in a support request.
schema:
type: string
X-RateLimit-Remaining:
description: Calls left in the current window.
schema:
type: integer
content:
application/json:
schema:
$ref: '#/components/schemas/SearchEnvelope'
example:
success: true
data:
- photo_id: 0192f3a1-6c1e-7a3b-9f2d-4c8e1b5a7d20
image_url: https://cdn.pexafy.com/p/0192f3a1/regular.jpg
urls:
thumb: https://cdn.pexafy.com/p/0192f3a1/thumb.jpg
small: https://cdn.pexafy.com/p/0192f3a1/small.jpg
regular: https://cdn.pexafy.com/p/0192f3a1/regular.jpg
large: https://cdn.pexafy.com/p/0192f3a1/large.jpg
full: https://cdn.pexafy.com/p/0192f3a1/full.jpg
width: 5472
height: 3648
orientation: landscape
color_name: blue
color_hex: '#2f4f6f'
photographer_username: a-photographer
source: unsplash
license_type: unsplash
alt_description: an empty office lit only by monitors
relevance_score: 0.71
pagination:
next_cursor: c2NvcmU6MC42MQ
per_page: 20
has_more: true
meta:
request_id: req_01J8Z3
took_ms: 61.4
'400':
description: The request could not be parsed.
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
example:
success: false
error:
code: bad_request
message: The request could not be parsed.
request_id: req_01J8Z3
'401':
description: The API key is missing, malformed or revoked.
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
example:
success: false
error:
code: unauthenticated
message: The API key is missing, malformed or revoked.
request_id: req_01J8Z3
'403':
description: The key is valid but lacks the scope for this call.
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
example:
success: false
error:
code: forbidden
message: The key is valid but lacks the scope for this call.
request_id: req_01J8Z3
'422':
description: A parameter was rejected before the search ran.
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
example:
success: false
error:
code: validation_failed
message: A parameter was rejected before the search ran.
request_id: req_01J8Z3
'429':
description: Too many requests, or the plan quota is exhausted.
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
example:
success: false
error:
code: rate_limited
message: Too many requests, or the plan quota is exhausted.
request_id: req_01J8Z3
'500':
description: Something broke on our side.
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
example:
success: false
error:
code: internal_error
message: Something broke on our side.
request_id: req_01J8Z3
security:
- apiKey: []
servers:
- url: https://api.pexafy.com/api/v1
description: Production
/api/v1/search/photos:
get:
tags:
- search
summary: Search photos by text
description: 'Search the photo library with **natural language**, not keywords. Pexafy understands the *meaning, mood and story* of a scene, so full descriptive sentences return far better matches than a couple of tags. For example:
- `an old man sitting at a café table he has visited every morning for thirty years`
- `two people sharing a bench in comfortable silence`
- `the last sunlight of the day hitting a dusty windowsill`
- `a child discovering snow for the first time`
Short queries like `mountain sunset` still work, but the more you describe the scene, light and emotion you have in mind, the closer the results.
You can combine your text query with filters (color, orientation, source, license, photographer, date) to narrow the results. **At least one criterion is required**: either a text query (`q`) or one of the filters.
### What you get back
A list of photos in `data`. The most useful fields on each photo are:
| Field | Description |
| --- | --- |
| `photo_id` | Unique Pexafy identifier — use it with the **Photos** and **Collections** endpoints. |
| `urls` | Ready-to-use image links in five sizes: `thumb`, `small`, `regular`, `large`, `full`. |
| `description` / `alt_description` | A short caption and accessibility-friendly text. |
| `color_name`, `color_hex`, `orientation` | Visual attributes of the photo. |
| `source`, `license_type` | Where the photo comes from and how it may be used. |
| `photographer_username`, `attribution` | Credit information. `attribution.html` and `attribution.plain` are ready to display. |
| `relevance_score` | How closely the photo matches your query, from `0` to `1` (higher is better). |
Results are returned page by page — see the [Pagination](/pagination) guide to fetch the next page.'
operationId: search_photos_api_v1_search_photos_get
parameters:
- name: q
in: query
required: false
schema:
anyOf:
- type: string
maxLength: 500
- type: 'null'
title: Q
description: Your search query in plain language — full descriptive sentences work best (e.g. `two people sharing a bench in comfortable silence`), though short queries like `mountain sunset` are fine too. Up to 500 characters. Optional if you provide at least one filter instead.
- name: color_name
in: query
required: false
schema:
anyOf:
- type: string
- type: 'null'
title: Color Name
description: Keep only photos whose dominant color matches this name (e.g. `blue`, `red`, `teal`). Call `GET /api/v1/facets/colors` (see the **Facets** section) to get the list of available color names. Cannot be combined with `color_hex`.
- name: color_hex
in: query
required: false
schema:
anyOf:
- type: string
- type: 'null'
title: Color Hex
description: Keep only photos close to this hex color (e.g. `#1E90FF`). Cannot be combined with `color_name`. Use `color_tolerance` to widen or tighten the match.
- name: color_tolerance
in: query
required: false
schema:
anyOf:
- type: integer
maximum: 255
minimum: 0
- type: 'null'
description: 'How far a photo''s colour may sit from the one you asked for, from `0` (strict) to `255` (very loose). Defaults to `20`. Applies to **both** `color_hex` and `color_name`: for a hex it is the radius around that exact shade, for a name it widens or narrows the colour family. The published spec used to say it applied to `color_hex` only, which was never true of the code.'
title: Color Tolerance
description: 'How far a photo''s colour may sit from the one you asked for, from `0` (strict) to `255` (very loose). Defaults to `20`. Applies to **both** `color_hex` and `color_name`: for a hex it is the radius around that exact shade, for a name it widens or narrows the colour family.'
- name: orientation
in: query
required: false
schema:
anyOf:
- type: array
items:
type: string
- type: 'null'
title: Orientation
description: 'Keep only photos with these shapes: `landscape`, `portrait` and/or `square`. Repeat the parameter to pass several. See `GET /api/v1/facets/orientations`.'
- name: source
in: query
required: false
schema:
anyOf:
- type: array
items:
type: string
- type: 'null'
title: Source
description: Keep only photos from these providers (e.g. `Pexels`, `Unsplash`, `Pixabay`). Repeat the parameter to pass several. Call `GET /api/v1/facets/sources` to list every available source.
- name: license_type
in: query
required: false
schema:
anyOf:
- type: array
items:
type: string
- type: 'null'
title: License Type
description: Keep only photos with these license types (e.g. `free`). Call `GET /api/v1/facets/licenses` to list the available license types.
- name: per_page
in: query
required: false
schema:
anyOf:
- type: integer
maximum: 100
minimum: 1
- type: 'null'
title: Per Page
description: Number of photos per page, from 1 to 100. Defaults to 20.
- name: limit
in: query
required: false
schema:
anyOf:
- type: integer
maximum: 100
minimum: 1
- type: 'null'
description: Maximum TOTAL number of results to return across all pages — NOT the page size (use `per_page` for that). e.g. limit=4 with per_page=2 returns two pages of 2 results, then stops. Omit for no cap.
title: Limit
description: Maximum TOTAL number of results to return across all pages — NOT the page size (use `per_page` for that). e.g. limit=4 with per_page=2 returns two pages of 2 results, then stops. Omit for no cap.
- name: score_threshold
in: query
required: false
schema:
anyOf:
- type: number
maximum: 1.0
minimum: 0.0
- type: 'null'
title: Score Threshold
description: Minimum relevance a photo must reach to appear, from `0` to `1`. Raise it (e.g. `0.3`) to keep only strong matches and drop loosely related photos; leave it unset to get the widest set of results.
- name: cursor
in: query
required: false
schema:
anyOf:
- type: string
- type: 'null'
title: Cursor
description: Token used to fetch the next page. Take the `pagination.next_cursor` value from a previous response and pass it back here. See the [Pagination](/pagination) guide.
- name: fields
in: query
required: false
schema:
anyOf:
- type: string
- type: 'null'
title: Fields
description: Comma-separated list to return only the fields you need (e.g. `photo_id,urls,attribution`), which makes responses smaller. Omit to get every field your plan allows.
- name: after_date
in: query
required: false
schema:
anyOf:
- type: string
- type: 'null'
description: Only return photos published on or after this date, formatted `YYYY-MM-DD`.
title: After Date
description: Only return photos published on or after this date, formatted `YYYY-MM-DD`.
- name: sort_by
in: query
required: false
schema:
anyOf:
- enum:
- relevance
- newest
type: string
- type: 'null'
description: 'Order of the results: `relevance` (default — best match first) or `newest` (most recently added first).'
title: Sort By
description: 'Order of the results: `relevance` (default — best match first) or `newest` (most recently added first).'
- name: photographer
in: query
required: false
schema:
anyOf:
- type: string
- type: 'null'
description: Only return photos from this photographer's username. Use `GET /api/v1/facets/photographers/suggest` to find usernames.
title: Photographer
description: Only return photos from this photographer's username. Use `GET /api/v1/facets/photographers/suggest` to find usernames.
responses:
'200':
description: Successful Response
content:
application/json:
schema:
$ref: '#/components/schemas/PhotoListResponse'
'422':
description: Validation Error
content:
application/json:
schema:
$ref: '#/components/schemas/HTTPValidationError'
security:
- ApiKeyAuth: []
post:
tags:
- search
summary: Search photos by image
description: 'Upload an image and get visually similar photos in return — same subject, composition, colors or style. Ideal for finding alternatives to a photo you already have.
You can also pass a text query (`q`) together with the image to steer the results: the search then balances *what the image looks like* with *what your words describe*. Use `text_alpha` to control that balance.
**Accepted formats:** JPEG, PNG, WebP or AVIF, up to 10 MB.
The same filters as text search are available, and the response has the exact same shape — see **Search photos by text** for the list of fields you get back.'
operationId: search_photos_by_image_api_v1_search_photos_post
parameters:
- name: q
in: query
required: false
schema:
anyOf:
- type: string
maxLength: 500
- type: 'null'
description: Optional words, in plain language, to steer the visual match (e.g. `the same scene but at golden hour` or `warmer and more cheerful`). When set, results match both the picture and your text.
title: Q
description: Optional words, in plain language, to steer the visual match (e.g. `the same scene but at golden hour` or `warmer and more cheerful`). When set, results match both the picture and your text.
- name: text_alpha
in: query
required: false
schema:
anyOf:
- type: number
maximum: 10.0
minimum: 0.0
- type: 'null'
description: Balance between your text and the image when both are provided, from `0` to `10`. `0` ignores the text (pure visual search), `1.7` (the default) is balanced, and higher values give your words more weight. Has no effect without `q`.
title: Text Alpha
description: Balance between your text and the image when both are provided, from `0` to `10`. `0` ignores the text (pure visual search), `1.7` (the default) is balanced, and higher values give your words more weight. Has no effect without `q`.
- name: color_name
in: query
required: false
schema:
anyOf:
- type: string
- type: 'null'
title: Color Name
description: Keep only photos whose dominant color matches this name (e.g. `blue`, `red`, `teal`). Call `GET /api/v1/facets/colors` (see the **Facets** section) to get the list of available color names. Cannot be combined with `color_hex`.
- name: color_hex
in: query
required: false
schema:
anyOf:
- type: string
- type: 'null'
title: Color Hex
description: Keep only photos close to this hex color (e.g. `#1E90FF`). Cannot be combined with `color_name`. Use `color_tolerance` to widen or tighten the match.
- name: color_tolerance
in: query
required: false
schema:
anyOf:
- type: integer
maximum: 255
minimum: 0
- type: 'null'
description: 'How far a photo''s colour may sit from the one you asked for, from `0` (strict) to `255` (very loose). Defaults to `20`. Applies to **both** `color_hex` and `color_name`: for a hex it is the radius around that exact shade, for a name it widens or narrows the colour family. The published spec used to say it applied to `color_hex` only, which was never true of the code.'
title: Color Tolerance
description: 'How far a photo''s colour may sit from the one you asked for, from `0` (strict) to `255` (very loose). Defaults to `20`. Applies to **both** `color_hex` and `color_name`: for a hex it is the radius around that exact shade, for a name it widens or narrows the colour family.'
- name: orientation
in: query
required: false
schema:
anyOf:
- type: array
items:
type: string
- type: 'null'
title: Orientation
description: 'Keep only photos with these shapes: `landscape`, `portrait` and/or `square`. Repeat the parameter to pass several. See `GET /api/v1/facets/orientations`.'
- name: source
in: query
required: false
schema:
anyOf:
- type: array
items:
type: string
- type: 'null'
title: Source
description: Keep only photos from these providers (e.g. `Pexels`, `Unsplash`, `Pixabay`). Repeat the parameter to pass several. Call `GET /api/v1/facets/sources` to list every available source.
- name: license_type
in: query
required: false
schema:
anyOf:
- type: array
items:
type: string
- type: 'null'
title: License Type
description: Keep only photos with these license types (e.g. `free`). Call `GET /api/v1/facets/licenses` to list the available license types.
- name: per_page
in: query
required: false
schema:
type: integer
maximum: 100
minimum: 1
default: 20
title: Per Page
description: Number of photos per page, from 1 to 100. Defaults to 20.
- name: limit
in: query
required: false
schema:
anyOf:
- type: integer
maximum: 100
minimum: 1
- type: 'null'
description: Maximum TOTAL number of results to return across all pages — NOT the page size (use `per_page` for that). Omit for no cap.
title: Limit
description: Maximum TOTAL number of results to return across all pages — NOT the page size (use `per_page` for that). Omit for no cap.
- name: score_threshold
in: query
required: false
schema:
anyOf:
- type: number
maximum: 1.0
minimum: 0.0
- type: 'null'
title: Score Threshold
description: Minimum relevance a photo must reach to appear, from `0` to `1`. Raise it (e.g. `0.3`) to keep only strong matches and drop loosely related photos; leave it unset to get the widest set of results.
- name: after_date
in: query
required: false
schema:
anyOf:
- type: string
- type: 'null'
description: Only return photos published on or after this date, formatted `YYYY-MM-DD`.
title: After Date
description: Only return photos published on or after this date, formatted `YYYY-MM-DD`.
- name: sort_by
in: query
required: false
schema:
anyOf:
- enum:
- relevance
- newest
type: string
- type: 'null'
description: 'Order of the results: `relevance` (default — best match first) or `newest` (most recently added first).'
title: Sort By
description: 'Order of the results: `relevance` (default — best match first) or `newest` (most recently added first).'
- name: photographer
in: query
required: false
schema:
anyOf:
- type: string
- type: 'null'
description: Only return photos from this photographer's username. Use `GET /api/v1/facets/photographers/suggest` to find usernames.
title: Photographer
description: Only return photos from this photographer's username. Use `GET /api/v1/facets/photographers/suggest` to find usernames.
- name: cursor
in: query
required: false
schema:
anyOf:
- type: string
- type: 'null'
description: Token to fetch the next page. Take `pagination.next_cursor` from a previous response and pass it back here — no need to re-upload the image. See the Pagination guide.
title: Cursor
description: Token to fetch the next page. Take `pagination.next_cursor` from a previous response and pass it back here — no need to re-upload the image. See the Pagination guide.
requestBody:
content:
multipart/form-data:
schema:
$ref: '#/components/schemas/Body_search_photos_by_image_api_v1_search_photos_post'
required: true
responses:
'200':
# --- truncated at 32 KB (44 KB total) ---
# Full source: https://raw.githubusercontent.com/api-evangelist/pexafy/refs/heads/main/openapi/pexafy-search-api-openapi.yml