Search results are returned one page at a time. Instead of asking for a page number, you follow a cursor — a token that points to the next page. This keeps paging fast and consistent even while new photos are being added.

The pagination object

Every list response includes a pagination object next to your data:
{
  "success": true,
  "data": [ /* photos for this page */ ],
  "pagination": {
    "per_page": 20,
    "has_more": true,
    "next_cursor": "eyJycyI6Ij... (opaque token)"
  }
}
FieldMeaning
per_pageHow many photos are on each page. You choose this on the first request with per_page (1–100, default 20).
has_moretrue if there is at least one more page after this one.
next_cursorThe token to pass as cursor to get the next page. It is null when has_more is false.

Fetching the next page

  1. Make your search as usual.
  2. Read pagination.next_cursor from the response.
  3. Repeat the same request, adding cursor=<next_cursor>.
  4. Stop when has_more is false (at that point next_cursor is null).
curl "https://api.pexafy.com/api/v1/search/photos?q=a+child+discovering+snow+for+the+first+time&per_page=20" \
  -H "x-api-key: YOUR_API_KEY"
curl "https://api.pexafy.com/api/v1/search/photos?q=a+child+discovering+snow+for+the+first+time&per_page=20&cursor=PASTE_next_cursor_HERE" \
  -H "x-api-key: YOUR_API_KEY"
Here is the same loop in Python:
import requests

BASE = "https://api.pexafy.com/api/v1/search/photos"
headers = {"x-api-key": "YOUR_API_KEY"}
params = {"q": "a child discovering snow for the first time", "per_page": 20}

photos = []
while True:
    r = requests.get(BASE, headers=headers, params=params).json()
    photos.extend(r["data"])
    page = r["pagination"]
    if not page["has_more"]:
        break
    params["cursor"] = page["next_cursor"]   # follow the cursor

print(f"Collected {len(photos)} photos")
Keep your query the same between pages. The cursor already remembers your search text and filters, so you only need to add cursor. Set per_page on the first request; following pages keep that same page size.
Cursors are short-lived — they stay valid for about 5 minutes. If a cursor expires (or you change the query), simply start again from the first page without a cursor.

Which endpoints paginate?

Three search endpoints support cursor pagination:
  • Search photos by text — GET /api/v1/search/photos
  • Search photos by picture — POST /api/v1/search/photos (an upload, or a photo_id)
  • Find similar photos — GET /api/v1/photos/{photo_id}/similar
They all work the same way: read pagination.next_cursor and pass it back as cursor to get the next page, stopping when has_more is false. (On some plans the number of similar photos is capped to a single page — in that case has_more is false from the start.)
For image search, you only upload the image on the first request. To get the next page, send the request again with just cursor=<next_cursor> — no need to re-upload the image. The cursor already remembers your image and filters.Searching with photo_id instead of an upload pages the same way. The per-plan cap on similar photos applies to it too, so on a capped plan you get one page and has_more is false.
The other endpoints return a single response and do not take a cursor: facets, collections and usage return their full list directly.