Server proxy
Your backend validates the signed-in designer, applies product-level authz, then calls Epochal with the server key. Return only fields the UI needs (title, thumb, id)—not full signed download URLs unless required.
Developer Image API · v1
End-to-end, production-shaped samples for catalog search, metadata, tags, stats, original downloads,
and rate-limit backoff. Every snippet loads secrets from the environment—never hard-coded live keys.
Base URL used on this site: https://epochalstock.com/api/v1.
Store the Bearer secret only on trusted servers or serverless runtimes. Recommended names:
EPOCHAL_API_KEY for the secret and optional EPOCHAL_API_BASE if you need to
override the default base. Catalog calls require either a free-trial key
(instant via POST /trial/signup — 2,000 requests, 2/minute, 1 month) or an active
$50 USD/month subscription (first month $40 with the 20% upgrade
discount) with a named key created after PayPal reports the plan ACTIVE.
Never ship keys in browsers, mobile apps, SPA bundles, or git.
Query-string keys (?api_key=…) are rejected. Use
Authorization: Bearer … only from your backend.
# Export for the current shell session (prefer your host's secret store in production)
export EPOCHAL_API_BASE="https://epochalstock.com/api/v1"
export EPOCHAL_API_KEY="es_live_your_secret_here"
# Quick connectivity check (no key)
curl -sS -i "$EPOCHAL_API_BASE/health"
// process.env — set via your host, Docker secrets, or secrets manager
const base = process.env.EPOCHAL_API_BASE ?? "https://epochalstock.com/api/v1";
const apiKey = process.env.EPOCHAL_API_KEY;
if (!apiKey) {
throw new Error("Set EPOCHAL_API_KEY in the server environment");
}
export function authHeaders() {
return {
Authorization: `Bearer ${apiKey}`,
Accept: "application/json",
};
}
export { base, apiKey };
<?php
declare(strict_types=1);
$base = rtrim((string) getenv('EPOCHAL_API_BASE'), '/') ?: 'https://epochalstock.com/api/v1';
$apiKey = (string) getenv('EPOCHAL_API_KEY');
if ($apiKey === '') {
throw new RuntimeException('Set EPOCHAL_API_KEY in the server environment');
}
function epochal_headers(string $apiKey): array
{
return [
'Authorization: Bearer ' . $apiKey,
'Accept: application/json',
];
}
import os
BASE = os.environ.get("EPOCHAL_API_BASE", "https://epochalstock.com/api/v1").rstrip("/")
API_KEY = os.environ["EPOCHAL_API_KEY"] # KeyError if missing — fail fast
def auth_headers() -> dict[str, str]:
return {
"Authorization": f"Bearer {API_KEY}",
"Accept": "application/json",
}
package main
import (
"os"
"strings"
)
func config() (base, apiKey string) {
base = strings.TrimRight(os.Getenv("EPOCHAL_API_BASE"), "/")
if base == "" {
base = "https://epochalstock.com/api/v1"
}
apiKey = os.Getenv("EPOCHAL_API_KEY")
if apiKey == "" {
panic("set EPOCHAL_API_KEY")
}
return base, apiKey
}
BASE = (ENV["EPOCHAL_API_BASE"] || "https://epochalstock.com/api/v1").sub(%r{/+\z}, "")
API_KEY = ENV.fetch("EPOCHAL_API_KEY") # raises if missing
def auth_headers
{
"Authorization" => "Bearer " + API_KEY,
"Accept" => "application/json",
}
end
// Environment.GetEnvironmentVariable — inject via host config / Key Vault / secrets
var base = (Environment.GetEnvironmentVariable("EPOCHAL_API_BASE")
?? "https://epochalstock.com/api/v1").TrimEnd('/');
var apiKey = Environment.GetEnvironmentVariable("EPOCHAL_API_KEY")
?? throw new InvalidOperationException("Set EPOCHAL_API_KEY");
using var client = new HttpClient { Timeout = TimeSpan.FromSeconds(20) };
client.DefaultRequestHeaders.Authorization =
new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", apiKey);
client.DefaultRequestHeaders.Accept.ParseAdd("application/json");
// System.getenv — set on the process or inject from a secrets manager
String base = System.getenv().getOrDefault("EPOCHAL_API_BASE", "https://epochalstock.com/api/v1");
if (base.endsWith("/")) {
base = base.substring(0, base.length() - 1);
}
String apiKey = System.getenv("EPOCHAL_API_KEY");
if (apiKey == null || apiKey.isBlank()) {
throw new IllegalStateException("Set EPOCHAL_API_KEY");
}
X-Request-ID / meta.request_id—never log Authorization headers or download URLs.GET /images)
Search title, description, and tags with q (max 200 characters). Filter with
tags (up to 20, comma-separated) and tag_mode (all or
any). Sort with relevance, newest, or oldest.
Page with limit (1–100, default 30) and opaque cursor /
next_cursor—do not invent cursors.
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"
# Next page — paste next_cursor from the previous response unchanged
curl -sS "https://epochalstock.com/api/v1/images?q=golden+forest&tags=nature,trees&tag_mode=all&sort=newest&limit=30&cursor=CURSOR" \
-H "Authorization: Bearer $EPOCHAL_API_KEY" \
-H "Accept: application/json"
const base = process.env.EPOCHAL_API_BASE ?? "https://epochalstock.com/api/v1";
const apiKey = process.env.EPOCHAL_API_KEY;
const params = new URLSearchParams({
q: "golden forest",
tags: "nature,trees",
tag_mode: "all",
sort: "newest",
limit: "30",
});
const response = await fetch(`${base}/images?${params}`, {
headers: {
Authorization: `Bearer ${apiKey}`,
Accept: "application/json",
},
});
if (!response.ok) {
throw new Error(`${response.status} ${await response.text()}`);
}
const { data, meta } = await response.json();
console.log("total:", data.total);
console.log("page size:", data.images?.length);
console.log("next_cursor:", data.next_cursor ?? null);
console.log("request_id:", meta?.request_id);
console.log("remaining:", response.headers.get("x-ratelimit-remaining"));
<?php
$base = rtrim((string) getenv('EPOCHAL_API_BASE'), '/') ?: 'https://epochalstock.com/api/v1';
$apiKey = (string) getenv('EPOCHAL_API_KEY');
$query = http_build_query([
'q' => 'golden forest',
'tags' => 'nature,trees',
'tag_mode' => 'all',
'sort' => 'newest',
'limit' => 30,
]);
$ch = curl_init($base . '/images?' . $query);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 20,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $apiKey,
'Accept: application/json',
],
]);
$response = curl_exec($ch);
$status = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($status !== 200) {
throw new RuntimeException('HTTP ' . $status . ': ' . $response);
}
$payload = json_decode((string) $response, true, 512, JSON_THROW_ON_ERROR);
$data = $payload['data'];
// $data['images'], $data['total'], $data['next_cursor'], $data['filters']
import os
import requests
base = os.environ.get("EPOCHAL_API_BASE", "https://epochalstock.com/api/v1").rstrip("/")
api_key = os.environ["EPOCHAL_API_KEY"]
r = requests.get(
f"{base}/images",
params={
"q": "golden forest",
"tags": "nature,trees",
"tag_mode": "all",
"sort": "newest",
"limit": 30,
},
headers={
"Authorization": f"Bearer {api_key}",
"Accept": "application/json",
},
timeout=20,
)
r.raise_for_status()
body = r.json()
data = body["data"]
print("total:", data["total"])
print("images:", len(data["images"]))
print("next_cursor:", data.get("next_cursor"))
print("request_id:", body["meta"]["request_id"])
print("remaining:", r.headers.get("X-RateLimit-Remaining"))
package main
import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"os"
"time"
)
func main() {
base := os.Getenv("EPOCHAL_API_BASE")
if base == "" {
base = "https://epochalstock.com/api/v1"
}
q := url.Values{}
q.Set("q", "golden forest")
q.Set("tags", "nature,trees")
q.Set("tag_mode", "all")
q.Set("sort", "newest")
q.Set("limit", "30")
req, _ := http.NewRequest(http.MethodGet, base+"/images?"+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 struct {
Data struct {
Total int `json:"total"`
Images []map[string]any `json:"images"`
NextCursor string `json:"next_cursor"`
} `json:"data"`
Meta struct {
RequestID string `json:"request_id"`
} `json:"meta"`
}
if err := json.Unmarshal(body, &payload); err != nil {
panic(err)
}
fmt.Println(payload.Data.Total, len(payload.Data.Images), payload.Data.NextCursor)
}
require "json"
require "net/http"
require "uri"
base = (ENV["EPOCHAL_API_BASE"] || "https://epochalstock.com/api/v1").sub(%r{/+\z}, "")
uri = URI(base + "/images")
uri.query = URI.encode_www_form(
q: "golden forest",
tags: "nature,trees",
tag_mode: "all",
sort: "newest",
limit: 30
)
req = Net::HTTP::Get.new(uri)
req["Authorization"] = "Bearer " + ENV.fetch("EPOCHAL_API_KEY")
req["Accept"] = "application/json"
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true, open_timeout: 10, read_timeout: 20) do |http|
http.request(req)
end
raise "HTTP #{res.code}: #{res.body}" unless res.code.to_i == 200
data = JSON.parse(res.body).fetch("data")
puts "total=#{data["total"]} page=#{data["images"]&.size} next=#{data["next_cursor"].inspect}"
using System.Net.Http.Headers;
using System.Text.Json;
using System.Web; // or manual query building
var baseUrl = (Environment.GetEnvironmentVariable("EPOCHAL_API_BASE")
?? "https://epochalstock.com/api/v1").TrimEnd('/');
var apiKey = Environment.GetEnvironmentVariable("EPOCHAL_API_KEY")
?? throw new InvalidOperationException("Set EPOCHAL_API_KEY");
using var client = new HttpClient { Timeout = TimeSpan.FromSeconds(20) };
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", apiKey);
client.DefaultRequestHeaders.Accept.ParseAdd("application/json");
var qs =
"q=golden%20forest&tags=nature,trees&tag_mode=all&sort=newest&limit=30";
using var response = await client.GetAsync(baseUrl + "/images?" + qs);
var text = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode) {
throw new HttpRequestException($"HTTP {(int)response.StatusCode}: {text}");
}
using var doc = JsonDocument.Parse(text);
var data = doc.RootElement.GetProperty("data");
Console.WriteLine(data.GetProperty("total"));
Console.WriteLine(data.GetProperty("images").GetArrayLength());
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 base = System.getenv().getOrDefault("EPOCHAL_API_BASE", "https://epochalstock.com/api/v1");
if (base.endsWith("/")) base = base.substring(0, base.length() - 1);
String apiKey = System.getenv("EPOCHAL_API_KEY");
String q = URLEncoder.encode("golden forest", StandardCharsets.UTF_8);
String url = base + "/images?q=" + q
+ "&tags=nature,trees&tag_mode=all&sort=newest&limit=30";
HttpClient client = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(10))
.build();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(url))
.timeout(Duration.ofSeconds(20))
.header("Authorization", "Bearer " + apiKey)
.header("Accept", "application/json")
.GET()
.build();
HttpResponse<String> response =
client.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() != 200) {
throw new IllegalStateException(response.statusCode() + " " + response.body());
}
// Parse data.images, data.total, data.next_cursor, meta.request_id
System.out.println(response.body());
Full parameter reference: Search & list images.
GET /images/{image_id})Returns ID, slug, title, description, collection, dimensions, file size, extension, tags, creation time, thumbnail URL, and watermarked preview URL. Original filesystem paths are never exposed.
curl -sS "https://epochalstock.com/api/v1/images/es_001_example" \
-H "Authorization: Bearer $EPOCHAL_API_KEY" \
-H "Accept: application/json"
const base = process.env.EPOCHAL_API_BASE ?? "https://epochalstock.com/api/v1";
const imageId = "es_001_example";
const response = await fetch(
`${base}/images/${encodeURIComponent(imageId)}`,
{
headers: {
Authorization: `Bearer ${process.env.EPOCHAL_API_KEY}`,
Accept: "application/json",
},
}
);
if (!response.ok) throw new Error(await response.text());
const { data, meta } = await response.json();
console.log(data.id, data.title, data.width, data.height);
console.log("thumb:", data.thumbnail_url);
console.log("preview:", data.preview_url);
console.log("request_id:", meta.request_id);
<?php
$base = rtrim((string) getenv('EPOCHAL_API_BASE'), '/') ?: 'https://epochalstock.com/api/v1';
$imageId = 'es_001_example';
$url = $base . '/images/' . rawurlencode($imageId);
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 20,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('EPOCHAL_API_KEY'),
'Accept: application/json',
],
]);
$response = curl_exec($ch);
$status = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($status !== 200) {
throw new RuntimeException((string) $response);
}
$image = json_decode((string) $response, true, 512, JSON_THROW_ON_ERROR)['data'];
// $image['id'], title, tags, thumbnail_url, preview_url, …
import os
import requests
base = os.environ.get("EPOCHAL_API_BASE", "https://epochalstock.com/api/v1").rstrip("/")
image_id = "es_001_example"
r = requests.get(
f"{base}/images/{image_id}",
headers={
"Authorization": f"Bearer {os.environ['EPOCHAL_API_KEY']}",
"Accept": "application/json",
},
timeout=20,
)
r.raise_for_status()
data = r.json()["data"]
print(data["id"], data["title"], data.get("thumbnail_url"))
package main
import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"os"
"time"
)
func main() {
base := os.Getenv("EPOCHAL_API_BASE")
if base == "" {
base = "https://epochalstock.com/api/v1"
}
imageID := "es_001_example"
endpoint := base + "/images/" + url.PathEscape(imageID)
req, _ := http.NewRequest(http.MethodGet, endpoint, 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 map[string]any `json:"data"`
}
_ = json.Unmarshal(body, &payload)
fmt.Println(payload.Data["id"], payload.Data["title"])
}
require "json"
require "net/http"
require "uri"
base = (ENV["EPOCHAL_API_BASE"] || "https://epochalstock.com/api/v1").sub(%r{/+\z}, "")
image_id = "es_001_example"
uri = URI(base + "/images/" + URI.encode_www_form_component(image_id))
req = Net::HTTP::Get.new(uri)
req["Authorization"] = "Bearer " + ENV.fetch("EPOCHAL_API_KEY")
req["Accept"] = "application/json"
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true, open_timeout: 10, read_timeout: 20) do |http|
http.request(req)
end
raise "HTTP #{res.code}" unless res.code.to_i == 200
image = JSON.parse(res.body).fetch("data")
puts image["id"], image["title"], image["thumbnail_url"]
using System.Net.Http.Headers;
using System.Text.Json;
var baseUrl = (Environment.GetEnvironmentVariable("EPOCHAL_API_BASE")
?? "https://epochalstock.com/api/v1").TrimEnd('/');
var apiKey = Environment.GetEnvironmentVariable("EPOCHAL_API_KEY")!;
var imageId = "es_001_example";
using var client = new HttpClient { Timeout = TimeSpan.FromSeconds(20) };
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", apiKey);
client.DefaultRequestHeaders.Accept.ParseAdd("application/json");
using var response = await client.GetAsync(
baseUrl + "/images/" + Uri.EscapeDataString(imageId));
response.EnsureSuccessStatusCode();
using var doc = JsonDocument.Parse(await response.Content.ReadAsStringAsync());
var image = doc.RootElement.GetProperty("data");
Console.WriteLine(image.GetProperty("id").GetString());
Console.WriteLine(image.GetProperty("title").GetString());
import java.net.URI;
import java.net.URLEncoder;
import java.net.http.*;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
String base = System.getenv().getOrDefault("EPOCHAL_API_BASE", "https://epochalstock.com/api/v1");
if (base.endsWith("/")) base = base.substring(0, base.length() - 1);
String imageId = "es_001_example";
String path = URLEncoder.encode(imageId, StandardCharsets.UTF_8).replace("+", "%20");
HttpClient client = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(10))
.build();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(base + "/images/" + path))
.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());
}
System.out.println(response.body());
Field notes and examples: Detail & batch.
POST /images/batch)
Resolve 1–100 unique image IDs in a single request (counts as one API call against
the 30/min budget). Missing IDs are omitted; compare found to requested
for partial matches.
curl -sS -X POST "https://epochalstock.com/api/v1/images/batch" \
-H "Authorization: Bearer $EPOCHAL_API_KEY" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{"ids":["es_001_example","es_002_example"]}'
const base = process.env.EPOCHAL_API_BASE ?? "https://epochalstock.com/api/v1";
const ids = ["es_001_example", "es_002_example"];
const response = await fetch(`${base}/images/batch`, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.EPOCHAL_API_KEY}`,
"Content-Type": "application/json",
Accept: "application/json",
},
body: JSON.stringify({ ids }),
});
if (!response.ok) throw new Error(await response.text());
const { data } = await response.json();
console.log(`found ${data.found} of ${data.requested}`);
for (const image of data.images ?? []) {
console.log(image.id, image.title);
}
<?php
$base = rtrim((string) getenv('EPOCHAL_API_BASE'), '/') ?: 'https://epochalstock.com/api/v1';
$ids = ['es_001_example', 'es_002_example'];
$ch = curl_init($base . '/images/batch');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_TIMEOUT => 20,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('EPOCHAL_API_KEY'),
'Content-Type: application/json',
'Accept: application/json',
],
CURLOPT_POSTFIELDS => json_encode(['ids' => $ids], JSON_THROW_ON_ERROR),
]);
$response = curl_exec($ch);
$status = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($status !== 200) {
throw new RuntimeException((string) $response);
}
$data = json_decode((string) $response, true, 512, JSON_THROW_ON_ERROR)['data'];
// $data['requested'], $data['found'], $data['images']
import os
import requests
base = os.environ.get("EPOCHAL_API_BASE", "https://epochalstock.com/api/v1").rstrip("/")
ids = ["es_001_example", "es_002_example"]
r = requests.post(
f"{base}/images/batch",
headers={
"Authorization": f"Bearer {os.environ['EPOCHAL_API_KEY']}",
"Content-Type": "application/json",
"Accept": "application/json",
},
json={"ids": ids},
timeout=20,
)
r.raise_for_status()
data = r.json()["data"]
print(f"found {data['found']} of {data['requested']}")
for image in data.get("images", []):
print(image["id"], image["title"])
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"time"
)
func main() {
base := os.Getenv("EPOCHAL_API_BASE")
if base == "" {
base = "https://epochalstock.com/api/v1"
}
body, _ := json.Marshal(map[string]any{
"ids": []string{"es_001_example", "es_002_example"},
})
req, _ := http.NewRequest(http.MethodPost, base+"/images/batch", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer "+os.Getenv("EPOCHAL_API_KEY"))
req.Header.Set("Content-Type", "application/json")
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()
raw, _ := io.ReadAll(res.Body)
if res.StatusCode != http.StatusOK {
panic(fmt.Sprintf("%d %s", res.StatusCode, raw))
}
fmt.Println(string(raw))
}
require "json"
require "net/http"
require "uri"
base = (ENV["EPOCHAL_API_BASE"] || "https://epochalstock.com/api/v1").sub(%r{/+\z}, "")
uri = URI(base + "/images/batch")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer " + ENV.fetch("EPOCHAL_API_KEY")
req["Content-Type"] = "application/json"
req["Accept"] = "application/json"
req.body = { ids: %w[es_001_example es_002_example] }.to_json
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true, open_timeout: 10, read_timeout: 20) do |http|
http.request(req)
end
raise "HTTP #{res.code}" unless res.code.to_i == 200
data = JSON.parse(res.body).fetch("data")
puts "found #{data["found"]} of #{data["requested"]}"
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
var baseUrl = (Environment.GetEnvironmentVariable("EPOCHAL_API_BASE")
?? "https://epochalstock.com/api/v1").TrimEnd('/');
var apiKey = Environment.GetEnvironmentVariable("EPOCHAL_API_KEY")!;
using var client = new HttpClient { Timeout = TimeSpan.FromSeconds(20) };
using var req = new HttpRequestMessage(HttpMethod.Post, baseUrl + "/images/batch");
req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", apiKey);
req.Headers.Accept.ParseAdd("application/json");
req.Content = new StringContent(
"""{"ids":["es_001_example","es_002_example"]}""",
Encoding.UTF8,
"application/json");
using var res = await client.SendAsync(req);
var text = await res.Content.ReadAsStringAsync();
res.EnsureSuccessStatusCode();
using var doc = JsonDocument.Parse(text);
var data = doc.RootElement.GetProperty("data");
Console.WriteLine($"{data.GetProperty("found")} of {data.GetProperty("requested")}");
import java.net.URI;
import java.net.http.*;
import java.time.Duration;
String base = System.getenv().getOrDefault("EPOCHAL_API_BASE", "https://epochalstock.com/api/v1");
if (base.endsWith("/")) base = base.substring(0, base.length() - 1);
String json = "{\"ids\":[\"es_001_example\",\"es_002_example\"]}";
HttpClient client = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(10))
.build();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(base + "/images/batch"))
.timeout(Duration.ofSeconds(20))
.header("Authorization", "Bearer " + System.getenv("EPOCHAL_API_KEY"))
.header("Content-Type", "application/json")
.header("Accept", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(json))
.build();
HttpResponse<String> response =
client.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() != 200) {
throw new IllegalStateException(response.body());
}
System.out.println(response.body());
GET /tags)
Browse normalized tags and image counts. Query with optional q;
sort is name or count; order is
asc or desc; paginate with limit and cursor.
Each item exposes tag and image_count.
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 (path-encode the tag segment)
curl -sS "https://epochalstock.com/api/v1/tags/forest" \
-H "Authorization: Bearer $EPOCHAL_API_KEY" \
-H "Accept: application/json"
const base = process.env.EPOCHAL_API_BASE ?? "https://epochalstock.com/api/v1";
const params = new URLSearchParams({
q: "forest",
sort: "count",
order: "desc",
limit: "50",
});
const listRes = await fetch(`${base}/tags?${params}`, {
headers: {
Authorization: `Bearer ${process.env.EPOCHAL_API_KEY}`,
Accept: "application/json",
},
});
if (!listRes.ok) throw new Error(await listRes.text());
const { data } = await listRes.json();
const rows = data.tags ?? [];
for (const row of rows) {
console.log(row.tag, row.image_count);
}
<?php
$base = rtrim((string) getenv('EPOCHAL_API_BASE'), '/') ?: 'https://epochalstock.com/api/v1';
$query = http_build_query([
'q' => 'forest',
'sort' => 'count',
'order' => 'desc',
'limit' => 50,
]);
$ch = curl_init($base . '/tags?' . $query);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 20,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('EPOCHAL_API_KEY'),
'Accept: application/json',
],
]);
$response = curl_exec($ch);
if ((int) curl_getinfo($ch, CURLINFO_HTTP_CODE) !== 200) {
throw new RuntimeException((string) $response);
}
$data = json_decode((string) $response, true, 512, JSON_THROW_ON_ERROR)['data'];
// Each row: tag, image_count
import os
import requests
base = os.environ.get("EPOCHAL_API_BASE", "https://epochalstock.com/api/v1").rstrip("/")
r = requests.get(
f"{base}/tags",
params={"q": "forest", "sort": "count", "order": "desc", "limit": 50},
headers={
"Authorization": f"Bearer {os.environ['EPOCHAL_API_KEY']}",
"Accept": "application/json",
},
timeout=20,
)
r.raise_for_status()
data = r.json()["data"]
rows = data.get("tags") or []
for row in rows:
print(row["tag"], row["image_count"])
package main
import (
"fmt"
"io"
"net/http"
"net/url"
"os"
"time"
)
func main() {
base := os.Getenv("EPOCHAL_API_BASE")
if base == "" {
base = "https://epochalstock.com/api/v1"
}
q := url.Values{}
q.Set("q", "forest")
q.Set("sort", "count")
q.Set("order", "desc")
q.Set("limit", "50")
req, _ := http.NewRequest(http.MethodGet, base+"/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)
fmt.Println(res.StatusCode, string(body))
}
require "json"
require "net/http"
require "uri"
base = (ENV["EPOCHAL_API_BASE"] || "https://epochalstock.com/api/v1").sub(%r{/+\z}, "")
uri = URI(base + "/tags")
uri.query = URI.encode_www_form(q: "forest", sort: "count", order: "desc", limit: 50)
req = Net::HTTP::Get.new(uri)
req["Authorization"] = "Bearer " + ENV.fetch("EPOCHAL_API_KEY")
req["Accept"] = "application/json"
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true, open_timeout: 10, read_timeout: 20) do |http|
http.request(req)
end
raise "HTTP #{res.code}" unless res.code.to_i == 200
puts res.body
using System.Net.Http.Headers;
var baseUrl = (Environment.GetEnvironmentVariable("EPOCHAL_API_BASE")
?? "https://epochalstock.com/api/v1").TrimEnd('/');
using 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");
using var res = await client.GetAsync(
baseUrl + "/tags?q=forest&sort=count&order=desc&limit=50");
res.EnsureSuccessStatusCode();
Console.WriteLine(await res.Content.ReadAsStringAsync());
import java.net.URI;
import java.net.http.*;
import java.time.Duration;
String base = System.getenv().getOrDefault("EPOCHAL_API_BASE", "https://epochalstock.com/api/v1");
if (base.endsWith("/")) base = base.substring(0, base.length() - 1);
HttpClient client = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(10))
.build();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(base + "/tags?q=forest&sort=count&order=desc&limit=50"))
.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());
}
System.out.println(response.body());
More tag/stats detail: Tags & stats.
GET /stats)
Returns total_images, total_tags, and catalog_built_at.
Cache this on your backend for tens of seconds to a few minutes—polling every second wastes the
shared 30 req/min budget.
curl -sS "https://epochalstock.com/api/v1/stats" \
-H "Authorization: Bearer $EPOCHAL_API_KEY" \
-H "Accept: application/json"
const base = process.env.EPOCHAL_API_BASE ?? "https://epochalstock.com/api/v1";
const response = await fetch(`${base}/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);
<?php
$base = rtrim((string) getenv('EPOCHAL_API_BASE'), '/') ?: 'https://epochalstock.com/api/v1';
$ch = curl_init($base . '/stats');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 20,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('EPOCHAL_API_KEY'),
'Accept: application/json',
],
]);
$response = curl_exec($ch);
if ((int) curl_getinfo($ch, CURLINFO_HTTP_CODE) !== 200) {
throw new RuntimeException((string) $response);
}
$data = json_decode((string) $response, true, 512, JSON_THROW_ON_ERROR)['data'];
// $data['total_images'], $data['total_tags'], $data['catalog_built_at']
import os
import requests
base = os.environ.get("EPOCHAL_API_BASE", "https://epochalstock.com/api/v1").rstrip("/")
r = requests.get(
f"{base}/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 (
"fmt"
"io"
"net/http"
"os"
"time"
)
func main() {
base := os.Getenv("EPOCHAL_API_BASE")
if base == "" {
base = "https://epochalstock.com/api/v1"
}
req, _ := http.NewRequest(http.MethodGet, base+"/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)
fmt.Println(string(body))
}
require "net/http"
require "uri"
base = (ENV["EPOCHAL_API_BASE"] || "https://epochalstock.com/api/v1").sub(%r{/+\z}, "")
uri = URI(base + "/stats")
req = Net::HTTP::Get.new(uri)
req["Authorization"] = "Bearer " + ENV.fetch("EPOCHAL_API_KEY")
req["Accept"] = "application/json"
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true, open_timeout: 10, read_timeout: 20) do |http|
http.request(req)
end
raise "HTTP #{res.code}" unless res.code.to_i == 200
puts res.body
using System.Net.Http.Headers;
var baseUrl = (Environment.GetEnvironmentVariable("EPOCHAL_API_BASE")
?? "https://epochalstock.com/api/v1").TrimEnd('/');
using 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");
using var res = await client.GetAsync(baseUrl + "/stats");
res.EnsureSuccessStatusCode();
Console.WriteLine(await res.Content.ReadAsStringAsync());
import java.net.URI;
import java.net.http.*;
import java.time.Duration;
String base = System.getenv().getOrDefault("EPOCHAL_API_BASE", "https://epochalstock.com/api/v1");
if (base.endsWith("/")) base = base.substring(0, base.length() - 1);
HttpClient client = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(10))
.build();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(base + "/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());
}
System.out.println(response.body());
Originals never put the API key on the binary URL. First mint a five-minute, single-use token with
POST /downloads, then GET download_url without
Authorization. Only one original may transfer at a time per account
(409 download_already_active if another transfer is in flight).
Two-step original delivery
# 1) Create single-use download token (Bearer required)
curl -sS -X POST "https://epochalstock.com/api/v1/downloads" \
-H "Authorization: Bearer $EPOCHAL_API_KEY" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{"image_id":"es_001_example"}'
# 2) Download binary WITHOUT the API key
# Replace $DOWNLOAD_URL with data.download_url from the mint response
curl -fL "$DOWNLOAD_URL" -o image.jpg
import { writeFile } from "node:fs/promises";
const base = process.env.EPOCHAL_API_BASE ?? "https://epochalstock.com/api/v1";
const apiKey = process.env.EPOCHAL_API_KEY;
const imageId = "es_001_example";
// 1) Mint token
const mint = await fetch(`${base}/downloads`, {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
Accept: "application/json",
},
body: JSON.stringify({ image_id: imageId }),
});
if (!mint.ok) {
throw new Error(`mint ${mint.status}: ${await mint.text()}`);
}
const { data } = await mint.json();
const downloadUrl = data.download_url;
// 2) Fetch original — no Authorization header
const fileRes = await fetch(downloadUrl);
if (!fileRes.ok) {
throw new Error(`download ${fileRes.status}`);
}
const buffer = Buffer.from(await fileRes.arrayBuffer());
await writeFile("image.jpg", buffer);
<?php
$base = rtrim((string) getenv('EPOCHAL_API_BASE'), '/') ?: 'https://epochalstock.com/api/v1';
$apiKey = getenv('EPOCHAL_API_KEY');
$imageId = 'es_001_example';
// 1) Mint token
$ch = curl_init($base . '/downloads');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 20,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $apiKey,
'Content-Type: application/json',
'Accept: application/json',
],
CURLOPT_POSTFIELDS => json_encode(['image_id' => $imageId], JSON_THROW_ON_ERROR),
]);
$raw = curl_exec($ch);
$status = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($status !== 201 && $status !== 200) {
throw new RuntimeException('mint failed: ' . $raw);
}
$downloadUrl = json_decode((string) $raw, true, 512, JSON_THROW_ON_ERROR)['data']['download_url'];
// 2) Binary GET — no API key
$ch = curl_init($downloadUrl);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_TIMEOUT => 120,
]);
$bytes = curl_exec($ch);
if ((int) curl_getinfo($ch, CURLINFO_HTTP_CODE) !== 200) {
throw new RuntimeException('download failed');
}
file_put_contents('image.jpg', $bytes);
import os
import requests
base = os.environ.get("EPOCHAL_API_BASE", "https://epochalstock.com/api/v1").rstrip("/")
api_key = os.environ["EPOCHAL_API_KEY"]
image_id = "es_001_example"
# 1) Mint token
mint = requests.post(
f"{base}/downloads",
headers={
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
"Accept": "application/json",
},
json={"image_id": image_id},
timeout=20,
)
mint.raise_for_status()
download_url = mint.json()["data"]["download_url"]
# 2) Binary GET — no API key
with requests.get(download_url, stream=True, timeout=120) as r:
r.raise_for_status()
with open("image.jpg", "wb") as f:
for chunk in r.iter_content(chunk_size=1024 * 256):
if chunk:
f.write(chunk)
package main
import (
"bytes"
"encoding/json"
"io"
"net/http"
"os"
"time"
)
func main() {
base := os.Getenv("EPOCHAL_API_BASE")
if base == "" {
base = "https://epochalstock.com/api/v1"
}
apiKey := os.Getenv("EPOCHAL_API_KEY")
body, _ := json.Marshal(map[string]string{"image_id": "es_001_example"})
// 1) Mint token
req, _ := http.NewRequest(http.MethodPost, base+"/downloads", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer "+apiKey)
req.Header.Set("Content-Type", "application/json")
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()
var payload struct {
Data struct {
DownloadURL string `json:"download_url"`
} `json:"data"`
}
if err := json.NewDecoder(res.Body).Decode(&payload); err != nil {
panic(err)
}
// 2) Binary GET — no API key
dlClient := &http.Client{Timeout: 120 * time.Second}
dl, err := dlClient.Get(payload.Data.DownloadURL)
if err != nil {
panic(err)
}
defer dl.Body.Close()
out, _ := os.Create("image.jpg")
defer out.Close()
io.Copy(out, dl.Body)
}
require "json"
require "net/http"
require "uri"
base = (ENV["EPOCHAL_API_BASE"] || "https://epochalstock.com/api/v1").sub(%r{/+\z}, "")
api_key = ENV.fetch("EPOCHAL_API_KEY")
# 1) Mint token
uri = URI(base + "/downloads")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer " + api_key
req["Content-Type"] = "application/json"
req["Accept"] = "application/json"
req.body = { image_id: "es_001_example" }.to_json
mint = Net::HTTP.start(uri.host, uri.port, use_ssl: true, open_timeout: 10, read_timeout: 20) do |http|
http.request(req)
end
raise "mint failed: #{mint.code}" unless [200, 201].include?(mint.code.to_i)
download_url = JSON.parse(mint.body).dig("data", "download_url")
# 2) Binary GET — no API key
dl_uri = URI(download_url)
Net::HTTP.start(dl_uri.host, dl_uri.port, use_ssl: true, open_timeout: 10, read_timeout: 120) do |http|
resp = http.get(dl_uri.request_uri)
raise "download failed: #{resp.code}" unless resp.code.to_i == 200
File.binwrite("image.jpg", resp.body)
end
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
var baseUrl = (Environment.GetEnvironmentVariable("EPOCHAL_API_BASE")
?? "https://epochalstock.com/api/v1").TrimEnd('/');
var apiKey = Environment.GetEnvironmentVariable("EPOCHAL_API_KEY")!;
using var client = new HttpClient { Timeout = TimeSpan.FromSeconds(20) };
// 1) Mint token
using var mintReq = new HttpRequestMessage(HttpMethod.Post, baseUrl + "/downloads");
mintReq.Headers.Authorization = new AuthenticationHeaderValue("Bearer", apiKey);
mintReq.Headers.Accept.ParseAdd("application/json");
mintReq.Content = new StringContent(
"""{"image_id":"es_001_example"}""",
Encoding.UTF8,
"application/json");
using var mintRes = await client.SendAsync(mintReq);
mintRes.EnsureSuccessStatusCode();
using var mintDoc = JsonDocument.Parse(await mintRes.Content.ReadAsStringAsync());
var downloadUrl = mintDoc.RootElement
.GetProperty("data")
.GetProperty("download_url")
.GetString()!;
// 2) Binary GET — no API key
using var dlClient = new HttpClient { Timeout = TimeSpan.FromMinutes(2) };
using var fileRes = await dlClient.GetAsync(downloadUrl);
fileRes.EnsureSuccessStatusCode();
await using var fs = File.Create("image.jpg");
await fileRes.Content.CopyToAsync(fs);
import java.net.URI;
import java.net.http.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Duration;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
String base = System.getenv().getOrDefault("EPOCHAL_API_BASE", "https://epochalstock.com/api/v1");
if (base.endsWith("/")) base = base.substring(0, base.length() - 1);
String apiKey = System.getenv("EPOCHAL_API_KEY");
HttpClient client = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(10))
.build();
// 1) Mint token
HttpRequest mintReq = HttpRequest.newBuilder()
.uri(URI.create(base + "/downloads"))
.timeout(Duration.ofSeconds(20))
.header("Authorization", "Bearer " + apiKey)
.header("Content-Type", "application/json")
.header("Accept", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(
"{\"image_id\":\"es_001_example\"}"))
.build();
HttpResponse<String> mintRes =
client.send(mintReq, HttpResponse.BodyHandlers.ofString());
if (mintRes.statusCode() != 200 && mintRes.statusCode() != 201) {
throw new IllegalStateException("mint " + mintRes.statusCode() + " " + mintRes.body());
}
// Minimal extraction — prefer a real JSON library in production
Matcher m = Pattern.compile("\"download_url\"\s*:\s*\"([^\"]+)\"")
.matcher(mintRes.body());
if (!m.find()) throw new IllegalStateException("download_url missing");
String downloadUrl = m.group(1).replace("\\/", "/");
// 2) Binary GET — no API key
HttpRequest fileReq = HttpRequest.newBuilder()
.uri(URI.create(downloadUrl))
.timeout(Duration.ofMinutes(2))
.GET()
.build();
HttpResponse<byte[]> fileRes =
client.send(fileReq, HttpResponse.BodyHandlers.ofByteArray());
if (fileRes.statusCode() != 200) {
throw new IllegalStateException("download " + fileRes.statusCode());
}
Files.write(Path.of("image.jpg"), fileRes.body());
Treat download_url as a short-lived secret.
Do not log it to analytics, paste it into tickets, or put it in browser-visible HTML when you can
stream bytes through your backend instead. Details:
Downloads.
The account limit is 30 authenticated requests per minute (shared across all keys).
On over-limit, the API returns HTTP 429, rate_limit_exceeded, and usually
Retry-After. Retry only 429, 502, and 503 with
bounded exponential backoff and jitter—do not blind-retry other 4xx.
# Inspect rate-limit headers on any authenticated call
curl -sS -D - -o /tmp/epochal-body.json \
"https://epochalstock.com/api/v1/images?limit=5" \
-H "Authorization: Bearer $EPOCHAL_API_KEY" \
-H "Accept: application/json" | grep -iE 'HTTP/|x-ratelimit|retry-after|x-request-id'
# On 429, sleep at least Retry-After seconds, then retry once (shell sketch)
# sleep "$RETRY_AFTER"; curl ...
const base = process.env.EPOCHAL_API_BASE ?? "https://epochalstock.com/api/v1";
async function fetchWithBackoff(url, options = {}, maxAttempts = 5) {
let attempt = 0;
for (;;) {
const res = await fetch(url, options);
if (res.ok) return res;
const retryable = [429, 502, 503].includes(res.status);
if (!retryable || attempt >= maxAttempts - 1) {
const body = await res.text();
throw new Error(
`HTTP ${res.status} id=${res.headers.get("x-request-id")} ${body}`
);
}
const retryAfter = Number(res.headers.get("retry-after"));
const baseMs = Number.isFinite(retryAfter) && retryAfter > 0
? retryAfter * 1000
: Math.min(30_000, 500 * 2 ** attempt);
const jitter = Math.floor(Math.random() * 250);
await new Promise((r) => setTimeout(r, baseMs + jitter));
attempt += 1;
}
}
const res = await fetchWithBackoff(`${base}/images?limit=30`, {
headers: {
Authorization: `Bearer ${process.env.EPOCHAL_API_KEY}`,
Accept: "application/json",
},
});
const json = await res.json();
console.log(json.data?.total, res.headers.get("x-ratelimit-remaining"));
<?php
function fetch_with_backoff(string $url, array $headers, int $maxAttempts = 5): string
{
$attempt = 0;
while (true) {
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => $headers,
CURLOPT_TIMEOUT => 20,
CURLOPT_HEADER => true,
]);
$raw = curl_exec($ch);
$status = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
$headerSize = (int) curl_getinfo($ch, CURLINFO_HEADER_SIZE);
curl_close($ch);
$headerBlob = substr((string) $raw, 0, $headerSize);
$body = substr((string) $raw, $headerSize);
if ($status >= 200 && $status < 300) {
return $body;
}
$retryable = in_array($status, [429, 502, 503], true);
if (!$retryable || $attempt >= $maxAttempts - 1) {
throw new RuntimeException("HTTP {$status}: {$body}");
}
$retryAfter = 0.0;
if (preg_match('/^Retry-After:\s*(\d+)/mi', $headerBlob, $m)) {
$retryAfter = (float) $m[1];
}
$baseDelay = $retryAfter > 0 ? $retryAfter : min(30.0, 0.5 * (2 ** $attempt));
$jitter = mt_rand(0, 250) / 1000;
usleep((int) (($baseDelay + $jitter) * 1_000_000));
$attempt++;
}
}
$base = rtrim((string) getenv('EPOCHAL_API_BASE'), '/') ?: 'https://epochalstock.com/api/v1';
$headers = [
'Authorization: Bearer ' . getenv('EPOCHAL_API_KEY'),
'Accept: application/json',
];
$json = fetch_with_backoff($base . '/images?limit=30', $headers);
import os
import random
import time
import requests
BASE = os.environ.get("EPOCHAL_API_BASE", "https://epochalstock.com/api/v1").rstrip("/")
RETRYABLE = {429, 502, 503}
def fetch_with_backoff(method, url, *, max_attempts=5, **kwargs):
attempt = 0
while True:
r = requests.request(method, url, timeout=20, **kwargs)
if r.ok:
return r
if r.status_code not in RETRYABLE or attempt >= max_attempts - 1:
rid = r.headers.get("X-Request-ID")
raise RuntimeError(f"HTTP {r.status_code} request_id={rid} body={r.text}")
retry_after = r.headers.get("Retry-After")
if retry_after and retry_after.isdigit():
delay = float(retry_after)
else:
delay = min(30.0, 0.5 * (2 ** attempt))
delay += random.uniform(0, 0.25)
time.sleep(delay)
attempt += 1
r = fetch_with_backoff(
"GET",
f"{BASE}/images",
params={"limit": 30},
headers={
"Authorization": f"Bearer {os.environ['EPOCHAL_API_KEY']}",
"Accept": "application/json",
},
)
print(r.headers.get("X-RateLimit-Remaining"), r.json()["data"]["total"])
package main
import (
"fmt"
"io"
"math"
"math/rand"
"net/http"
"os"
"strconv"
"time"
)
func fetchWithBackoff(req *http.Request, maxAttempts int) (*http.Response, []byte, error) {
client := &http.Client{Timeout: 20 * time.Second}
for attempt := 0; attempt < maxAttempts; attempt++ {
// Clone is needed if you reuse req after reading the body on POSTs
res, err := client.Do(req)
if err != nil {
return nil, nil, err
}
body, _ := io.ReadAll(res.Body)
res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
return res, body, nil
}
retryable := res.StatusCode == 429 || res.StatusCode == 502 || res.StatusCode == 503
if !retryable || attempt == maxAttempts-1 {
return res, body, fmt.Errorf("HTTP %d %s", res.StatusCode, body)
}
delay := 0.5 * math.Pow(2, float64(attempt))
if ra := res.Header.Get("Retry-After"); ra != "" {
if sec, err := strconv.ParseFloat(ra, 64); err == nil {
delay = sec
}
}
if delay > 30 {
delay = 30
}
delay += rand.Float64() * 0.25
time.Sleep(time.Duration(delay * float64(time.Second)))
}
return nil, nil, fmt.Errorf("exhausted retries")
}
func main() {
base := os.Getenv("EPOCHAL_API_BASE")
if base == "" {
base = "https://epochalstock.com/api/v1"
}
req, _ := http.NewRequest(http.MethodGet, base+"/images?limit=30", nil)
req.Header.Set("Authorization", "Bearer "+os.Getenv("EPOCHAL_API_KEY"))
req.Header.Set("Accept", "application/json")
res, body, err := fetchWithBackoff(req, 5)
if err != nil {
panic(err)
}
fmt.Println(res.Header.Get("X-RateLimit-Remaining"), string(body))
}
require "json"
require "net/http"
require "uri"
base = (ENV["EPOCHAL_API_BASE"] || "https://epochalstock.com/api/v1").sub(%r{/+\z}, "")
api_key = ENV.fetch("EPOCHAL_API_KEY")
RETRYABLE = [429, 502, 503].freeze
def fetch_with_backoff(uri, req, max_attempts: 5)
attempt = 0
loop do
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true, open_timeout: 10, read_timeout: 20) do |http|
http.request(req)
end
return res if res.code.to_i.between?(200, 299)
status = res.code.to_i
raise "HTTP #{status}: #{res.body}" if !RETRYABLE.include?(status) || attempt >= max_attempts - 1
retry_after = res["Retry-After"].to_f
delay = retry_after.positive? ? retry_after : [30.0, 0.5 * (2**attempt)].min
delay += rand * 0.25
sleep delay
attempt += 1
end
end
uri = URI(base + "/images?limit=30")
req = Net::HTTP::Get.new(uri)
req["Authorization"] = "Bearer " + api_key
req["Accept"] = "application/json"
res = fetch_with_backoff(uri, req)
puts res["X-RateLimit-Remaining"], JSON.parse(res.body).dig("data", "total")
using System.Net.Http.Headers;
var baseUrl = (Environment.GetEnvironmentVariable("EPOCHAL_API_BASE")
?? "https://epochalstock.com/api/v1").TrimEnd('/');
var apiKey = Environment.GetEnvironmentVariable("EPOCHAL_API_KEY")!;
using var client = new HttpClient { Timeout = TimeSpan.FromSeconds(20) };
async Task<HttpResponseMessage> FetchWithBackoffAsync(
Func<HttpRequestMessage> factory, int maxAttempts = 5)
{
for (var attempt = 0; ; attempt++)
{
using var req = factory();
req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", apiKey);
req.Headers.Accept.ParseAdd("application/json");
var res = await client.SendAsync(req);
if (res.IsSuccessStatusCode) return res;
var code = (int)res.StatusCode;
var retryable = code is 429 or 502 or 503;
if (!retryable || attempt >= maxAttempts - 1)
{
var body = await res.Content.ReadAsStringAsync();
throw new HttpRequestException($"HTTP {code}: {body}");
}
double delay = 0.5 * Math.Pow(2, attempt);
if (res.Headers.RetryAfter?.Delta is TimeSpan ts)
delay = ts.TotalSeconds;
delay = Math.Min(30, delay) + Random.Shared.NextDouble() * 0.25;
res.Dispose();
await Task.Delay(TimeSpan.FromSeconds(delay));
}
}
using var response = await FetchWithBackoffAsync(
() => new HttpRequestMessage(HttpMethod.Get, baseUrl + "/images?limit=30"));
Console.WriteLine(await response.Content.ReadAsStringAsync());
import java.net.URI;
import java.net.http.*;
import java.time.Duration;
import java.util.Set;
import java.util.concurrent.ThreadLocalRandom;
String base = System.getenv().getOrDefault("EPOCHAL_API_BASE", "https://epochalstock.com/api/v1");
if (base.endsWith("/")) base = base.substring(0, base.length() - 1);
String apiKey = System.getenv("EPOCHAL_API_KEY");
Set<Integer> retryable = Set.of(429, 502, 503);
HttpClient client = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(10))
.build();
HttpResponse<String> response = null;
for (int attempt = 0; attempt < 5; attempt++) {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(base + "/images?limit=30"))
.timeout(Duration.ofSeconds(20))
.header("Authorization", "Bearer " + apiKey)
.header("Accept", "application/json")
.GET()
.build();
response = client.send(request, HttpResponse.BodyHandlers.ofString());
int code = response.statusCode();
if (code >= 200 && code < 300) break;
if (!retryable.contains(code) || attempt == 4) {
throw new IllegalStateException(code + " " + response.body());
}
double delay = Math.min(30.0, 0.5 * Math.pow(2, attempt));
String ra = response.headers().firstValue("Retry-After").orElse("");
try { if (!ra.isBlank()) delay = Double.parseDouble(ra); } catch (NumberFormatException ignored) {}
delay += ThreadLocalRandom.current().nextDouble(0, 0.25);
Thread.sleep((long) (delay * 1000));
}
System.out.println(response.body());
Strategy tables and error codes: Rate limits & errors.
Design tools, CMS plugins, and Canva-class editors should treat Epochal as a server-side dependency. End-user browsers and desktop renderer processes must never hold live API keys.
Your backend validates the signed-in designer, applies product-level authz, then calls Epochal with the server key. Return only fields the UI needs (title, thumb, id)—not full signed download URLs unless required.
Cache search pages, tag lists, and GET /stats briefly. Prefer
POST /images/batch when rehydrating many known IDs (one request ≤ 100 IDs).
No keys in Electron renderer configs, browser extensions, mobile binaries, or public env files shipped with installers. Revoke and rotate if a build may have leaked a secret.
Original downloads allow unlimited volume but only one concurrent transfer per account.
Queue exports and handle 409 download_already_active by waiting, not fan-out.
Recommended architecture
design-app-prod, design-app-staging).
Common mistake: embedding es_live_… in a frontend env file
(VITE_, NEXT_PUBLIC_, Expo public config). Those values ship to every
user. Keep keys server-only and proxy required catalog fields.
Bearer format, key lifecycle, and why query-string keys fail.
Full search parameter and cursor pagination reference.
Token expiry, concurrency lease, and transfer errors.
Checklist, portal management endpoints, and license notes.
Structured codes and when not to retry.
Account → subscribe → first authenticated request.