CyberStock API
Programmatic access to the CyberStock AI Engine for stock photo and video metadata. Titles, descriptions, keywords, and Selling Score in one API call, powered by real buyer searches.
CYBERSTOCK_API_BASE.export CYBERSTOCK_API_BASE="https://<your-api-base>/functions/v1/api-gateway"Overview
The CyberStock API lets you process images and videos and get stock-ready metadata: titles, descriptions, keywords, categories, and Selling Score. It uses the same AI Engine as the CyberStock web app.
Authentication
All API requests require an API key sent via the X-API-Key header.
- Sign in to CyberStock and go to your Account panel
- Click the API tab
- Click Generate Key. Your key starts with
cs_live_ - Copy and save the key securely. It's shown only once
X-API-Key: cs_live_YOUR_API_KEY_HEREEndpoints
/v1/processProcess an image or video/v1/accountGet account info and credits/v1/sessionsList your API sessions/v1/sessions/:idGet results for a session or a batch/v1/batchesCreate a CyberBatch (SFTP credentials)/v1/batches/:id/startStart a CyberBatch with settings/v1/batchesList batches (filter by sftp_username, status)/v1/batches/:idBatch status and billingPOST /v1/process
Process a single image or video file and receive metadata generated by the CyberStock AI Engine. Provide the file either as a public URL or base64-encoded data.
| Parameter | Type | Description |
|---|---|---|
image_urlrequired | string | Public URL of the image or video to process. Provide this or image_base64. |
image_base64 | string | Base64-encoded file data. Use when the file isn't publicly accessible. Limit: 250,000 base64 characters (for larger files, use image_url). |
filename | string | Original filename with extension (e.g., "beach.jpg"). Helps AI detect media type. |
language | string | Output language code. Default: "en". Examples: "es", "de", "fr", "ja". |
keywords_limit | number | Maximum number of keywords to return. Default: 50. Range: 1-50. |
keyword_count | number | Exact number of keywords to generate (must be ≤ keywords_limit). Omit to let the engine choose, up to the limit. |
keyword_mode | string | "single" (default) for single-word keywords, or "long-tail" for multi-word phrases. |
platform | string | Target stock platform. Options: "shutterstock", "adobe", "alamy", "getty", etc. |
media_type | string | Override media type detection: "image", "video", or "vector". |
max_description_chars | number | Maximum characters for the description. Default: 480. |
max_title_chars | number | Maximum characters for the title. Default: 200. |
editorial | boolean | Enable editorial mode for news/documentary content. Default: false. |
editorial_seed | object | Editorial context: {"city":"NYC","country":"US","date":"2024-01-15"} |
single_word_only | boolean | Force strictly single-word keywords (no compounds). Default: false. |
allow_compound_keywords | boolean | Allow multi-word compound keywords. Default: false. |
title_case | boolean | Output title in Title Case (capitalize each major word). Default: false (sentence case). |
title_all_caps | boolean | Output title in ALL CAPS. Default: false. |
{
"ok": true,
"session_id": "sess_12345",
"image_id": "img_12345",
"status": "ready",
"metadata": {
"title": "Golden sunset over tropical beach with palm trees",
"description": "Stunning golden hour sunset illuminating a pristine tropical beach lined with swaying palm trees, calm turquoise waters reflecting warm amber light, creating a serene and idyllic paradise scene perfect for travel and vacation concepts.",
"keywords": [
"sunset", "beach", "tropical", "palm trees", "ocean",
"golden hour", "paradise", "vacation", "travel", "seascape",
"turquoise", "calm", "serene", "idyllic", "coastline"
],
"category": "Nature/Landscapes",
"media_type": "image"
},
"scores": {
"selling_score": 87,
"title_score": 92,
"description_score": 89,
"keyword_score": 85
},
"credits": {
"used": true,
"remaining": 742
},
"processing": {
"duration_ms": 1247,
"retryable": false
}
}On Unlimited plans the response also carries a fair_use object.
Code Examples
export CYBERSTOCK_API_BASE="https://<your-api-base>/functions/v1/api-gateway"
curl -X POST "$CYBERSTOCK_API_BASE/v1/process" \
-H "X-API-Key: cs_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"image_url": "https://example.com/photo.jpg",
"filename": "sunset-beach.jpg",
"language": "en",
"keywords_limit": 50,
"keyword_mode": "single"
}'import requests
import concurrent.futures
API_KEY = "cs_live_YOUR_KEY"
API_BASE = "https://<your-api-base>/functions/v1/api-gateway"
API_URL = f"{API_BASE}/v1/process"
files = [
{"url": "https://example.com/photo1.jpg", "name": "photo1.jpg"},
{"url": "https://example.com/photo2.jpg", "name": "photo2.jpg"},
{"url": "https://example.com/video1.mp4", "name": "video1.mp4"},
]
def process_file(file):
resp = requests.post(API_URL, json={
"image_url": file["url"],
"filename": file["name"],
"language": "en",
"keywords_limit": 50,
}, headers={"X-API-Key": API_KEY})
return resp.json()
# Process up to 5 files concurrently
with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
results = list(executor.map(process_file, files))
for r in results:
if r.get("ok"):
print(f"{r['metadata']['title']}")GET /v1/account
Check your current plan, credit balance, and account status.
curl "$CYBERSTOCK_API_BASE/v1/account" \
-H "X-API-Key: cs_live_YOUR_KEY"{
"ok": true,
"account": {
"email": "user@example.com",
"plan": "pro",
"api_access": true,
"api_tier": "pro",
"credits": 742,
"credits_subscription": 500,
"credits_topup": 242,
"is_unlimited": false
}
}GET /v1/sessions
List your recent API processing sessions. Supports pagination via limit and offset (or page).
curl "$CYBERSTOCK_API_BASE/v1/sessions?limit=100&offset=0" \
-H "X-API-Key: cs_live_YOUR_KEY"GET /v1/sessions/:id
Get processed results for a specific session, including full metadata for each file. Supports pagination for large sessions (CyberBatch sessions can hold up to 1 000 000 files).
# First page (default limit=100, max=2000)
curl "$CYBERSTOCK_API_BASE/v1/sessions/<SESSION_ID>?limit=1000&offset=0" \
-H "X-API-Key: cs_live_YOUR_KEY"
# Loop until pagination.has_more === false
curl "$CYBERSTOCK_API_BASE/v1/sessions/<SESSION_ID>?limit=1000&offset=1000" \
-H "X-API-Key: cs_live_YOUR_KEY"{
"ok": true,
"session": { "id": "...", "status": "completed", "total_images": 10219 },
"pagination": {
"total": 10219,
"offset": 0,
"limit": 1000,
"returned": 1000,
"has_more": true,
"next_offset": 1000
},
"results": [ /* 1000 items */ ]
}| Parameter | Type | Default / Max | Description |
|---|---|---|---|
limit | int | 100 / 2000 | Rows to return per call. Values above the ceiling are silently capped. |
offset | int | 0 / no cap | Number of rows to skip. Use pagination.next_offset from the previous response. |
page | int | 1 | 1-indexed alternative to offset. Ignored when offset is set. |
CyberBatch API (1,000+ files)
For large jobs, CyberBatch processes a whole folder in one go instead of one request per file. You upload over SFTP, start the batch with the same settings you send to /v1/process, and get a webhook when it is done. Results come back through the same GET /v1/sessions/:id endpoint.
Billing. On Unlimited, files first use your plan's fair-use allowance, then your credits at the 15% discount, then the fair-use slow lane, exactly like single-file processing. An Unlimited batch always starts. On other plans a batch runs on credits. Files that fail are refunded automatically.
Creates a batch and returns SFTP credentials. The password is shown once, and access is valid for 72 hours (see sftp_access_expires_at).
curl -X POST "$CYBERSTOCK_API_BASE/v1/batches" \
-H "X-API-Key: cs_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{ "name": "Shoot 2026-09-27" }'{
"ok": true,
"batch_id": "6f1c…",
"sftp": {
"host": "<sftp host>",
"port": 2022,
"protocol": "sftp",
"username": "cb_vqenvKAO0Cc",
"password": "shown once — store it now",
"remote_dir": "/"
},
"sftp_access_expires_at": "2026-09-30T10:00:00Z",
"min_files": 1000,
"next": "Upload your files over SFTP, then POST /v1/batches/6f1c…/start"
}Starts the batch once your files are uploaded. The body takes the same generation parameters as /v1/process (language, keywords_limit, keyword_count, keyword_mode, platform, max_title_chars, title_case and the rest). Anything you omit uses the same default as /v1/process. Optional: custom_prompt, and notify_email (an address, or false for no email).
curl -X POST "$CYBERSTOCK_API_BASE/v1/batches/<BATCH_ID>/start" \
-H "X-API-Key: cs_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"language": "en",
"keyword_count": 50,
"platform": "adobe"
}'Returns 202 with the batch object. Returns 400 BATCH_TOO_SMALL if fewer than 1,000 files are registered, and 409 BATCH_ALREADY_STARTED on a second start.
Status, file counts and billing split. status is one of awaiting_start, uploading, queued, submitted, running, aggregating, ready, partial, failed, canceled.
Before the batch is started, this call registers whatever has finished arriving over SFTP, so files.registered is current right after your transfer (refreshed at most every 20 seconds). A file still being written is counted once its transfer completes, so poll until the number stops changing before you call /start; /start also registers everything that has landed before it checks the minimum. A batch is finished when status is ready, partial or failed and files.queued and files.processing are both 0.
{
"ok": true,
"batch": {
"batch_id": "6f1c…",
"job_id": "a93e…",
"status": "running",
"name": "Shoot 2026-09-27",
"sftp_username": "cb_vqenvKAO0Cc",
"files": { "registered": 12000, "processed": 8200, "failed": 0,
"queued": 3800, "processing": 0, "awaiting_preview": 0 },
"billing": {
"discount_pct": 15,
"files_from_allowance": 2000,
"files_on_credits": 10000,
"files_on_fair_use_slow_lane": 0,
"credits_charged": 8500,
"credits_refunded": 0
},
"started_at": "2026-09-27T10:12:00Z",
"completed_at": null,
"results_path": "/v1/sessions/6f1c…"
}
}Lists your batches, newest first, paginated with limit / offset. Filter with sftp_username to find the batch behind an SFTP account, or with status. Batches are not included in GET /v1/sessions, which lists single-file API sessions.
curl "$CYBERSTOCK_API_BASE/v1/batches?sftp_username=cb_vqenvKAO0Cc" \
-H "X-API-Key: cs_live_YOUR_KEY"Webhooks
Set a webhook URL once on the CyberBatch page in the app (Completion webhook card). You get a signing secret and can send a test ping. Every batch, started from the app or the API, then posts batch.completed to your URL when it finishes.
POST https://your-server.example.com/cyberstock
Content-Type: application/json
CyberStock-Event: batch.completed
CyberStock-Delivery: 2b7d… (stable across retries: dedupe on it)
CyberStock-Signature: t=1790000000,v1=5f2a…
{
"id": "2b7d…",
"type": "batch.completed",
"created": "2026-09-27T14:03:11Z",
"data": {
"session_id": "6f1c…",
"job_id": "a93e…",
"status": "ready",
"total_files": 12000,
"processed_files": 12000,
"failed_files": 0,
"sftp_username": "cb_vqenvKAO0Cc",
"session_url": "https://cyberstock.lol/sessions/6f1c…/review-edit",
"results_path": "/v1/sessions/6f1c…"
}
}CyberStock-Signature is t=<unix time>,v1=<hex>, where v1 is HMAC-SHA256 of "<t>.<raw body>" with your secret. Verify it against the raw body, before parsing. Reply with any 2xx to acknowledge. Anything else is retried with backoff for about 22 hours.
import hashlib, hmac
def verify(raw_body: bytes, signature_header: str, secret: str) -> bool:
parts = dict(p.split("=", 1) for p in signature_header.split(","))
signed = f"{parts['t']}.".encode() + raw_body
expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, parts["v1"])Fair Use (Unlimited)
On Unlimited plans, /v1/process responses (including 429) and batch start responses carry a fair_use object, so an automation can tell when to top up without watching the app.
"fair_use": {
"state": "approaching",
"message": "You are approaching the fair-use threshold of your plan. To keep full speed, credit packs are available.",
"buy_credits_url": "https://cyberstock.lol/pricing"
}| state | Meaning |
|---|---|
ok | Full speed, nothing to do. |
approaching | Close to the fair-use threshold. Still full speed. |
credits | Past the threshold, running at full speed on your credits. |
slowed | Past the threshold with no credits: reduced speed. Single-file calls get 429 with retry_after_ms. |
Full Automation Guide
- Create an API key in Account → API, and set a webhook URL on the CyberBatch page.
POST /v1/batchesand storebatch_idand the SFTP password.- Upload 1,000 or more files over SFTP.
POST /v1/batches/:id/startwith your generation settings.- Wait for
batch.completed(or pollGET /v1/batches/:idevery few minutes). - Page through
GET /v1/sessions/:idfor every file's title, description and keywords.
"""Full CyberBatch automation: create -> SFTP upload -> start -> wait -> results."""
import os, time, requests, paramiko
API = os.environ["CYBERSTOCK_API_BASE"]
H = {"X-API-Key": os.environ["CYBERSTOCK_API_KEY"]}
FOLDER = "/path/to/images" # 1 000+ JPG / PNG / TIFF / MP4 files
# 1. Create a batch. The SFTP password is returned once.
b = requests.post(f"{API}/v1/batches", json={"name": "Nightly upload"}, headers=H).json()
batch_id, s = b["batch_id"], b["sftp"]
# 2. Upload over SFTP.
t = paramiko.Transport((s["host"], s["port"]))
t.connect(username=s["username"], password=s["password"])
sftp = paramiko.SFTPClient.from_transport(t)
for name in os.listdir(FOLDER):
sftp.put(os.path.join(FOLDER, name), f"{s['remote_dir'].rstrip('/')}/{name}")
sftp.close(); t.close()
# 3. Start with the same settings you would send to /v1/process.
r = requests.post(f"{API}/v1/batches/{batch_id}/start",
json={"language": "en", "keyword_count": 50}, headers=H)
if r.status_code == 400 and r.json()["error"] == "BATCH_TOO_SMALL":
raise SystemExit(r.json()["message"]) # fewer than 1 000 files registered
r.raise_for_status()
# 4. Wait. With a webhook configured you can skip this loop entirely and
# fetch results when batch.completed arrives.
while True:
st = requests.get(f"{API}/v1/batches/{batch_id}", headers=H).json()["batch"]
if st["status"] in ("ready", "partial", "failed", "canceled"):
break
time.sleep(300)
# 5. Results, paginated.
offset, results = 0, []
while True:
page = requests.get(f"{API}/v1/sessions/{batch_id}",
params={"limit": 1000, "offset": offset}, headers=H).json()
results += page["results"]
if not page["pagination"]["has_more"]:
break
offset = page["pagination"]["next_offset"]
print(len(results), "files keyworded")Keep /v1/process for small or urgent jobs. It returns metadata in about a second per file, while a batch completes within 24 hours, usually much sooner.
Rate Limits
Rate limits depend on your subscription plan:
| Plan | Requests / Minute | Max API Keys |
|---|---|---|
| Pro / 60k Top-Up | 60 | 3 |
| Studio / 120k Top-Up | 120 | 3 |
| Unlimited | 300 | 3 |
When you exceed the limit, the API returns 429 RATE_LIMIT_EXCEEDED. Wait a few seconds and retry.
Error Reference
| Status | Error Code | Description |
|---|---|---|
| 401 | UNAUTHORIZED | Missing or invalid API key |
| 401 | INVALID_API_KEY | API key is revoked or doesn't exist |
| 402 | INSUFFICIENT_CREDITS | Not enough credits to process |
| 403 | API_ACCESS_DENIED | Plan doesn't include API access (need Pro+) |
| 403 | SCOPE_DENIED | API key doesn't have required scope |
| 429 | RATE_LIMIT_EXCEEDED | Too many requests per minute |
| 400 | MISSING_INPUT | No image_url or image_base64 provided |
| 400 | INVALID_JSON | Request body is not valid JSON |
| 502 | PROCESSING_FAILED | Internal processing error (retry) |
| 429 | RATE_LIMITED | Fair-use speed limit (Unlimited): wait retry_after_ms, or add credits |
| 400 | BATCH_TOO_SMALL | Fewer than 1,000 files registered for the batch |
| 404 | BATCH_NOT_FOUND | Batch doesn't exist or belongs to another account |
| 409 | BATCH_ALREADY_STARTED | The batch was already started |
Ready to get started?
Generate your API key and start processing files in minutes.