CPU Tools
FOTOhub runs a large family of file tools that need no AI model and no GPU: PDF merge, split, compress and conversion, image and video conversion, audio cutting and joining, QR codes and barcodes, print preparation, photo development, subtitles and translation, publishing, e-signature and more. They are cheap, billed per unit of work, and several have a daily free allowance.
There are two ways in:
| Surface | Paths | Inputs | Best for |
|---|---|---|---|
| Simple file tools | /v1/documents/*, /v1/convert/*, /v1/codes/*, /v1/files/* | Opaque file ids from POST /v1/files/upload-url | The everyday conversions: start here |
| Full tool family | /v1/cpu/* | Storage references from POST /v1/cpu/inputs/upload-url | Everything else (about 200 routes) |
Base URL: https://apis.fotohub.appAuthentication: Authorization: Bearer fh_live_your_api_key (see Authentication)
Prices
Prices are per unit of work (pages, minutes, files, codes) and are not fixed in this page. Every submit returns the quote it was charged at, the simple tools have free quote routes, and failed jobs are refunded automatically. API keys pay from the prepaid USD wallet.
Simple file tools
Endpoints
| Method | Path | Charged | Description |
|---|---|---|---|
| POST | /v1/files/upload-url | no | A one-time upload link and a file id |
| PUT | /v1/files/upload/{token} | no | Send the file's bytes (the link is the credential) |
| GET, HEAD | /v1/files/download/{token} | no | Download a result (the link is the credential) |
| POST | /v1/documents/merge | yes | Merge 2-20 PDFs |
| POST | /v1/documents/split | yes | Split one PDF |
| POST | /v1/documents/compress | yes | Compress one PDF |
| POST | /v1/documents/convert | yes | Office to PDF, or PDF to Word / PowerPoint / Excel |
| POST | /v1/documents/quote | no | Price of a documents operation |
| GET | /v1/documents/jobs/{job_id} | no | A documents job |
| POST | /v1/convert/image | yes | Convert or compress 1-64 images |
| POST | /v1/convert/video | yes | Convert, compress, shrink to a target size, or make a GIF |
| POST | /v1/convert/audio | yes | Convert, cut or join audio |
| POST | /v1/convert/quote | no | Price of a convert operation |
| GET | /v1/convert/jobs/{job_id} | no | A convert job |
| POST | /v1/codes/qr | yes | QR codes in bulk, QR label sheets, barcodes |
| POST | /v1/codes/quote | no | Price of a codes operation |
| GET | /v1/codes/jobs/{job_id} | no | A codes job |
Keys. These routes take API keys only: a browser session or a storage bucket key gets 401 {"detail": {"error": "api_key_required", "message": "Use an API key (fh_live_...)."}}. Submits, quotes and upload-url need a key with write access; a read-only key gets 403 with {"detail": "This API key is read-only. Use a key created with write access, or the dashboard, to perform this action."}. Job reads work with any key.
Rate limits per API key, per minute: 30 submits and upload links (one shared bucket), 120 quotes, 240 job reads. Over the limit: 429 rate_limited, Retry-After: 60, Too many requests for this key. You were not charged.
1. Upload a file
curl -X POST https://apis.fotohub.app/v1/files/upload-url \
-H "Authorization: Bearer fh_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{"name": "contract.pdf", "size": 1843221}'| Field | Type | Description |
|---|---|---|
name | string 1-200 | The file name; its extension decides the file kind |
size | integer > 0 | Bytes. At most 50 MiB (52,428,800) through the API |
{
"file": {"id": "file_...", "name": "contract.pdf", "size": 1843221},
"upload": {"url": "https://apis.fotohub.app/v1/files/upload/...", "method": "PUT", "expires_at": 1760002800, "max_bytes": 52428800}
}Then PUT the raw bytes to upload.url before expires_at (Unix seconds, one to two hours ahead). Content-Length is required; the body may not be larger than the declared size. Answer: {"uploaded": true, "bytes": 1843221}.
| Upload error | HTTP |
|---|---|
file_too_large (max_bytes): A <kind> file can be at most 50 MB (52428800 bytes) through the API. | 413 |
too_many_pending_uploads (cap_urls, pending_urls, window_s, retry_after_s), upload_quota_exceeded (cap_bytes, outstanding_bytes, retry_after_s), upload_busy | 429 |
upload_unavailable, temporarily_unavailable | 503 |
PUT: file_not_found 404, link_expired 410, length_required 411, file_too_large 413, length_mismatch 400, upload_failed 502 |
Pass the file to a tool as {"id", "name", "size", "content_type"} in files (size is required; name and content_type are optional, up to 255 characters). A file id is bound to your account: another account's id is 404 file_not_found. Uploaded files that no job uses are deleted after 24 hours, and a job's inputs one hour after it finishes. Besides the 50 MiB API cap, each tool has its own input cap (for example 25 MiB for an Office file, 2 MiB for a CSV of codes).
2. Submit
Every submit is asynchronous and answers 200 with the job and its quote:
{
"job": {"id": "a3e1c7d2-...", "type": "document.job", "operation": "merge", "status": "queued", "expires_at": "2026-10-10T09:00:00Z"},
"created": true,
"quote": {"credits": 0, "units": 1, "free": true, "free_remaining": 4, "free_daily": 5, "eta_s": 3}
}quote holds units, the price (credits, and credits_full when part of the job was covered by the free allowance), free, free_units, free_remaining, free_daily and eta_s. The price is computed from the server's own reading of your files (pages, duration, rows), never from numbers you send.
Idempotency. Send Idempotency-Key (or X-Idempotency-Key). A retry with the same key returns the first job with "created": false and the full job view, and is not charged again. The key is not compared with the body: within one tool, reusing a key returns the first job whatever you send.
3. Poll and download
GET /v1/{documents|convert|codes}/jobs/{job_id}:
{
"id": "a3e1c7d2-...",
"type": "document.job",
"operation": "merge",
"status": "succeeded",
"progress": 100,
"eta_s": null,
"charged_credits": 0,
"created_at": "...", "started_at": "...", "finished_at": "...", "expires_at": "...",
"refunded": false,
"error": null,
"result": {"assets": [{"name": "contract-merged.pdf", "content_type": "application/pdf", "bytes": 2210311, "url": "https://apis.fotohub.app/v1/files/download/..."}]}
}status:queued,running,succeeded,failed,cancelled.progressis 0-100.result.assets[].urlis valid for one hour; read the job again for fresh links. The download streams the file as an attachment (Content-Disposition), withCache-Control: private, no-store. Errors:404 file_not_found,410 link_expired,502 download_unavailable.- A job that is not yours, of another tool family, or not a UUID is
404 job_not_found. The job record lives 24 hours (expires_at). - A failed job carries
error: {code, message}and is refunded automatically (refunded: true, the message ends withYour credits were returned.). Codes includepassword_required,wrong_password,needs_ocr,page_limit,file_too_large,unsupported_format,corrupt_file,too_long,input_changed,exceeds_priced_limits,frame_rate_too_high,unsupported_or_unsafe,input_expired,input_rejected,queue_timeout,timeout,resource_limit; anything else readsThe job could not be completed.
Quotes
POST /v1/documents/quote, /v1/convert/quote and /v1/codes/quote take the submit body and answer the price without creating a job:
{"operation": "merge", "available": true, "quote": {"credits": 0.5, "units": 1, "free": false, "free_remaining": 0, "free_daily": 5, "eta_s": 3}, "charged": false}- Documents: add
"operation": "merge" | "split" | "compress" | "convert"(andtoforconvert;passwordis not accepted here). - Convert: add
"type": "image" | "video" | "audio". - Codes: the same body as
POST /v1/codes/qr.
available: false (with retry_after_s) means the queue cannot take the work right now. The quote reserves nothing: a submit is priced again from the files.
Documents
Merge, split, compress take {"files": [...], "options": {...}}. Merge takes 2-20 PDFs (Choose between 2 and 20 PDFs to merge.), split and compress exactly one (Choose one PDF.). Each PDF may have up to 2,000 pages (422 too_many_pages, max_pages); an unreadable file is 422 pdf_unreadable; a non-PDF content_type is 422 unsupported_type (This tool works on PDF files.).
| Operation | Options | Units |
|---|---|---|
merge | bookmarks (boolean, default false) | 1 per started 50 pages (all files together) |
split | mode: ranges (default, with ranges such as "1-3,5"), every (with every: pages per file), size (with max_mb 0.1-50) or bookmarks | 1 per started 50 pages |
compress | level: low, recommended (default) or extreme | 1 per started 100 pages |
options is at most 16 KiB of JSON; password fields are refused. An option value the tool cannot use fails the job (refunded).
Convert takes {"files": [one file], "to": "pdf" | "docx" | "pptx" | "xlsx", "password": "..."}:
to | Input | Units | Notes |
|---|---|---|---|
pdf | docx, doc, odt, rtf, xlsx, xls, ods, pptx, ppt, odp, txt (up to 25 MiB) | 1 per file | password is refused |
docx | One PDF, up to 50 pages | 1 per started 25 pages | password: the PDF's open password (up to 128 characters; never stored with the job) |
pptx, xlsx | One PDF, up to 50 pages | 1 | as above |
Job errors specific to conversion: password_required, wrong_password, needs_ocr (a scanned PDF without a text layer).
Convert
Images (POST /v1/convert/image): {"files": [1-64 images], "operation": "convert" | "compress", "format": "same" | "jpg" | "jpeg" | "png" | "webp" | "avif" | "heic" | "gif" | "tiff" | "ico", "options": {...}}. Options include quality (1-100), colour_profile (srgb | keep), animated (WebP / GIF output), background (hex colour), resize ({width, height, fit}, each side 1-8192), and for compress a compress object (mode: quality, target_kb with target_kb 1-102400, or ssim with ssim 0.8-0.999). Metadata is always stripped. One input gives one file; several give images.zip. Units: 1 per started 10 images.
Video (POST /v1/convert/video): {"files": [one video], "operation": "convert" | "compress" | "target_size" | "gif", "options": {...}}. Options are checked before any charge (422 invalid_options):
operation | Options |
|---|---|
convert | format (mp4 | webm), quality (low | medium | high), audio (keep | drop) |
compress | quality, max_height (0, 240, 360, 480, 720, 1080 or 1440), audio_kbps (48-192), audio |
target_size | target: discord_8, discord_25, whatsapp_16, email_20, email_25 or custom (default) with target_mb 0.5-500; max_height; audio |
gif | start_s (0-300), fps (5-30), width (120-1080), speed (0.25-4), ping_pong, reverse (not with ping_pong), duration_s (0.1-120; the GIF may last at most 30 s), loop (-1..100), dither (bayer | sierra | none), crop ({x, y, w, h}), caption_pos, optimise |
Limits: source up to 30 minutes (422 too_long, max_seconds) and 3840x2160 (422 too_large_picture); GIF source up to 300 s. Units: per started output minute, x2 above 1080p and x4 above 1440p; target_size counts twice (two passes); a GIF is 1 unit (2 when it is longer than 15 s).
Audio (POST /v1/convert/audio): {"files": [...], "operation": "convert" | "cut" | "join", "format": "wav" | "mp3" | "flac" | "m4a" | "aac" | "ogg" | "opus", "bitrate_k": 32-320, "sample_rate": 8000 | 11025 | 12000 | 16000 | 22050 | 24000 | 32000 | 44100 | 48000, "channels": 1 | 2, "options": {...}}.
operation | Files | Options | Units |
|---|---|---|---|
convert | 1 | none (default format mp3) | 1 per file |
cut | 1 | start_s, end_s (0-86400, at least one), fade_in_s, fade_out_s (0-30) | 1 per file |
join | 2-20 | crossfade_s or gap_s (0-10, not both), fade_in_s, fade_out_s | 1 per started 30 minutes in total |
Each file may last 30 minutes, a join 2 hours in total.
Codes
POST /v1/codes/qr:
| Field | Type | Default | Description |
|---|---|---|---|
operation | bulk | sheet | barcode | bulk | bulk: many QR codes; sheet: a printable label sheet (always PDF); barcode: barcodes |
symbology | qr, ean13, ean8, upca, code128, code39, itf, datamatrix, pdf417, aztec | qr (code128 for barcode) | bulk and sheet are QR only; barcode cannot be qr |
format | svg | png | pdf | svg | |
values | string[] (max 500, each 1-2048 bytes) | Send values or one CSV in files, not both | |
files | 0-1 CSV | Up to 2 MiB and 500 rows | |
template | string (max 4096) | The code content, with {column} placeholders over the CSV columns | |
filename_column, caption_column | string (max 128) | For inline values, only "data" | |
options | object | {} | Allowed keys: ec, size_mm, width_mm, height_mm, quiet, fg, bg, dpi, text, layout (for example {"ec": "Q", "size_mm": 30} or {"layout": {"preset": "avery_l7160"}}) |
Codes are static: nothing is shortened or tracked, and values / template are not stored with the job. One inline value gives one file; more, or any CSV job, gives qr-codes.zip / barcodes.zip; a sheet gives label-sheet.pdf. Units: 1 per started 100 codes.
Errors (simple tools)
All errors are {"detail": {...}} and state charged: false when nothing was taken:
| HTTP | error | Notes |
|---|---|---|
| 401 | api_key_required | A browser session or bucket key |
| 402 | insufficient_funds | required_usd, balance_usd, shortfall_usd, currency, topup_url |
| 403 | not_available | The tool is not enabled for your account; reason (tier_required adds min_tier) |
| 404 | file_not_found, job_not_found | |
| 409 | units_mismatch | A file changed after it was priced: ask for the price again |
| 413 | file_too_large | max_bytes |
| 422 | invalid_request | fields: [{field, problem}] for schema errors, or a message |
| 422 | unsupported_type, pdf_unreadable, too_many_pages, invalid_options, too_long, too_large_picture | |
| 429 | rate_limited, ceiling_exceeded | retry_after_s, Retry-After |
| 503 | not_available | The tool is switched off right now; Retry-After: 300 |
| 503 | temporarily_unavailable | No capacity now; retry_after_s. You were not charged |
Full tool family (/v1/cpu/*)
How every /v1/cpu/* tool works
Keys. Browser sessions and API keys both work. Every route that starts work or changes something needs a key with write access (403, This API key is read-only. Use a key created with write access, or the dashboard, to perform this action.); storage bucket keys are refused the same way. Reads (GET) work with read-only keys unless noted.
Availability. Each tool is enabled per account and some are rolled out in stages. A tool that is not enabled for you answers 403 with {"error": "feature_disabled", "reason": "..."} (reason: "tier_required" adds min_tier, the plan that unlocks it). A tool that is switched off or not yet priced answers 503 (not_priced or feature_off, This tool is not available yet. You were not charged., Retry-After: 300). Nothing is charged in either case.
Uploading inputs for CPU tools
/v1/cpu/* tools never fetch URLs. They take files from your FOTOhub storage as input_refs:
curl -X POST https://apis.fotohub.app/v1/cpu/inputs/upload-url \
-H "Authorization: Bearer fh_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{"name": "talk.mp4", "size": 734003200}'| Field | Type | Description |
|---|---|---|
name | string 1-200 | File name |
size | integer 1 to 4 GiB | Bytes |
{"path": "cpu-inputs/<your user id>/cputier/9f2c...-talk.mp4", "bucket": "cpu-inputs", "object": "<your user id>/cputier/9f2c...-talk.mp4", "upload_url": "https://s1.fotohub.app/storage/v1/object/upload/sign/...", "expires_at": 1760006400}Upload the bytes to upload_url before expires_at; the link creates one new object and never overwrites. POST /v1/cpu/inputs/confirm with {"object": "<object>"} (optional) answers {object, confirmed} and releases the pending-upload reservation. Limits: 60 pending upload links per hour and 20 GiB of unconsumed uploads (429 cpu_input_pending_urls_exceeded / cpu_input_quota_exceeded, with retry_after_s); 429 cpu_input_upload_busy (Retry-After: 2); 503 input_store_unavailable.
Then reference the file:
{"input_refs": [{"bucket": "cpu-inputs", "path": "cpu-inputs/<your user id>/cputier/9f2c...-talk.mp4", "name": "talk.mp4", "size": 734003200, "content_type": "video/mp4"}]}path is <bucket>/<your user id>/... and its first segment must equal bucket. Files already in your photos, videos or audio storage can be referenced the same way. A path in someone else's folder is 403 input_not_owned (One of the files is not yours.); a ref larger than the tool's cap is 413 input_too_large (max_bytes). At most 64 refs per job unless a tool says otherwise. size is a hint: the server measures the files itself (pages, duration, rows) and prices from its own measurement.
Submit and the job
Bodies are JSON and strict: an unknown field is 422. A submit answers 200:
{"job_id": "5b0e...", "quote": {"units": 2, "credits": 0.2, "usd": 0.002, "free": false, "free_remaining": 0, "free_daily": 5, "eta_s": 6, "available": true}, "status": "queued", "expires_at": "2026-10-10T09:00:00Z"}The numbers are an example. Work is charged at submit and refunded once, automatically, if the job fails or is cancelled before it starts. Many tools have a daily free allowance, shown in the quote as free_remaining / free_daily; a fully free job has free: true and costs nothing. The quote may carry more fields (ETA detail, internal identifiers): treat any field not listed here as opaque.
When the work cannot run right now the answer is still 200, with {"available": false, "reason": "...", "eta_s": ..., "quote": {...}, "charged": false}. Nothing is charged; retry later.
There is no separate quote route on most /v1/cpu/* tools: the price is the quote returned with the job, and a few tools expose a free unit or quota lookup (listed in the tables below). Idempotency-Key makes a retry return the original job with "duplicate": true and no second charge.
Follow the job with:
| Method | Path | Description |
|---|---|---|
| GET | /v1/cpu/jobs/{job_id} | One job |
| GET | /v1/cpu/jobs?status=&limit= | Your jobs, newest first (limit 1-100, default 50) |
| GET | /v1/cpu/jobs/{job_id}/events | Server-Sent Events: the whole job first, then changed fields; keep-alive every 15 s; ends at a terminal status or after 600 s |
| POST | /v1/cpu/jobs/{job_id}/cancel | Cancel. Refunded only if the job had not started (cancelled, refunded in the answer) |
The job: {id, status, progress, outputs: [{name, url, content_type, bytes}], report, error_code, credits_charged, units, created_at, started_at, finished_at, expires_at, refunded} (plus an identifier of the tool, which you should treat as opaque). status is queued, running, succeeded, failed, cancelled or refunded. Output links are signed and valid for one hour; read the job again for fresh ones. report holds tool-specific results (for example per-file savings, a diff, a list of findings). A failed job has an error_code, for example password_required, wrong_password, needs_ocr, page_limit, file_too_large, unsupported_format, corrupt_file, too_long, input_changed (a file changed after it was priced), exceeds_priced_limits, no_face_found, timeout, input_rejected or processing_failed.
Errors
All errors are {"detail": {...}}; refusals before any charge say You were not charged. and carry charged: false.
| HTTP | error | When |
|---|---|---|
| 402 | insufficient_funds | Wallet too low: required_usd, balance_usd, shortfall_usd, currency, topup_url |
| 403 | feature_disabled | Not enabled for your account (reason, min_tier) |
| 403 | input_not_owned | A file is not in your storage |
| 409 | units_mismatch | A file changed after pricing (units): ask for the price again |
| 413 | input_too_large, file_too_large, request_too_large | max_bytes |
| 422 | invalid_request, invalid_options, unsupported_type, unsupported_format, unsupported_conversion, pdf_unreadable, password_protected, too_many_pages, too_long, too_many_rows, too_large_picture, op_disabled, ... | The request or file cannot be processed; extra fields such as max_pages, max_seconds |
| 429 | ceiling_exceeded | A concurrency, rate or daily spending ceiling (retry_after_s, Retry-After); fair_use_exceeded and rate_limited on some free tools |
| 503 | not_priced, feature_off | The tool is not available now (Retry-After: 300) |
| 503 | cpu_tier_unavailable, feature_check_unavailable, feature_degraded | Temporarily unavailable; retry after Retry-After |
Schema errors from the request model itself are FastAPI's standard 422 list.
Video, GIF and media — /v1/cpu/media/*
Body: {"input_refs": [...], "op": "...", "options": {...}, "caption": "..."}. op is required; unknown option keys are 422 invalid_options (Unknown option for <op>: ...). caption is one line of up to 80 characters, for video_to_gif, overlay and social_resize only. The server measures each video; source videos may last 30 minutes and be up to 3840x2160 (422 too_long, too_large_picture).
| Route | op values | Units |
|---|---|---|
POST /v1/cpu/media/gif | video_to_gif, video_to_anim (animated WebP or silent WebM), gif_maker (2-64 images), gif_optimise, gif_resize, gif_crop, gif_reverse, gif_speed, gif_frames | 1 per GIF (2 when longer than 15 s; at most 30 s) |
POST /v1/cpu/media/tools | trim, merge (2-20 videos), mute, extract_audio, replace_audio, frames, contact_sheet, thumbnail (no re-encode) | 1 per 500 MB of video |
POST /v1/cpu/media/edit | trim_exact, merge_reencode, rotate, flip, crop (re-encode) | per output minute x resolution factor |
POST /v1/cpu/media/compress | compress, target_size, convert, speed, slowmo, loop, reverse, social_resize, overlay, burn_subs | per output minute x resolution factor (x2 for target_size) |
The resolution factor is 1 up to 1080p (short side), 2 up to 1440p and 4 above. Main options:
op | Options |
|---|---|
video_to_gif | start_s 0-300, fps 5-30, width 120-1080, speed 0.25-4, ping_pong, reverse (not both), duration_s 0.1-120 (GIF at most 30 s), loop -1..100, dither (bayer | sierra | none), crop {x, y, w, h}, caption_pos (top | middle | bottom), optimise |
video_to_anim | format (webp | webm), start_s, fps 1-30, width 64-1280, duration_s 0.1-30, loop, quality 1-100 |
gif_maker | width, fit (contain | cover), background, delays_ms (one per image, 20-10000) or delay_ms, loop, dither |
gif_optimise / gif_resize / gif_speed / gif_frames | lossy 0-200, colors 2-256, level 1-3 / width, height 16-1080 / speed 0.25-4 / every 1-100 |
trim, trim_exact | start_s, and end_s or duration_s |
extract_audio | format (m4a | mp3 | wav | flac) |
replace_audio | Files: video then audio. mode (replace | add), volume, orig_volume 0-2 |
frames | every_s 0.1-3600 or count 1-200, format (jpg | png), width 64-1920 (ZIP output) |
contact_sheet / thumbnail | count 4-48, cols 2-8, thumb_w 120-480, timestamps / at_s, format, width |
merge_reencode | width 64-3840 and height 64-2160 together, fps 10-60 |
rotate / flip / crop | angle 90|180|270 / direction (horizontal | vertical | both) / crop {x, y, w, h} |
compress | quality (low | medium | high), max_height (0, 240, 360, 480, 720, 1080, 1440), audio_kbps 48-192, audio (keep | drop) |
target_size | target (discord_8, discord_25, whatsapp_16, email_20, email_25, custom with target_mb 0.5-500), max_height, audio |
convert | format (mp4 | webm), quality, audio |
speed / slowmo / loop | factor 0.25-4 / factor 1.25-8 / count 2-20 |
social_resize | aspect (9:16, 1:1, 4:5, 16:9), mode (blur | crop | fit), res (720 | 1080), focus_x, focus_y 0-1 |
overlay | caption and/or logo: true (the last input is the logo, up to 25 MB): caption_pos, caption_color, caption_size_pct 2-12, logo_pos, logo_scale_pct 5-50, logo_margin_pct, logo_opacity 0.1-1, start_s, end_s |
burn_subs | Files: video then an SRT / WebVTT / ASS file (up to 2 MB). size (small | medium | large), position (bottom | top) |
PDF — /v1/cpu/pdf/*
The server counts the pages itself. Units are per started block of pages: edit 50 (merge: all files together; images_pdf: one per image), compress 100, render 20 (selected pages), finish 50, compare 50 (the longer file), ocr 10, forms 50, repair 50. Page ceilings: 2,000 per file for edit, compress, finish and repair; 300 for render and compare; 100 for ocr; 500 for forms.
POST /v1/cpu/pdf/edit — {"input_refs", "op", "options", "password", "user_password", "owner_password"} (password fields up to 128 characters, never stored):
op | Inputs | Options |
|---|---|---|
merge | 2-20 PDFs | bookmarks |
split | 1 PDF | mode: ranges (with ranges, e.g. "1-3,5"), every, size (max_mb 0.1-50), bookmarks |
extract, delete | 1 PDF | pages (required) |
rotate | 1 PDF | angle (90, 180, 270, -90), pages |
reorder / organise | 1 PDF | order (page numbers) / map ([{page, rotate}]) |
protect | 1 PDF | top-level user_password (required), owner_password; permissions {print, copy, modify, annotate} |
unlock | 1 PDF | top-level password |
watermark | 1 PDF (+ 1 image for kind: "image") | kind, layout (single | tile), position, opacity 0.05-1, rotation, pages, text (up to 120 chars), size, color, scale |
page_numbers | 1 PDF | position, format (n, n_of_total, page_n, page_n_of_total, dash_n), start, size, margin_mm, pages, skip_first, color |
crop | 1 PDF | mode: "margins" with margins (mm) or mode: "box" with box (fractions); pages |
resize / nup | 1 PDF | size (a4 | letter), orientation / per_sheet (2, 4, 6, 9), margin_mm, gap_mm |
images_pdf | 1-100 images | page_size (a4 | letter | fit), orientation, margin_mm |
metadata | 1 PDF | clear, title, author, subject, keywords |
| Route | Body / options |
|---|---|
POST /v1/cpu/pdf/compress | options.level: low, recommended (default), extreme. Never larger than the input |
POST /v1/cpu/pdf/render | op: render (default) or thumbs (128 px); options: format (png | jpg | webp), dpi 72-300 (150), pages |
POST /v1/cpu/pdf/finish | op: sign (placements: up to 50 stamps, an image input or text, positioned in page fractions), redact (rects and/or terms, case_sensitive; the result is checked and the job fails if text survives), flatten (annotations, forms), pdfa (PDF/A-2b) |
POST /v1/cpu/pdf/compare | Exactly 2 PDFs; options: dpi 50-150, sensitivity 0.01-0.5. Outputs a text diff (JSON) and a visual diff PDF |
POST /v1/cpu/pdf/ocr | {"input_refs": [1 PDF], "languages": ["pl", "en"]} (1-3 of pl, en, de). Outputs a searchable PDF |
POST /v1/cpu/pdf/forms | 1 PDF; op: detect (include_existing, min_confidence), create (fields: up to 5,000 of {kind: text|checkbox|radio|dropdown|date|signature, name, page, rect, ...}, remove), fill (values: name -> value, flatten), export, flatten. Field values are not stored with the job |
POST /v1/cpu/pdf/repair | {"input_refs": [1 PDF]}. A damaged file is priced from its size (at most 10 units) and the unused part is refunded; outputs the repaired PDF and a report. Nothing recoverable: unrecoverable, refunded |
Images — /v1/cpu/images/*
POST /v1/cpu/images/convert — {"input_refs": [1-64 images], "op", "format", "options"}. Units: 1 per started 10 images. Up to 120 megapixels per image; metadata is always stripped. One input gives one file, several give images.zip.
op | Options |
|---|---|
convert | format (same, jpg, jpeg, png, webp, avif, heic, gif, tiff, ico), quality 1-100, colour_profile (srgb | keep), animated, background, resize {width, height, fit}, ico_sizes |
compress | compress {mode: quality | target_kb | ssim, target_kb, ssim}; the report gives saved_percent per file |
rename | pattern (1-120 chars) with {name}, {n}, {n:03}, {date}, {w}, {h}, {ext} |
social | preset: instagram-square, instagram-portrait, instagram-story, facebook-post, x-post, linkedin-post, youtube-thumbnail, pinterest-pin, whatsapp-status, whatsapp-profile, favicon-32, favicon-180 |
rotate, flip, crop | angle / flip (h | v | both) / crop {x, y, width, height} or {aspect, gravity} |
circle, rounded, border, shadow | radius_px or radius_pct / border {width, color} / shadow {dx, dy, blur, color, opacity} (PNG or WebP output) |
icc, strip | Convert to sRGB / remove metadata |
svg_png, svg_optimise | svg_width 16-8192, svg_dpi 36-600 / none |
POST /v1/cpu/images/sets — op: favicon and app_icons (1 source image; app_name, theme_color, background_color), sprite (1-64 images; layout, padding, max_size, columns, prefix), og_card (0-1 logo; title required, subtitle, colours). 1 unit.
Documents — /v1/cpu/docs/*
| Route | Body | Units |
|---|---|---|
POST /v1/cpu/docs/office-pdf | {"input_refs": [1 file]}: docx, doc, odt, rtf, xlsx, xls, ods, pptx, ppt, odp, txt, up to 25 MB | 1 |
POST /v1/cpu/docs/pdf-office | {"input_refs": [1 PDF], "op": "docx" | "pptx" | "xlsx", "password"}; up to 50 pages | Word: 1 per started 25 pages; PowerPoint / Excel: per 50 |
POST /v1/cpu/docs/convert | {"input_refs": [1 file], "to": "pdf" | "epub" | "md" | "txt" | "docx" | "rtf", "title", "toc", "page_size", "csv_header", "csv_delimiter"} | 1 |
/v1/cpu/docs/convert detects the source from the file's bytes: Markdown -> pdf, epub, docx, rtf, txt; text -> epub, md, docx, rtf; RTF -> epub, md, txt, docx; CSV -> pdf, md; EPUB -> pdf, md, txt, docx; Apple Pages -> pdf, docx, epub, md, txt; PDF -> epub, md, txt. Any other pair is 422 unsupported_conversion with supported and, where another route fits, use. Limits: Markdown and text 5 MB, CSV and RTF 20 MB (CSV at most 10,000 rows and 100 columns), EPUB, Pages and PDF 50 MB (PDF at most 500 pages).
Bulk variants — /v1/cpu/bulk
Render one design scene once per row of a data file (CSV, XLSX or JSON, up to 10,000 rows) into one ZIP. For templates managed by the API, POST /v1/render/bulk is the simpler entry point; this route takes the files directly.
POST /v1/cpu/bulk:
| Field | Type | Default | Description |
|---|---|---|---|
input_refs | array | In order: the scene .json (up to 4 MB), the data file (up to 50 MB), then each asset in the order of assets | |
rows | integer 1-10,000 | Required. The number of rows you are paying for | |
feed_name | string | Required. The data file's name, ending in .csv, .xlsx or .json | |
mapping | object | Required. Which scene object and property each column fills (at most 32 KB) | |
bundle_hash | string (64 hex) | Required. From GET /v1/cpu/bulk/bundle-hash | |
format | png | jpeg | webp | pdf | png | |
scale | number 0.1-8 | 1.0 | |
transparent | boolean | false | png or webp only |
quality | integer 1-100 | 90 | |
assets | string[] (max 64) | [] | Unique plain file names |
image_hosts | string[] (max 20) | [] | Host names image cells may be fetched from |
pdf_merge | boolean | false | pdf only |
name_column | string 1-128 | The column that names each file | |
webhook_url | string (max 512) | Signed webhook when the job settles |
Billing is per started block of 100 rows, charged at submit; blocks that did not render are refunded when the job finishes (at least one block is kept), and a job where no row renders is refunded in full. A file with more rows than rows fails the job (input_rejected, refunded). Limits: 20 bulk jobs per hour (429 rate_limited, Retry-After: 600) and 3 running at once.
| Method | Path | Description |
|---|---|---|
| GET | /v1/cpu/bulk/{job_id} | The job, plus blocks: {charged, billed, refunded} and webhook delivery state |
| GET | /v1/cpu/bulk/{job_id}/download | {url, expires_in: 604800}: a 7-day link to the ZIP (409 not_ready before success) |
| GET | /v1/cpu/bulk/bundle-hash | {bundle_hash} |
| GET | /v1/cpu/bulk/webhook-secret | The signing secret (write key) |
| POST | /v1/cpu/bulk/webhook-secret/rotate | A new secret generation (write key) |
| GET | /v1/cpu/bulk/webhooks/dead | Up to 50 deliveries that gave up |
| POST | /v1/cpu/bulk/{job_id}/webhook/retry | Re-arm a dead delivery (409 nothing_to_retry otherwise) |
Webhooks are the bulk.completed / bulk.failed events, signed with a timestamp, retried for 24 hours; the format and verification are described under Render API webhooks. Payload: event, job_id, status, error_code, refunded, and on success rows, ok, failed, blocks_charged, blocks_billed, download.
Other tools
Compact list; every charged route follows the contract above (write key, input_refs, 200 with job_id and quote, poll GET /v1/cpu/jobs/{id}). Free routes answer directly and charge nothing. Where a tool has more options than shown, the request model refuses unknown fields with 422, so the error tells you what is accepted.
Audio
| Method | Path | What it does | Main fields |
|---|---|---|---|
| POST | /v1/cpu/audiotools/tools | cut, convert, join, fade, speed, pitch, silence, gain (incl. loudness presets), eq, channels, metadata | op, format, bitrate_k 32-320, sample_rate, channels, options; per 30 min of output, files up to 30 min |
| POST | /v1/cpu/audiotools/waveform | Audiogram video, waveform PNG or peaks JSON | op (audiogram | waveform_png | peaks), srt (audiogram), options; audio up to 10 min |
| POST | /v1/cpu/asr/transcribe | Transcription to txt / srt / vtt / json | duration_s (max 7200), language, formats; per started 10 min |
| POST | /v1/cpu/audio/enhance | Voice clean-up and loudness preset | duration_s (max 3600), preset (spotify, youtube, podcast, broadcast, none), denoise, strength, format |
| POST | /v1/cpu/audio/repair | Hum, clicks, clipping, noise, reverb, breaths, loudness | modules, format; up to 60 min |
| POST | /v1/cpu/audio/repair/preview | Free. Before/after excerpt (10-30 s) | modules, start_s, length_s; 20 a day |
| POST | /v1/cpu/audio/bleep | Bleep or mute chosen words | transcript ([{t, s, e}]), languages, custom_words, style; up to 60 min |
| POST | /v1/cpu/audio/bleep/detect | Free. Which words would be replaced | as above |
| POST | /v1/cpu/audio/midi | Audio to MIDI plus tempo, key and chords | ops, mode, quantize, sensitivity; up to 10 min |
| POST | /v1/cpu/audio/switch | Multi-camera podcast cut list (EDL) from 2-8 audio angles | angle_ids, layout, min_shot_s, diarization, ... |
| POST | /v1/cpu/master/run | Reference mastering | Inputs: track, reference; duration_s (max 600), format |
| POST | /v1/cpu/midi/render | Render a .mid to audio | duration_s (max 1800), soundfont, format |
| POST | /v1/cpu/podcast/mix | Mix up to 8 tracks with ducking and loudness | tracks ({ref, role, gain_db, offset_s, duration_s}), preset, format, duck_amount_db; per started 30 min of the longest track |
Video
| Method | Path | What it does | Main fields |
|---|---|---|---|
| POST | /v1/cpu/video/transcode | Delivery encode (AV1, HEVC, H.264, ProRes) | codec, quality, preset (youtube, instagram, tiktok, broadcast, web-av1, archive-prores), duration_s, width, height, captions_srt; per output minute |
| POST | /v1/cpu/video/hls | Adaptive streaming ladder (3-5 renditions) | duration_s, width, height; per minute |
| GET | /v1/cpu/video/hls/playlist?path= | Free. The playlist with signed segment links | |
| GET | /v1/cpu/video/hls/sign?path= | Free. One fresh signed link | |
| DELETE | /v1/cpu/video/hls/ladder?path= | Delete a ladder you made | |
| POST | /v1/cpu/video/repair | Deinterlace, lens, stabilise, deflicker, denoise, deband, frame rate | level (standard | strong), modules; up to 30 min, 4 GB |
| POST | /v1/cpu/video/repair/preview | Free. 5-second before/after | level, modules, start_s; 20 a day |
| POST | /v1/cpu/video/thumbs | Pick the best thumbnail frames, with crop packs | top_n 1-12; per started 10 min |
| GET | /v1/cpu/video/thumbs/units?duration_s= | Free. {units} | |
| POST | /v1/cpu/video/parallax | 2.5D camera move from one photo | motion, duration_s 1-30, fps, resolution, formats, layers |
| GET | /v1/cpu/video/parallax/units | Free. {units, frames, allowed_formats, max_layers} | duration_s, fps, resolution |
| POST | /v1/cpu/video/proxy | Free. Editing proxy for a library video | file_id |
| POST | /v1/cpu/color/tonemap | HDR to SDR | duration_s, width, height, fps, tonemap |
| POST | /v1/cpu/vedit/analyze | Silences, filler words, scene cuts, chapters | duration_s (max 7200), language, words, ... |
| POST | /v1/cpu/vedit/chapters | Free. Chapters from a word list | words, language, max_chapters, min_chapter_s |
| POST | /v1/cpu/anim/render | Animated design export (MP4, WebM, GIF, APNG) | bundle_hash, format, fps, duration_s, scale, ... |
| POST | /v1/cpu/lottie/render | Lottie to video or animated image | duration_s (max 30), format, fps, width, height |
Photo
| Method | Path | What it does | Main fields |
|---|---|---|---|
| POST | /v1/cpu/photo/hdr | Merge 2-7 exposures | align, deghost, format, quality |
| POST | /v1/cpu/photo/pano | Stitch 2-12 photos | projection, crop, fov_deg |
| POST | /v1/cpu/photo/focus | Focus-stack 2-20 photos | align |
| POST | /v1/cpu/raw/develop, /v1/cpu/raw/photo | Develop RAW files (1-64, or exactly 1) | preset, look, format |
| POST | /v1/cpu/photo/recipe | Apply a saved edit to 1-64 photos | recipe, bundle_hash, format, zip |
| POST | /v1/cpu/photo/lens | Lens correction and upright | distortion, vignetting, tca, upright, guides |
| POST | /v1/cpu/photo/portrait | Portrait retouch | sliders smooth, blemish, eyes, teeth, ... |
| POST | /v1/cpu/photo/sky | Sky replacement | sky, sensitivity, horizon_blend, ... |
| POST | /v1/cpu/photo/cull | Rate 1-500 photos for sharpness, exposure and blinks | blur_min, burst_gap_s, refresh |
| POST | /v1/cpu/photo/contact-sheet | Contact sheet or proof book PDF | mode, preset, page, captions, ... |
| POST | /v1/cpu/creative/collage | Collage of 2-20 images | layout, width, height, gap, background, format |
| POST | /v1/cpu/creative/effects | Duotone, halftone, pixel, ASCII | op, options |
| POST | /v1/cpu/creative/mockup | Device and print mockups | frame, tone, pose, aspect |
| POST | /v1/cpu/creative/photo-id | ID / passport photo and print sheet | op, preset (country), background, sheet |
| GET | /v1/cpu/creative/ops | Free. Which creative tools are on | |
| POST | /v1/cpu/privacy/detect, /v1/cpu/privacy/blur | Detect and blur faces and number plates in images or a video (no identification) | mode (image | video), faces, plates, blur, strength |
| POST | /v1/cpu/color/lut | Apply a .cube LUT to an image | format |
| POST | /v1/cpu/color/ocio | Colour-space conversion | src, dst or display + view |
| GET | /v1/cpu/color/ocio/spaces | Free. The available colour spaces | |
| POST | /v1/cpu/luts/upload-url, /finalize, /read-url, /delete | Free. Your LUT library | |
| POST | /v1/cpu/meta/iptc | Write IPTC/XMP into copies of up to 100 photos, or a stock-agency CSV | defaults, files, csv, stock |
| POST | /v1/cpu/meta/iptc/prefill | Free. Captions and keywords from your library | asset_ids |
| POST | /v1/cpu/watermark/embed, /embed-audio, /embed-video | Invisible watermark in an image, audio or video | format, duration_s |
| POST | /v1/cpu/watermark/verify | Free. Check a file for the watermark (raw file body) | ?media=audio|video |
Design, print and 3D
| Method | Path | What it does | Main fields |
|---|---|---|---|
| POST | /v1/cpu/codes/render | QR codes, label sheets and barcodes from /v1/cpu/* inputs (same options as /v1/codes/qr) | op, symbology, values or a CSV |
| POST | /v1/cpu/vector/cutfile | Cut files for plotters and lasers (DXF, SVG, PLT, EPS, PDF) from SVG / DXF / DWG | formats, size_mm, kerf_mm, bridges, nest, ... |
| POST | /v1/cpu/vectorize | Image to SVG (and PDF) | width, height, mode, detail, colors |
| POST | /v1/cpu/sticker/render | Sticker, sticker sheet or messenger pack | op, border_px, paper, mode |
| POST | /v1/cpu/print/preflight | Print preflight report | pages, profile, bleed_mm |
| POST | /v1/cpu/print/pdfx | Print-ready PDF/X | pages, profile, intent, spots |
| POST | /v1/cpu/print/softproof | Free. Soft proof and gamut warning | profile, paper |
| POST | /v1/cpu/print/impose | Imposition (n-up, booklet, cut-and-stack) | pages, plan |
| POST | /v1/cpu/print/vdp | Variable-data print from a PDF template and a CSV | records, fields, output |
| POST | /v1/cpu/scan/pdf | Document scan from 1-64 photos | colour, page_size, pages |
| POST | /v1/cpu/scan/detect | Free. Page corners in 1-8 photos | |
| POST | /v1/cpu/pptx/export, /v1/cpu/pptx/import | Designs to PowerPoint, and back | title, notes / thumbnails |
| POST | /v1/cpu/import | Design file (PSD, PDF, AI, EPS, SVG, IDML, Sketch) to editable layers | name, page |
| PUT, GET, DELETE | /v1/cpu/import/figma/token | Your Figma personal access token | token |
| POST | /v1/cpu/import/figma/frames | Free. Frames of a Figma file | url |
| POST | /v1/cpu/import/figma | Import Figma frames | url, frame_ids |
| POST | /v1/cpu/export/layered | Design to layered PSD / SVG / PDF | formats, artboard |
| POST | /v1/cpu/html5-banner, /v1/cpu/html5-banner/validate | HTML5 ad banners, and a validator | artboards, networks / size |
| POST | /v1/cpu/magic-resize | Smart crop boxes per target format | formats, layer_ids |
| POST | /v1/cpu/attention | Predicted-attention heatmap and CTA score | cta or scene |
| POST | /v1/cpu/font-id | Closest fonts for text in an image | box |
| POST | /v1/cpu/fonts/inspect, /v1/cpu/fonts/subset | Free. Font report; WOFF2 subset | charsets, text, axes |
| DELETE | /v1/cpu/fonts/brand/{brand_id}/{font_id} | Free. Remove a brand font | |
| POST | /v1/cpu/brand-url | Palette, fonts, logo and tone from a website | url, palette_size |
| POST | /v1/cpu/brand-check/scan | Free. Check a design against brand and accessibility rules | scene, brand |
| POST | /v1/cpu/brand-check/file | PDF accessibility or video flash/caption check, with fix | fix, lang, title |
| POST | /v1/cpu/mesh/optimize, /v1/cpu/mesh/convert | Optimise or convert a 3D model (GLB, glTF, OBJ, FBX, STL, PLY, STEP) | target_ratio, format |
| POST | /v1/cpu/turntable/render | Turntable video of a 3D model | frames, fps, width, height, hdri |
| POST | /v1/cpu/splat | Gaussian-splat PLY to compressed formats and a viewer | splat_count, formats, sh_bands |
Text and translation
| Method | Path | What it does | Main fields |
|---|---|---|---|
| GET | /v1/cpu/localize/languages | Free. The 30 supported languages | |
| POST | /v1/cpu/localize/translate | Translate text segments | source, target, segments (max 500; at most 1,000 words and 12,000 characters in total), glossary, do_not_translate |
| POST | /v1/cpu/localize/subtitles | Translate SRT / VTT | source, target, content |
| POST | /v1/cpu/localize/postedit | AI tone edit of a translation (synchronous) | tone, segments (max 100) |
| POST | /v1/cpu/localize/fit | Free. Re-fit translated text into its boxes | boxes (max 500) |
| POST | /v1/cpu/images/translate/detect | Free. Text boxes in an image, with a draft translation | target, source |
| POST | /v1/cpu/images/translate | Replace the text in an image | target, boxes |
| POST | /v1/cpu/ocr | Text from images or PDFs (up to 10 pages) | pages |
Commerce
| Method | Path | What it does | Main fields |
|---|---|---|---|
| POST | /v1/cpu/ecom-pack | Store image pack per marketplace | marketplaces, formats, products |
| POST | /v1/cpu/ecom/comply | Marketplace image compliance check or fix | marketplace, mode |
| POST | /v1/cpu/ecom-push | Free. Push a pack to a connected shop (dry_run is the default) | job_id, connection_id, confirm_shop |
| GET | /v1/cpu/ecom-push/status | Free. State of a push | job_id, connection_id, push_id |
Library, publishing, e-signature and projects
| Area | Routes | Notes |
|---|---|---|
| Library search and curation | POST /v1/cpu/dam/search, /search/semantic; GET, POST /collections, PATCH, DELETE /collections/{id}, GET /collections/{id}/assets; POST /dedupe, GET /dedupe/status, GET /dedupe/clusters, POST /dedupe/clusters/{id}/dismiss and /reopen; GET /library; GET /index/status, POST /index/backfill | All under /v1/cpu/dam. Free, except indexing beyond the monthly free allowance (402 backfill_exceeds_free_allowance with a quote) |
| Captions and rights | POST /v1/cpu/dam/caption (charged), POST /caption/apply; GET, PUT /assets/{id}/caption; GET, PUT /assets/{id}/rights, GET /assets/{id}/rights/export; POST /rights/lookup | Under /v1/cpu/dam. Your own edits are never overwritten |
| People | GET /v1/cpu/people, /search, /photos, /{id}/photos, /export; GET, PUT /settings; PATCH /{id}; POST /{id}/merge, /{id}/split; DELETE /data | Face grouping of your library. It is on by default; PUT /settings with {"enabled": false} switches it off and DELETE /v1/cpu/people/data deletes all face data |
| Published pages | GET /v1/cpu/publish/quote, /quota, /slug; POST /page; GET /pages, /pages/{id}; POST /pages/{id}/update, /unpublish; DELETE /pages/{id}; POST /export, GET /export/{job_id} | Under /v1/cpu/publish. Free, limited by a per-plan page quota (402 page_quota_reached) |
| Podcasts | GET /v1/cpu/podcast/quote, /quota, /categories, /slug; GET, POST /shows, GET, PATCH, DELETE /shows/{id}, POST /shows/{id}/artwork, POST /shows/{id}/episodes; GET, PATCH, DELETE /episodes/{id}, POST /episodes/{id}/publish, /unpublish; GET /jobs/{job_id}; POST /validate | Under /v1/cpu/podcast. Hosted RSS feeds; free, limited by a per-plan quota |
| E-signature | GET /v1/cpu/esign/limits; GET, POST /envelopes; GET, PUT, DELETE /envelopes/{id}; POST /send (charged per envelope); POST /envelopes/{id}/cancel, /remind; GET /envelopes/{id}/download | Under /v1/cpu/esign. Up to 10 signers and 60 fields; a cancelled or expired envelope that nobody opened is refunded. Signers use the emailed link, not the API |
| Bundles | POST /v1/cpu/bundle, GET /v1/cpu/bundle/{bundle_id} | 1-5 small jobs billed together (at least the bundle minimum, never more than separately) |
| Version history | GET, POST /v1/cpu/versions/{editor}/{project_id}; GET /compare; GET, DELETE /{version_id}; POST /{version_id}/name, /snapshot, /branch, /restore | editor is video, canvas or audio. Free |
| Plugins | GET /v1/cpu/plugins; POST /drafts; DELETE /versions/{id}, POST /versions/{id}/submit; GET, POST /installs, DELETE /installs/{id}, DELETE /installs/{id}/grants/{capability}; POST /run (charged) | Under /v1/cpu/plugins. Sandboxed plugins; installing needs consent: true and per-capability grants |
Not part of the public API
These routes exist under the same prefixes but are not for API keys, and are not documented here:
- Administration and plugin review (
/v1/aiwave/admin/cpu/*): staff sessions only. - Service-to-service routes:
/v1/cpu/audio/relay,/v1/cpu/ocr/relay,/v1/cpu/video/encode-handoff*,/v1/cpu/jobs/{id}/io. - Link-token routes used by public pages: the signer pages (
/v1/esign/*), the shop plugin acknowledgement (/v1/cpu/ecom-push/ack), share links (/v1/cpu/video/hls/share/*,/v1/cpu/splat/viewer/*). - The free web tools behind
fotohub.app/tools(/v1/tools/guest/*): browser-only, no API key. /v1/cpu/photogrammetry/scanand/alignexist but have no price yet, so they are refused (503 not_priced, or403 feature_disabled) and nothing is charged.
Related
- Render API: templates and bulk rendering
- Compute Jobs: the generation job queue
- Billing & Pricing: the USD wallet

