Text search
q matches title, description, and tags (max 200 characters).
Catalog · GET /images
GET https://epochalstock.com/api/v1/images is the primary discovery endpoint for
design apps. Combine free-text search across title, description, and tags with up to twenty
normalized tag filters, choose how tags combine, pick a stable sort order, and page through
results with opaque cursors.
Without query parameters the endpoint returns a page of recent catalog entries (default sort
newest, default limit 30). Adding q switches the
default sort to relevance so text matches rank highest. Every call counts as one
authenticated request toward the shared 30 requests/minute account limit.
q matches title, description, and tags (max 200 characters).
Up to 20 comma-separated normalized tags with all or any logic.
Pass next_cursor unchanged. Never decode or invent cursors.
Response includes images, total, limit, next_cursor, and filters.
Requires a valid Bearer key. See Authentication if you are still wiring headers.
All parameters are optional query-string fields on GET /images.
| Parameter | Type | Default | Description |
|---|---|---|---|
q |
string | empty | Full-text style search over title, description, and tags. Maximum 200 characters. Excess length returns a validation error. |
tags |
comma-separated strings | empty | Up to 20 normalized tags (for example nature,trees,gold). Prefer tags discovered via GET /tags. |
tag_mode |
all | any |
all |
Whether every listed tag is required (all) or at least one tag is enough (any). |
sort |
relevance | newest | oldest |
relevance when q is set; otherwise newest |
Stable result order for the current filter set. Use with consistent paging. |
limit |
integer 1–100 | 30 |
Number of images returned on this page. Larger pages use fewer requests for bulk browse UIs. |
cursor |
string | empty | Opaque token from the previous response’s next_cursor. Omit on the first page. |
all vs any
When you pass multiple tags, tag_mode controls how they combine. The default
all is best for precise design-tool filters; any widens the set for
exploratory browsing.
tag_mode=allAn image must include every listed tag.
tags=nature,trees&tag_mode=all
Matches only assets tagged with both nature and trees.
Ideal for “narrow the catalog” UI chips where users stack constraints.
tag_mode=anyAn image must include at least one listed tag.
tags=gold,metal,blue&tag_mode=any
Matches assets tagged with gold or metal or blue. Useful for mood boards and multi-theme pickers.
Combine carefully with q. Text search and tag filters apply
together. A tight all tag set plus a specific q can return zero
hits even when each filter alone would succeed—surface “clear filters” in your UI.
| Mode | When to use | Notes |
|---|---|---|
relevance |
Keyword search, natural-language prompts, typeahead results. | Default whenever q is non-empty. Best ranking for text matches. |
newest |
Browse feeds, “latest arrivals”, admin review queues. | Default when q is omitted. Chronological recency. |
oldest |
Archive sweeps, backfill jobs, deterministic full scans. | Pair with cursor paging to walk the catalog from earliest entries. |
Keep sort (and all other filters) identical across pages of the same result set.
Changing sort mid-cursor produces undefined ordering relative to the previous page.
List responses may include next_cursor. When it is present and non-null, pass it
as the next request’s cursor query parameter—byte-for-byte, without decoding,
re-encoding, or constructing your own tokens. When next_cursor is null or
absent, you have reached the end of the filtered set.
Page → next_cursor → next page
q, tags, tag_mode, sort, and limit on every page.invalid_cursor (HTTP 400)—restart from the first page.total for UI counts; do not assume total / limit pages if the catalog is changing.
Search for “golden forest”, require both nature and trees, sort by
newest, and return up to 30 results. Load the API key from the environment only.
curl -sS "https://epochalstock.com/api/v1/images?q=golden+forest&tags=nature,trees&tag_mode=all&sort=newest&limit=30" \
-H "Authorization: Bearer $EPOCHAL_API_KEY" \
-H "Accept: application/json"
const params = new URLSearchParams({
q: "golden forest",
tags: "nature,trees",
tag_mode: "all",
sort: "newest",
limit: "30",
});
const response = await fetch(
`https://epochalstock.com/api/v1/images?${params}`,
{
headers: {
Authorization: `Bearer ${process.env.EPOCHAL_API_KEY}`,
Accept: "application/json",
},
}
);
if (!response.ok) throw new Error(await response.text());
const { data } = await response.json();
console.log(data.total, data.images.length, data.next_cursor);
$query = http_build_query([
'q' => 'golden forest',
'tags' => 'nature,trees',
'tag_mode' => 'all',
'sort' => 'newest',
'limit' => 30,
]);
$ch = curl_init('https://epochalstock.com/api/v1/images?' . $query);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('EPOCHAL_API_KEY'),
'Accept: application/json',
],
CURLOPT_TIMEOUT => 20,
]);
$response = curl_exec($ch);
if (curl_getinfo($ch, CURLINFO_HTTP_CODE) !== 200) {
throw new RuntimeException((string) $response);
}
$data = json_decode($response, true, 512, JSON_THROW_ON_ERROR)['data'];
// $data['images'], $data['total'], $data['next_cursor'], $data['filters']
import os
import requests
r = requests.get(
"https://epochalstock.com/api/v1/images",
params={
"q": "golden forest",
"tags": "nature,trees",
"tag_mode": "all",
"sort": "newest",
"limit": 30,
},
headers={
"Authorization": f"Bearer {os.environ['EPOCHAL_API_KEY']}",
"Accept": "application/json",
},
timeout=20,
)
r.raise_for_status()
data = r.json()["data"]
print(data["total"], len(data["images"]), data.get("next_cursor"))
using System.Net.Http.Headers;
var client = new HttpClient { Timeout = TimeSpan.FromSeconds(20) };
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer",
Environment.GetEnvironmentVariable("EPOCHAL_API_KEY"));
client.DefaultRequestHeaders.Accept.ParseAdd("application/json");
var url =
"https://epochalstock.com/api/v1/images" +
"?q=golden%20forest&tags=nature,trees&tag_mode=all&sort=newest&limit=30";
using var response = await client.GetAsync(url);
response.EnsureSuccessStatusCode();
var json = await response.Content.ReadAsStringAsync();
// Parse data.images, data.total, data.next_cursor
next_cursorMinimal pattern for walking every page of a fixed filter set.
# First page
curl -sS "https://epochalstock.com/api/v1/images?tags=forest&sort=newest&limit=50" \
-H "Authorization: Bearer $EPOCHAL_API_KEY"
# Next page — paste next_cursor exactly
curl -sS "https://epochalstock.com/api/v1/images?tags=forest&sort=newest&limit=50&cursor=CURSOR_FROM_PREVIOUS" \
-H "Authorization: Bearer $EPOCHAL_API_KEY"
async function* listAllForest(apiKey) {
let cursor = "";
for (;;) {
const params = new URLSearchParams({
tags: "forest",
sort: "newest",
limit: "50",
});
if (cursor) params.set("cursor", cursor);
const res = await fetch(
`https://epochalstock.com/api/v1/images?${params}`,
{ headers: { Authorization: `Bearer ${apiKey}`, Accept: "application/json" } }
);
if (!res.ok) throw new Error(await res.text());
const { data } = await res.json();
yield* data.images;
if (!data.next_cursor) break;
cursor = data.next_cursor;
}
}
import os
import requests
def iter_images(**params):
headers = {
"Authorization": f"Bearer {os.environ['EPOCHAL_API_KEY']}",
"Accept": "application/json",
}
cursor = None
while True:
q = dict(params)
if cursor:
q["cursor"] = cursor
r = requests.get(
"https://epochalstock.com/api/v1/images",
params=q,
headers=headers,
timeout=20,
)
r.raise_for_status()
data = r.json()["data"]
yield from data["images"]
cursor = data.get("next_cursor")
if not cursor:
break
for image in iter_images(tags="forest", sort="newest", limit=50):
print(image["id"], image["title"])
Successful responses wrap the page payload in data and include
meta.request_id. Image records expose catalog metadata and safe preview URLs—
original filesystem paths are never returned. The list view omits
description, file_size, and file_extension;
request a single image (GET /images/{image_id}) or POST /images/batch
for the full record.
{
"data": {
"images": [
{
"id": "es_001_example",
"slug": "golden-forest-canopy",
"title": "Golden Forest Canopy",
"collection": "nature",
"width": 6000,
"height": 4000,
"tags": ["nature", "trees", "forest", "gold"],
"created_at": "2025-11-02T14:22:10Z",
"thumbnail_url": "https://epochalstock.com/…/thumb.jpg",
"preview_url": "https://epochalstock.com/…/preview.jpg"
}
],
"total": 128,
"limit": 30,
"next_cursor": "eyJvZmZzZXQiOi4uLn0",
"filters": {
"q": "golden forest",
"tags": ["nature", "trees"],
"tag_mode": "all",
"sort": "newest"
}
},
"meta": {
"request_id": "req_01HZX…"
}
}
imagesArray of image objects for the current page (length ≤ limit).
totalEstimated or exact count of matches for the active filters—use for UI totals.
next_cursorOpaque token for the next page, or null/omitted when finished.
filtersEcho of normalized filters applied server-side—handy for debugging client state.
A live snapshot from GET /images (newest first) when this docs server has a
configured key; otherwise labeled demo placeholders illustrate the card layout.
Live catalog
Thumbnails and watermarked previews are safe to show in admin UIs. Fetch originals only through the two-step download flow.
GET /tags so users only pick normalized tags that exist.q input (200–300 ms) and cap length at 200 characters client-side.tag_mode=all; offer an explicit “match any tag” toggle for broader discovery.limit=50–100 for infinite-scroll grids; smaller limits for typeahead previews.id values, then load full metadata with detail or batch endpoints when the user focuses a card.preview_url / thumbnail_url in the canvas; request originals only on export.X-Request-ID in support logs when a search returns unexpected empties.Next steps: load a single asset with image detail & batch, or explore popular tags via tags & stats.