Two steps
POST https://epochalstock.com/api/v1/downloads with image_id and Bearer auth,
then GET the returned download_url with no API key.
Developer Image API · v1
Original files are never exposed as permanent filesystem paths. Instead, your server creates a
short-lived, single-use download URL with POST /downloads, then fetches that URL
without the Bearer API key. Tokens last five minutes, become invalid after the
first successful use, and only one original transfer may run at a time per account.
Catalog endpoints return metadata, thumbnails, and watermarked previews only. When your product needs the full original (for export, print pipeline, or licensed asset delivery), use the two-step download flow. The design keeps secret API keys out of download URLs so they never land in browser history, proxy logs, CDN access logs, or analytics beacons.
POST https://epochalstock.com/api/v1/downloads with image_id and Bearer auth,
then GET the returned download_url with no API key.
Each signed URL is valid for about five minutes and is single-use. After first use or expiry, create a new token.
Only one original may transfer at a time per account. Parallel attempts return
409 download_already_active. Serial downloads are unlimited.
The binary GET does not consume an extra catalog rate-limit request, but it
holds the concurrency lease until the transfer finishes.
Prerequisite: Resolve a valid image ID first via
search,
image detail, or
batch metadata.
Originals for missing or unavailable assets return catalog errors such as
image_not_found or original_unavailable.
Mint a token on your trusted server, then pull the binary. Never attach the Bearer key to the download URL.
Create token → fetch binary
POST https://epochalstock.com/api/v1/downloads with
Authorization: Bearer …,
Content-Type: application/json, and body
{"image_id":"…"}. A successful response is HTTP 201 with a signed
download_url (and related metadata such as request id in meta).
GET to that download_url. Do not add
the API key as a header or query parameter. Follow redirects if your client requires it, and
write the response body to disk as the original image bytes.
Never put API keys on download URLs. The signed URL already authorizes a
single transfer. Adding ?api_key=… or a Bearer header is unnecessary and can
leak secrets into logs. Query-string keys on catalog endpoints are rejected with
query_key_rejected; treat download URLs the same way—no secrets in the path or query.
Authenticated endpoint. Counts against the account’s 30 requests/minute catalog budget.
POST https://epochalstock.com/api/v1/downloads
Authorization: Bearer $EPOCHAL_API_KEY
Content-Type: application/json
Accept: application/json
{"image_id":"es_001_example"}
| Field | Location | Description |
|---|---|---|
image_id |
JSON body | Required. Catalog image identifier (for example from search or detail). |
Authorization |
Header | Bearer live API key. Required for token creation only. |
download_url |
Response | Signed URL for the binary transfer. Treat as a secret until it expires. |
Minimal cURL for token creation:
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"}'
Expect HTTP 201 on success. Parse the JSON body for download_url
(under data depending on response envelope). Capture X-Request-ID on failure.
Unauthenticated relative to your API key: the signature in the URL is the credential. Download immediately after minting so the five-minute window is not wasted on queue delay.
GET $DOWNLOAD_URL
# No Authorization header
# No api_key query parameter
curl -fL "$DOWNLOAD_URL" -o image.jpg
POST /downloads.Each account may run one concurrent original transfer. All named API keys on that account share the same lease. Creating more keys does not allow parallel originals.
Download A, wait until it completes, then download B, C, … without a hard cap on count. Serial throughput is limited by transfer time and your integration design—not a download credit balance.
Starting a second original while another is active returns HTTP 409 with
download_already_active. Finish or abandon the first transfer, then retry.
Staging and production keys on the same account compete for the single lease. Isolate high-volume export jobs or serialize them in a worker queue.
Integration tip: Queue original downloads in a single worker (or a mutex per
account) so concurrent product features never stampede POST /downloads. On
409 download_already_active, wait and retry with backoff rather than minting
many tokens that cannot be redeemed yet.
POST /downloads is an authenticated catalog-style request and counts toward the
30 requests per minute per account limit.
GET on download_url
does not consume an additional catalog rate-limit request.
POST /images/batch
when resolving many IDs so metadata work uses fewer of the 30 requests before you start downloading.
Full headers, 429 behavior, and error codes:
Rate limits & errors.
Create a five-minute token for es_001_example, then download the original to a local
file. Load the API key from the environment only. Never log the signed download_url
to analytics platforms.
# 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 201 response
curl -fL "$DOWNLOAD_URL" -o image.jpg
import { writeFile } from "node:fs/promises";
const apiKey = process.env.EPOCHAL_API_KEY;
const imageId = "es_001_example";
// 1) Mint token
const mint = await fetch("https://epochalstock.com/api/v1/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);
$apiKey = getenv('EPOCHAL_API_KEY');
$imageId = 'es_001_example';
// 1) Mint token
$ch = curl_init('https://epochalstock.com/api/v1/downloads');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $apiKey,
'Content-Type: application/json',
'Accept: application/json',
],
CURLOPT_POSTFIELDS => json_encode(['image_id' => $imageId], JSON_THROW_ON_ERROR),
CURLOPT_TIMEOUT => 20,
]);
$raw = curl_exec($ch);
$status = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($status !== 201) {
throw new RuntimeException('mint failed: ' . $raw);
}
$downloadUrl = json_decode($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
api_key = os.environ["EPOCHAL_API_KEY"]
image_id = "es_001_example"
# 1) Mint token
mint = requests.post(
"https://epochalstock.com/api/v1/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() {
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,
"https://epochalstock.com/api/v1/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)
}
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
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,
"https://epochalstock.com/api/v1/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);
require "json"
require "net/http"
require "uri"
api_key = ENV.fetch("EPOCHAL_API_KEY")
# 1) Mint token
uri = URI("https://epochalstock.com/api/v1/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) do |http|
http.request(req)
end
raise "mint failed: #{mint.code}" unless mint.code.to_i == 201
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) 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
Treat download URLs as secrets. A signed download_url can
retrieve the full original until it expires or is used. Do not paste it into chat, tickets,
public issue trackers, or client-side analytics. Prefer streaming the bytes through your
backend when end users must not see the signed URL.
download_url values to long-lived analytics or third-party APM without redaction.X-Request-ID when token creation fails—support can investigate without secrets.Broader key handling, portal CSRF, and billing isolation: Security & billing.
Failures return structured JSON on authenticated calls. Binary URL failures may return status codes without a full catalog envelope—still capture status and any request id you have from the mint step.
| HTTP | Code | When it happens | What to do |
|---|---|---|---|
401 |
invalid_api_key |
Bearer key missing, revoked, or wrong on POST /downloads. |
Fix credentials; do not retry blindly. |
401 |
invalid_download_token |
Token expired, already used, or malformed. | Mint a new token and download once promptly. |
404 |
image_not_found |
Unknown image_id. |
Re-search or refresh metadata; confirm the ID. |
404 |
original_unavailable |
Metadata exists but original cannot be served. | Skip asset or report with Request-ID. |
409 |
download_already_active |
Another original transfer is in progress for the account. | Wait for the active transfer; retry with backoff. |
402 |
subscription_required / subscription_past_due |
Plan not active. | Resolve billing before minting tokens. |
429 |
rate_limit_exceeded |
Too many authenticated requests this minute. | Honor Retry-After; space out mint calls. |
Complete catalog of codes: Error reference.
Resolve IDs, dimensions, and extensions before minting download tokens.
Shared 30 req/min budget, retry strategy, and full error table.
Key storage, portal session rules, and subscription lifecycle.
Broader multi-language samples across catalog endpoints.