API reference
A REST API over JSON. Available on Pro and above. Base URL: https://app.example.com. Every path below also answers under /api/v1/….
Authentication
Create a key in Settings → API keys. Send it as a bearer token. Keys are workspace-scoped: the workspace is implied by the key, so there is no account id to pass.
curl -H "Authorization: Bearer isk_xxxxx_yyyyy" https://app.example.com/api/v1/jobs
The three-call flow
Create a job, PUT each image into it, then queue it. Uploading one file per request means per-file progress and per-file retries, with no multipart parsing on either side.
#1 create
JOB=$(curl -s -X POST $BASE/api/v1/jobs \
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{"name":"Solar client — August","params":{
"location":"Gulberg, Lahore, Pakistan",
"keywords":["solar installation","net metering"],
"businessName":"Nexablack Solar",
"businessDescription":"We install rooftop solar for homes and factories.",
"purpose":"gbp"}}' | jq -r .job.id)
#2 upload (repeat per image)
curl -X PUT "$BASE/api/v1/jobs/$JOB/files/roof-install-01.jpg" \
-H "Authorization: Bearer $KEY" --data-binary @roof-install-01.jpg
#3 queue
curl -X POST $BASE/api/v1/jobs/$JOB/queue \
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" -d '{}'
Jobs
/api/v1/jobs — create a draft. Body: { name, brandId?, params, aiMode?, provider?, model? }./api/v1/jobs?limit=25&status=done — list./api/v1/jobs/:id — the job plus every image and its metadata./api/v1/jobs/:id/queue — reserve allowance and start. Fails with 402 if the plan has no images left./api/v1/jobs/:id/cancel — stop; finished images are kept and counted./api/v1/jobs/:id/retry — re-queue only the failed images./api/v1/jobs/:id — delete the job and its files.Uploads
/api/v1/jobs/:id/files/<filename> — raw image bytes as the body. JPEG, PNG, WebP, AVIF, TIFF, HEIC. The type is checked from the file's magic bytes, not its name./api/v1/jobs/:id/files — what has been uploaded so far.Results and downloads
/api/v1/jobs/:id/download — everything as a ZIP. ?only=images skips the CSV/HTML/JSON./api/v1/jobs/:id/sheet — the upload CSV on its own./api/v1/jobs/:id/file/<name> — one finished file.A finished item looks like this:
{
"sourceName": "IMG_4821.jpg",
"outputName": "rooftop-solar-installation-gulberg-lahore.jpg",
"width": 2048, "height": 1365,
"sourceBytes": 4218342, "outputBytes": 288104, "savedPercent": 93,
"metadata": {
"metaTitle": "Rooftop Solar Installation in Gulberg, Lahore",
"metaDescription": "A 12 kW rooftop solar array installed on a home in Gulberg…",
"altText": "Technician mounting solar panels on a rooftop in Gulberg, Lahore",
"caption": "Our crew commissioning a 12 kW system in Gulberg.",
"tags": ["solar installation", "rooftop solar", "lahore"],
"latitude": 31.5204, "longitude": 74.3587
}
}
Webhooks
Set params.webhookUrl when creating a job and we POST to it when the job finishes:
{ "event": "job.completed", "job": { "id": "job_…", "status": "done", "doneCount": 42 }, "items": [ … ] }
One attempt, 15-second timeout; the delivery status is stored on the job. For live progress inside a browser use the SSE stream at /api/events.
Job parameters
{
"location": "Gulberg, Lahore, Pakistan",
"locations": [ { "label": "DHA Phase 5", "latitude": 31.47, "longitude": 74.40 } ],
"locationOrder": "shuffle", // or "random"
"keywords": ["solar installation"],
"businessName": "Nexablack Solar",
"businessDescription": "…",
"websiteUrl": "https://example.com",
"purpose": "gbp", // gbp | website | ecommerce | social | print | general | custom
"pageType": "service-detail",
"section": "hero",
"language": "English",
"brief": { "subject": "Site photos from a 12 kW rooftop install…" },
"imageNotes": { "IMG_4821.jpg": "This is the inverter, not a meter box." },
"spec": {
"format": "webp", "quality": 82, "maxKB": 300,
"width": 1920, "height": 1080, "variants": [640, 1024, 1600],
"namePattern": "{slug}-{section}", "metadataPolicy": "full", "keepGps": true
},
"webhookUrl": "https://n8n.example.com/webhook/images"
}
Any field of spec overrides the preset chosen by purpose/page/section. Call POST /api/v1/spec with the same purpose/page/section to see the resolved spec before you run anything.
Errors and limits
{ "error": { "code": "quota_exceeded", "message": "This job needs 40 images but only 12 are left…",
"details": { "required": 40, "remaining": 12, "resetsAt": 1790430588623 } } }
401 unauthenticated— missing or revoked key.402 quota_exceeded— the plan's image allowance is spent.403 forbidden— the key lacks a scope, or the subscription is past due.413 payload_too_large— the image is over the per-file limit.429 rate_limited— back off; the message says for how long.
n8n / Make recipe
- HTTP Request — POST
/api/v1/jobs, savejob.id. - Loop over your binary files — PUT each to
/api/v1/jobs/{{id}}/files/{{filename}}. - HTTP Request — POST
/api/v1/jobs/{{id}}/queue. - Webhook node — receives
job.completedwith every image's metadata, ready to write into WordPress, Shopify or a sheet.