Catalog · GET /tags · GET /stats
Tags & catalog statistics
Seed filter chips and browse UIs from
GET https://epochalstock.com/api/v1/tags
(each item has tag + image_count), and show catalog health with
GET https://epochalstock.com/api/v1/stats
(total_images, total_tags, catalog_built_at).
Live catalog snapshot
Figures below are loaded server-side from GET /stats and GET /tags
when this docs site has a configured API key. Without a key, labeled demo data keeps the page usable locally.
Live from API
Tag frequency
Top tags sorted by image_count (desc), limit 50
.
Bar widths are relative to the highest count on this page.
Top tags by image count
Tag cloud
Tag list
Same payload as the chart—normalized name plus exact image_count per tag.
| Tag | image_count |
|---|---|
nature |
2,222 |
landscape |
1,813 |
outdoor |
1,580 |
light |
1,515 |
beauty |
1,472 |
sunlight |
1,322 |
evening |
1,314 |
garden |
1,205 |
close up |
1,146 |
environment |
1,116 |
growth |
1,111 |
flora |
1,044 |
spring |
1,037 |
sunset |
971 |
bloom |
907 |
natural |
904 |
soft light |
875 |
summer |
870 |
scenery |
867 |
field |
832 |
peaceful |
813 |
blossom |
792 |
petals |
789 |
warmth |
787 |
joy |
761 |
tranquility |
754 |
lifestyle |
752 |
bright |
751 |
flowers |
742 |
atmosphere |
727 |
colorful |
723 |
plant |
723 |
connection |
681 |
wildflower |
669 |
sky |
663 |
meadow |
660 |
scene |
653 |
expression |
640 |
happiness |
617 |
petal |
602 |
outdoors |
596 |
color |
585 |
flower |
584 |
blooming |
582 |
floral |
578 |
faith |
550 |
seasonal |
550 |
grass |
549 |
horizon |
547 |
portrait |
534 |
GET /tags
Lists normalized tags and how many images use each tag. Prefer this endpoint to populate filter UIs so users only choose tags that exist in the catalog.
| Parameter | Type | Default | Description |
|---|---|---|---|
q |
string | empty | Optional filter over tag names (for typeahead / partial match UIs). |
sort |
name | count |
count |
Order by tag name or by image_count. |
order |
asc | desc |
desc |
Sort direction. Popular tags first is typically sort=count&order=desc. |
limit |
integer 1–100 | 50 |
Page size for the tag list. |
cursor |
integer ≥ 0 | empty / 0 | Pagination cursor from the previous page. Pass through unchanged. |
Example:
https://epochalstock.com/api/v1/tags?q=forest&sort=count&order=desc&limit=50
Each list item contains tag and image_count. Use those counts in chips
(forest 980)
so designers see how selective a filter will be.
GET /tags/{tag}
Returns one normalized tag, its exact image count, and the corresponding filtered image listing URL. Path-encode the tag (spaces and special characters) with your language’s URL encoder.
| Item | Value |
|---|---|
| Method / path | GET /tags/{url_encoded_tag} |
| Example | https://epochalstock.com/api/v1/tags/forest |
| Success | HTTP 200 · tag detail under data |
| Not found | HTTP 404 · tag_not_found |
- Use list + counts for pickers; use detail when opening a dedicated tag page.
- Always URL-encode the path segment—do not concatenate raw user input into the path.
- To list images for a tag, call GET /images with
tags=….
GET /stats
Lightweight catalog totals for dashboards, admin footers, and “index freshness” indicators. No query parameters.
| Field | Type | Description |
|---|---|---|
total_images |
integer | Number of images in the current catalog index. |
total_tags |
integer | Number of normalized tags. |
catalog_built_at |
string (timestamp) | When the catalog index was last built—use to show freshness. |
Cache GET /stats on your backend (tens of seconds to a few minutes). Totals change
only when the catalog is rebuilt, so frequent polling wastes the shared 30 req/min budget.
Code examples · tags
List tags by descending image count, optionally filtered by q.
curl -sS "https://epochalstock.com/api/v1/tags?q=forest&sort=count&order=desc&limit=50" \
-H "Authorization: Bearer $EPOCHAL_API_KEY" \
-H "Accept: application/json"
# Single tag detail (URL-encode the path segment)
curl -sS "https://epochalstock.com/api/v1/tags/forest" \
-H "Authorization: Bearer $EPOCHAL_API_KEY" \
-H "Accept: application/json"
const params = new URLSearchParams({
q: "forest",
sort: "count",
order: "desc",
limit: "50",
});
const listRes = await fetch(
`https://epochalstock.com/api/v1/tags?${params}`,
{
headers: {
Authorization: `Bearer ${process.env.EPOCHAL_API_KEY}`,
Accept: "application/json",
},
}
);
if (!listRes.ok) throw new Error(await listRes.text());
const { data: list } = await listRes.json();
for (const row of list.tags ?? []) {
console.log(row.tag, row.image_count);
}
const tag = "forest";
const oneRes = await fetch(
`https://epochalstock.com/api/v1/tags/${encodeURIComponent(tag)}`,
{
headers: {
Authorization: `Bearer ${process.env.EPOCHAL_API_KEY}`,
Accept: "application/json",
},
}
);
if (!oneRes.ok) throw new Error(await oneRes.text());
console.log(await oneRes.json());
$query = http_build_query([
'q' => 'forest',
'sort' => 'count',
'order' => 'desc',
'limit' => 50,
]);
$ch = curl_init('https://epochalstock.com/api/v1/tags?' . $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'];
// Each row: tag, image_count
// Detail
$tag = rawurlencode('forest');
$ch = curl_init('https://epochalstock.com/api/v1/tags/' . $tag);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('EPOCHAL_API_KEY'),
'Accept: application/json',
],
CURLOPT_TIMEOUT => 20,
]);
$detail = curl_exec($ch);
import os
import requests
headers = {
"Authorization": f"Bearer {os.environ['EPOCHAL_API_KEY']}",
"Accept": "application/json",
}
r = requests.get(
"https://epochalstock.com/api/v1/tags",
params={"q": "forest", "sort": "count", "order": "desc", "limit": 50},
headers=headers,
timeout=20,
)
r.raise_for_status()
payload = r.json()["data"]
for row in payload.get("tags") or []:
print(row["tag"], row["image_count"])
one = requests.get(
"https://epochalstock.com/api/v1/tags/forest",
headers=headers,
timeout=20,
)
one.raise_for_status()
print(one.json()["data"])
package main
import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"os"
"time"
)
func main() {
q := url.Values{}
q.Set("q", "forest")
q.Set("sort", "count")
q.Set("order", "desc")
q.Set("limit", "50")
req, _ := http.NewRequest(http.MethodGet,
"https://epochalstock.com/api/v1/tags?"+q.Encode(), nil)
req.Header.Set("Authorization", "Bearer "+os.Getenv("EPOCHAL_API_KEY"))
req.Header.Set("Accept", "application/json")
client := &http.Client{Timeout: 20 * time.Second}
res, err := client.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
if res.StatusCode != http.StatusOK {
panic(fmt.Sprintf("%d %s", res.StatusCode, body))
}
var payload map[string]any
_ = json.Unmarshal(body, &payload)
fmt.Println(string(body))
}
import java.net.URI;
import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
String qs = "q=" + URLEncoder.encode("forest", StandardCharsets.UTF_8)
+ "&sort=count&order=desc&limit=50";
HttpClient client = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(10))
.build();
HttpRequest listReq = HttpRequest.newBuilder()
.uri(URI.create("https://epochalstock.com/api/v1/tags?" + qs))
.timeout(Duration.ofSeconds(20))
.header("Authorization", "Bearer " + System.getenv("EPOCHAL_API_KEY"))
.header("Accept", "application/json")
.GET()
.build();
HttpResponse<String> listRes = client.send(listReq, HttpResponse.BodyHandlers.ofString());
if (listRes.statusCode() != 200) {
throw new IllegalStateException(listRes.body());
}
// Parse data tags[] with tag + image_count
System.out.println(listRes.body());
Code examples · stats
Fetch catalog totals. Ideal for a cached dashboard strip.
curl -sS "https://epochalstock.com/api/v1/stats" \
-H "Authorization: Bearer $EPOCHAL_API_KEY" \
-H "Accept: application/json"
const response = await fetch("https://epochalstock.com/api/v1/stats", {
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_images, data.total_tags, data.catalog_built_at);
$ch = curl_init('https://epochalstock.com/api/v1/stats');
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['total_images'], $data['total_tags'], $data['catalog_built_at']
import os
import requests
r = requests.get(
"https://epochalstock.com/api/v1/stats",
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_images"], data["total_tags"], data["catalog_built_at"])
package main
import (
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"time"
)
func main() {
req, _ := http.NewRequest(http.MethodGet,
"https://epochalstock.com/api/v1/stats", nil)
req.Header.Set("Authorization", "Bearer "+os.Getenv("EPOCHAL_API_KEY"))
req.Header.Set("Accept", "application/json")
client := &http.Client{Timeout: 20 * time.Second}
res, err := client.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
if res.StatusCode != http.StatusOK {
panic(fmt.Sprintf("%d %s", res.StatusCode, body))
}
var payload struct {
Data struct {
TotalImages int `json:"total_images"`
TotalTags int `json:"total_tags"`
CatalogBuiltAt string `json:"catalog_built_at"`
} `json:"data"`
}
_ = json.Unmarshal(body, &payload)
fmt.Println(payload.Data.TotalImages, payload.Data.TotalTags, payload.Data.CatalogBuiltAt)
}
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
HttpClient client = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(10))
.build();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://epochalstock.com/api/v1/stats"))
.timeout(Duration.ofSeconds(20))
.header("Authorization", "Bearer " + System.getenv("EPOCHAL_API_KEY"))
.header("Accept", "application/json")
.GET()
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() != 200) {
throw new IllegalStateException(response.body());
}
// Parse data.total_images, data.total_tags, data.catalog_built_at
System.out.println(response.body());
Sample JSON responses
GET /tags
{
"data": {
"tags": [
{ "tag": "nature", "image_count": 2140 },
{ "tag": "architecture", "image_count": 1680 },
{ "tag": "forest", "image_count": 980 }
],
"total": 942,
"next_cursor": null
},
"meta": {
"request_id": "req_01HZZ…"
}
}
GET /tags/{tag}
{
"data": {
"tag": "forest",
"image_count": 980,
"images_url": "https://epochalstock.com/api/v1/images?tags=forest"
},
"meta": {
"request_id": "req_01I00…"
}
}
GET /stats
{
"data": {
"total_images": 12840,
"total_tags": 942,
"catalog_built_at": "2026-07-19T12:00:00Z"
},
"meta": {
"request_id": "req_01I01…"
}
}