GCRflow API
Convert print PDF and TIFF files from your own server through a REST API: API keys, upload, conversion jobs and webhooks. This page is a public version of the API reference in Settings › API keys, where your current limits and the API host are shown.
Before you start
- Sign in to GCRflow with a verified e-mail address and create an API key in Settings › API keys. Keep the key on your server.
- Send the key with every request as «Authorization: Bearer <key>».
- Base URL: https://<API host>/api/v1. The host is filled in for you in the examples in Settings › API keys.
- PDF, TIF and TIFF files are accepted.
- Use of the API is subject to the Terms of Service.
Quickest way: two requests
Send the file and get a job back in one request, then fetch the result with a second one. Every option is optional: leave them all out for the default settings (GCR50, GRACoL 2013 × 320%, rich-black boost off). This way is for smaller files (the size limit is listed under Limits in Settings › API keys); for larger files use the full interface below.
export GCRFLOW_KEY="<your API key>" # Settings › API keys
export API="https://<API host>/api/v1"
# 1) send the file (options optional) → job
curl -s -X POST "$API/convert" -H "Authorization: Bearer $GCRFLOW_KEY" -F file=@brochure.pdf
# → 201 {"id": "3f2a…", "status": "queued", "file_url": "$API/jobs/3f2a…/file", …}
# options: -F tier=gcr60 -F deeper_blacks=true -F icc=fogra39 -F tac=300 -F cmyk_only=true -F preset_id=…
# 2) fetch the result (202 while running ⇒ wait and ask again; 302 ⇒ saved)
while :; do
code=$(curl -s -L -o brochure_GCR50.pdf -w '%{http_code}' "$API/jobs/3f2a…/file" -H "Authorization: Bearer $GCRFLOW_KEY")
[ "$code" = 202 ] || break; sleep 5
done; echo "$code" # 200 = the file is saved; otherwise brochure_GCR50.pdf holds the error JSONWhile the job runs, the file request answers 202 with Retry-After; when it is done it redirects (302) to a short-lived download link, so curl -L or Python requests save the file directly (your API key is not sent on to the download host). A failed job answers 409 with the reason.
The same in Python (requests):
import time, requests
API = "https://<API host>/api/v1"
H = {"Authorization": "Bearer <your API key>"}
with open("brochure.pdf", "rb") as f: # options optional, e.g. data={"tier": "gcr60"}
r = requests.post(f"{API}/convert", headers=H, files={"file": f})
r.raise_for_status()
job = r.json()
while True:
r = requests.get(job["file_url"], headers=H) # follows the 302 to the download link
if r.status_code != 202:
break
time.sleep(int(r.headers.get("Retry-After", "5")))
r.raise_for_status() # 409 = the job failed (see r.json())
open("brochure_GCR50.pdf", "wb").write(r.content)Optional fields of POST /convert
| Field | Meaning |
|---|---|
| file | The PDF (or TIF / TIFF), up to the POST /convert size limit (required) |
| tier | GCR strength gcr0 … gcr100, steps of 5 (default gcr50) |
| icc / tac | Paper standard and total ink limit (default gracol2013 × 320); other combinations may answer 409 DEVAPI-PREPARING the first time |
| deeper_blacks | Rich-black boost: true / false (default false) |
| cmyk_only | true = plain CMYK separation: no ink saving, CMYK values stay as they are and are not brought within the ink limit (default false). Cannot be sent with tier or deeper_blacks (422) |
| preset_id | One of your saved presets (send it alone: it decides everything) |
| Idempotency-Key | Header, optional: the same key + same file + same fields within 24 hours returns the first job |
Full interface: any file size, every option
Three calls instead of one: get an upload URL, PUT the file straight to storage (the same size limit as the website), then start the job with all options.
1. Create an API key
Create a key in Settings › API keys and keep it on your server. Send it with every request as «Authorization: Bearer <key>».
export GCRFLOW_KEY="<your API key>"
export API="https://<API host>/api/v1"2. Upload the file
Ask for an upload URL, then PUT the file to it within 15 minutes. PDF, TIF and TIFF are accepted.
curl -s -X POST "$API/uploads" \
-H "Authorization: Bearer $GCRFLOW_KEY" -H "Content-Type: application/json" \
-d '{"filename": "brochure.pdf"}'
# → {"upload_id": "up_…", "upload_url": "https://storage…", "method": "PUT", "headers": {"Content-Type": "application/pdf"}}
curl -s -X PUT "<upload_url>" -H "Content-Type: application/pdf" --data-binary @brochure.pdf3. Start the conversion
Create a job from the upload. An Idempotency-Key makes retries safe: the same key and body within 24 hours returns the first job.
curl -s -X POST "$API/jobs" \
-H "Authorization: Bearer $GCRFLOW_KEY" -H "Content-Type: application/json" \
-H "Idempotency-Key: order-1042" \
-d '{"upload_id": "up_…", "strength": 50}'
# → 201 {"id": "3f2a…", "status": "queued", …}4. Wait for it
Poll the job every 5–10 seconds, or set up a webhook (the Webhooks tab of Settings › API keys) and GCRflow tells you when it is done.
curl -s "$API/jobs/3f2a…" -H "Authorization: Bearer $GCRFLOW_KEY"
# status: queued → processing → done | failed | cancelled5. Download
Ask for a short-lived download URL, then fetch the file.
curl -s -X POST "$API/jobs/3f2a…/download" -H "Authorization: Bearer $GCRFLOW_KEY"
# → {"download_url": "https://storage…", "expires_in_seconds": 900, "file_name": "brochure_GCR50.pdf", …}
curl -s -o brochure_GCR50.pdf "<download_url>"Endpoints
| Method | Path | What it does |
|---|---|---|
| POST | /api/v1/convert | Upload + start a conversion in one request (multipart) |
| GET | /api/v1/jobs/{id}/file | The result: 302 to a download link when done, 202 while running |
| GET | /api/v1/me | The key and its account |
| GET | /api/v1/options | Every accepted option value |
| POST | /api/v1/uploads | Signed upload URL |
| POST | /api/v1/jobs | Start a conversion |
| GET | /api/v1/jobs | Jobs created through the API, newest first |
| GET | /api/v1/jobs/{id} | One job and its status |
| POST | /api/v1/jobs/{id}/download | Short-lived download URL |
| DELETE | /api/v1/jobs/{id} | Delete a job |
Job options
| Field | Meaning |
|---|---|
| upload_id | From POST /uploads (required) |
| strength | GCR strength 0–100 in steps of 5 (default 50) |
| icc / tac | Paper standard and total ink limit (default gracol2013 × 320); other combinations may answer 409 DEVAPI-PREPARING the first time |
| rich_black | Rich-black boost (true / false) |
| low_gray | Low Gray Enhancement 1.0–1.85 (default paper standard only) |
| mode | ink_saving (default) or cmyk_only (plain separation: no ink saving, CMYK values not brought within the ink limit; takes no strength, rich_black, low_gray or blend_strict, 422) |
| rgb_source | Untagged RGB: general = ECI-RGB V1.0, relative colorimetric (default for ink_saving); canva = sRGB, perceptual (default for cmyk_only) |
| preset_id | One of your saved presets (send it alone) |
| blend_strict | Keep colors under blend-mode objects out of ink saving |
| Idempotency-Key | Header, optional: safe retries (24 hours) |
Errors
Errors look like {"detail": {"code": "DEVAPI-…", "message": "…"}}. Codes from the conversion core keep their names.
| Status | Code | Meaning |
|---|---|---|
| 202 | — | Still running — retry after Retry-After |
| 401 | DEVAPI-KEY-MISSING / -INVALID / -REVOKED | Send a valid key |
| 403 | DEVAPI-ACCOUNT-UNAVAILABLE | The account cannot use the API |
| 404 | DEVAPI-UPLOAD-NOT-FOUND / DEVAPI-JOB-NOT-FOUND | Not found (or not yours) |
| 409 | DEVAPI-UPLOAD-MISSING | PUT the file to upload_url first |
| 409 | DEVAPI-PREPARING | The paper standard is being prepared — retry after Retry-After |
| 409 | DEVAPI-JOB-NOT-DONE | The job has not finished |
| 409 | DEVAPI-JOB-FAILED / -CANCELLED | The job failed or was cancelled — there is no file |
| 410 | DEVAPI-FILES-EXPIRED | The retention period ended and the files were deleted — upload the file again |
| 413 | DEVAPI-FILE-TOO-LARGE | File too large: over the POST /convert limit (use the full interface), or over the website’s file size limit for POST /uploads |
| 422 | DEVAPI-PARAM-INVALID / DEVAPI-IDEMPOTENCY-MISMATCH | Fix the request |
| 429 | DEVAPI-RATE-LIMIT / DEVAPI-CONCURRENCY-LIMIT | Slow down (see Retry-After) |
| 503 | DEVAPI-BUSY | Busy — retry after Retry-After |
Other codes may appear; always read detail.code.
Limits
Each account has limits on requests per minute per key and per account, new jobs per minute, jobs running at once, active keys, and the largest file for POST /convert. The current values for your account are listed in Settings › API keys. Over a limit the API answers 429 with a Retry-After header.
Webhooks
When a job created through the API ends, GCRflow POSTs the event to your URL. The body names the download call, never a file URL.
The URL must use https on port 443 or 8443 with a public host name: no IP address, localhost or internal name, and every address the name resolves to must be public.
POST <your URL>
Content-Type: application/json
GCRflow-Webhook-Id: evt_…
GCRflow-Webhook-Timestamp: 1760600000
GCRflow-Webhook-Signature: v1=<hex HMAC-SHA256(secret, timestamp + "." + raw body)>
{"id": "evt_…", "type": "job.done", "created_at": "…", "api_version": "v1",
"data": {"job": {"id": "3f2a…", "status": "done", …},
"download": {"method": "POST", "url": "https://<API host>/api/v1/jobs/3f2a…/download"}}}Verify every delivery: recompute the signature over «timestamp.raw body» with your signing secret, compare in constant time, reject timestamps older than 5 minutes and event ids you have already seen.
# Python
import hashlib, hmac, time
def verify(secret: str, headers: dict, raw_body: bytes) -> bool:
ts = headers["GCRflow-Webhook-Timestamp"]
if abs(time.time() - int(ts)) > 300: # reject old / replayed deliveries
return False
mac = hmac.new(secret.encode(), ts.encode() + b"." + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest("v1=" + mac, headers["GCRflow-Webhook-Signature"])// Node.js
const crypto = require('crypto');
function verify(secret, headers, rawBody) {
const ts = headers['gcrflow-webhook-timestamp'];
if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;
const mac = crypto.createHmac('sha256', secret).update(ts + '.').update(rawBody).digest('hex');
const want = Buffer.from('v1=' + mac), got = Buffer.from(headers['gcrflow-webhook-signature'] || '');
return want.length === got.length && crypto.timingSafeEqual(want, got);
}Answer with any 2xx within 10 seconds. Otherwise GCRflow retries after 1 minute, 5 minutes, 30 minutes, 2 hours and 6 hours (6 attempts in all). Redirects are not followed.