Bigjpg API Tutorial: Batch Upscale Images with Python
A complete, runnable script that uploads images, creates enlargement tasks, polls for results and downloads them — plus the concurrency and error handling that keeps a 500-image job from failing halfway.
The web interface is fine for a handful of images. For a folder of five hundred, you want the API: four endpoints, no browser, and a script you can re-run. This is the version we would hand to a colleague — upload, create, poll, download, with the failure handling that matters in practice.
Before you start
- API access is a paid-account feature. Your key is on the API reference page once you are logged in with an active plan.
- Python 3 with
requestsinstalled. Nothing else. - Enlargement runs asynchronously on our workers, so the script creates tasks, then polls. A 500-image job takes roughly as long as the slowest queue you are in, not the sum of the images.
The script
import json
import os
import time
import requests
API = "https://bigjpg.com"
KEY = "YOUR_API_KEY"
STYLE = "art" # "art" for illustration, "photo" for photographs
NOISE = "1" # -1 none, 0 low, 1 medium, 2 high, 3 highest
X2 = "2" # 1 = 2x, 2 = 4x, 3 = 8x, 4 = 16x
POLL_SECONDS = 8
MAX_RETRY = 1
HEADERS = {"X-API-KEY": KEY}
def upload(path):
"""Get a presigned URL, PUT the file, return its public URL."""
name = path.split("/")[-1]
r = requests.post(
API + "/api/img_upload",
headers=HEADERS,
data={"filename": name, "filetype": "image/jpeg"},
timeout=60,
)
info = r.json()
if info.get("status") != "ok":
raise RuntimeError("upload failed: " + str(info))
with open(path, "rb") as image:
r = requests.put(
info["url"],
data=image,
headers={"Content-Type": "image/jpeg"},
timeout=300,
)
if r.status_code != 200:
raise RuntimeError("upload failed with HTTP %s" % r.status_code)
return info["fileurl"]
def create(image_url):
"""Create an enlargement task, return its task id."""
payload = {
"style": STYLE,
"noise": NOISE,
"x2": X2,
"input": image_url,
}
r = requests.post(
API + "/api/task/",
headers=HEADERS,
data=json.dumps(payload),
timeout=60,
)
data = r.json()
if "tid" not in data:
raise RuntimeError("task failed: " + str(data))
return data["tid"]
def poll(tids):
"""Query up to 100 task ids at once. Returns task id -> result dict."""
r = requests.get(API + "/api/task/" + ",".join(tids), timeout=60)
return r.json()
def download(url, out):
"""Download a finished image, checking the transfer really succeeded."""
r = requests.get(url, timeout=300)
if r.status_code != 200:
raise RuntimeError("download failed with HTTP %s" % r.status_code)
with open(out, "wb") as f:
f.write(r.content)
return out
def main(paths):
pending = {}
attempts = {}
for path in paths:
try:
tid = create(upload(path))
except Exception as e:
print("skipped", path, e)
continue
pending[tid] = path
attempts[tid] = 0
print("queued", path, tid)
while pending:
for tid, info in poll(list(pending)[:100]).items():
if info.get("status") == "success":
out = os.path.splitext(pending[tid])[0] + "_bigjpg.jpg"
try:
download(info["url"], out)
except Exception as e:
print("download failed", pending[tid], e)
pending.pop(tid)
continue
print("done", pending.pop(tid), "->", out)
elif info.get("status") == "error":
# One retry, then give up on this file.
attempts[tid] += 1
if attempts[tid] <= MAX_RETRY:
requests.post(API + "/api/task/" + tid, headers=HEADERS, timeout=60)
print("retrying", pending[tid])
else:
print("giving up on", pending.pop(tid))
if pending:
time.sleep(POLL_SECONDS)
if __name__ == "__main__":
import sys
main(sys.argv[1:])
What each call does
| Call | Purpose | Notes |
|---|---|---|
| POST /api/img_upload | Returns a presigned upload URL and the public image URL | PUT the file with the same Content-Type you declared, then use fileurl as the task input |
| POST /api/task/ | Creates an enlargement task | Returns tid, an estimated minute, and remaining_api_calls |
| GET /api/task/{ids} | Status of one or many tasks | Comma-separated ids, up to 100 per request |
| POST /api/task/{ids} | Retries failed tasks | Only tasks in the error state move back to the queue |
Concurrency and manners
- Poll less often than you think. Enlargement is not instant; 5–10 seconds between polls is plenty, and querying 100 ids in one call keeps the request count low.
- Cap your uploads. Two to four uploads in flight is a reasonable ceiling for a single machine. If you need more throughput, run more instances rather than more threads per instance.
- Retry with backoff. A network error is not a task failure. Retry the HTTP call up to three times with a growing delay before you mark a file as broken.
- Watch
remaining_api_calls. Every created task reports it; stop the loop at some low threshold instead of discovering the limit from a wall of errors. - Log the mapping. Keep a file with
tid, input name, settings and output name. When a job runs overnight, that log is the only way to know what finished.
Errors you will actually hit
valid_api_key_required— the header is missing or unknown. Check the header name and that the key belongs to a plan that has not expired.requires_vip— free or expired account. The API is for active paid plans.month_limit— the monthly quota is used up. Each task reports the remainder, so you can stop before this happens.param_error—stylemust be art or photo,noise-1 to 3,x21 to 4.input_url_error— the input has to be a publicly reachable http or https image URL. Presigned private URLs expire; upload first, then pass the returned public URL.
What a batch costs
One image counts as one credit at 2x, 4x and 8x; 16x counts as two. So a 500-image job is 500 credits at 4x, and the same job at 16x is 1000. Since 4x is the right factor for most photographs anyway, the cheap option is also the one that looks better — see the factor guide for the reasoning.
The full endpoint reference, including parameters and every error status, is on the API page. If something in this script does not match what you get back, that page is the version we keep current.