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 JSON

While 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

FieldMeaning
fileThe PDF (or TIF / TIFF), up to the POST /convert size limit (required)
tierGCR strength gcr0 … gcr100, steps of 5 (default gcr50)
icc / tacPaper standard and total ink limit (default gracol2013 × 320); other combinations may answer 409 DEVAPI-PREPARING the first time
deeper_blacksRich-black boost: true / false (default false)
cmyk_onlytrue = 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_idOne of your saved presets (send it alone: it decides everything)
Idempotency-KeyHeader, 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.pdf

3. 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 | cancelled

5. 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

MethodPathWhat it does
POST/api/v1/convertUpload + start a conversion in one request (multipart)
GET/api/v1/jobs/{id}/fileThe result: 302 to a download link when done, 202 while running
GET/api/v1/meThe key and its account
GET/api/v1/optionsEvery accepted option value
POST/api/v1/uploadsSigned upload URL
POST/api/v1/jobsStart a conversion
GET/api/v1/jobsJobs created through the API, newest first
GET/api/v1/jobs/{id}One job and its status
POST/api/v1/jobs/{id}/downloadShort-lived download URL
DELETE/api/v1/jobs/{id}Delete a job

Job options

FieldMeaning
upload_idFrom POST /uploads (required)
strengthGCR strength 0–100 in steps of 5 (default 50)
icc / tacPaper standard and total ink limit (default gracol2013 × 320); other combinations may answer 409 DEVAPI-PREPARING the first time
rich_blackRich-black boost (true / false)
low_grayLow Gray Enhancement 1.0–1.85 (default paper standard only)
modeink_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_sourceUntagged RGB: general = ECI-RGB V1.0, relative colorimetric (default for ink_saving); canva = sRGB, perceptual (default for cmyk_only)
preset_idOne of your saved presets (send it alone)
blend_strictKeep colors under blend-mode objects out of ink saving
Idempotency-KeyHeader, optional: safe retries (24 hours)

Errors

Errors look like {"detail": {"code": "DEVAPI-…", "message": "…"}}. Codes from the conversion core keep their names.

StatusCodeMeaning
202—Still running — retry after Retry-After
401DEVAPI-KEY-MISSING / -INVALID / -REVOKEDSend a valid key
403DEVAPI-ACCOUNT-UNAVAILABLEThe account cannot use the API
404DEVAPI-UPLOAD-NOT-FOUND / DEVAPI-JOB-NOT-FOUNDNot found (or not yours)
409DEVAPI-UPLOAD-MISSINGPUT the file to upload_url first
409DEVAPI-PREPARINGThe paper standard is being prepared — retry after Retry-After
409DEVAPI-JOB-NOT-DONEThe job has not finished
409DEVAPI-JOB-FAILED / -CANCELLEDThe job failed or was cancelled — there is no file
410DEVAPI-FILES-EXPIREDThe retention period ended and the files were deleted — upload the file again
413DEVAPI-FILE-TOO-LARGEFile too large: over the POST /convert limit (use the full interface), or over the website’s file size limit for POST /uploads
422DEVAPI-PARAM-INVALID / DEVAPI-IDEMPOTENCY-MISMATCHFix the request
429DEVAPI-RATE-LIMIT / DEVAPI-CONCURRENCY-LIMITSlow down (see Retry-After)
503DEVAPI-BUSYBusy — 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.