Developer & Automation Integration

API Reference

Render print-ready PDFs from your templates with a single HTTP call. The whole API is plain JSON over HTTPS, so it drops straight into n8n, Make.com, Zapier, Pipedream, or any other automation platform — no SDK, no webhooks to configure, just one HTTP Request node per step.

Introduction

Print Studio exposes a JSON HTTP API for rendering designs from saved templates. You provide a template_id and a set of field values (text, colors, image URLs) — the service substitutes them into the template, renders a print-ready PDF with optional CMYK conversion, and returns a signed CDN URL.

The API is plain HTTP + JSON, which means you can drive it from n8n, Make.com, Zapier, Pipedream, Activepieces, Retool Workflows, or any cURL-friendly tool — no SDK to install. Every endpoint card below ships with a dedicated n8n / Make tab that you can copy straight into an HTTP Request node.

Two rendering modes are available: synchronous (POST /api/process) which blocks until the PDF is ready and returns the URL directly, and asynchronous (POST /api/jobs) which returns a jobId immediately and lets you poll for completion. Use sync for low-volume / user-facing flows and most n8n workflows; use async for bulk batches, scheduled runs, or long-running renders that exceed the platform's HTTP timeout.

Use with n8n / Make / Zapier

Every endpoint below ships with a paste-ready cURL on its n8n / Make tab. The flow is the same on every platform: copy the cURL, paste it into your HTTP step, replace YOUR_API_SECRET_KEY with your real key (or a credential reference), and hit run.

n8n — 30 seconds, no manual config

  1. Add an HTTP Request node.
  2. Click the menu in the node header → Import cURL command.
  3. Paste the cURL from the endpoint's n8n / Make tab. n8n auto-fills Method, URL, Headers, and Body in one shot.
  4. For the API key, switch Authentication to Header Auth with name Authorization and value Bearer {{ $env.API_SECRET_KEY }} — that way the secret stays out of the workflow JSON.
  5. Wire dynamic data into the body with {{ $json.x }} expressions — exec.

Make.com

  1. Add an HTTP > Make a request module.
  2. Read the cURL tab and copy URL + Method straight in. Add an Authorization header of Bearer YOUR_KEY (store the key in a connection rather than the scenario).
  3. Pick Body type: Raw, Content type: JSON, paste the JSON from the cURL -d argument, swap {{ $json.x }} placeholders for Make's {{1.x}} tokens.

Zapier · Pipedream · Activepieces

  1. Zapier: Webhooks by Zapier > Custom Request. Pipedream: HTTP / Webhook. Activepieces: HTTP > Send Request.
  2. Copy the URL, Method, and Authorization header from the cURL. Paste the JSON body from -d '...' into the body field.
  3. Rewrite {{ $json.x }} placeholders in the host's template syntax (e.g. {{step1.x}} on Zapier).

Workflow patterns

  • Sync render (≤ 30s): one POST /api/process node; map outputUrl downstream.
  • Async render: POST /api/jobs Wait GET /api/jobs/{jobId} in a loop until job.status === "completed".
  • Bulk render: Split In Batches (n8n) / Iterator (Make) over a CSV → one POST /api/jobs per row.
  • Template picker: GET /api/templates → expose id + name as a dropdown in your form.

Security: the API_SECRET_KEY is a server-side credential. Always store it as an n8n Header Auth credential / Make connection / Zapier hidden field and reference it from the workflow — never paste the raw key into a shared scenario or a browser-side automation.

Authentication

All integration endpoints require a server-side API key, passed as a Bearer token:

Authorization: Bearer ps_live_<your-key>

Keys are managed from the operations dashboard at /dashboard. From there you can generate keys, set per-endpoint scopes (render, external-jobs, templates:read, jobs:read), revoke keys, and see lifetime usage per key. The plaintext value is shown only once at creation — store it in your partner's secret manager.

A request that authenticates but lacks the scope a route requires returns 403 Insufficient scope. A request with no or an invalid token returns 401 Unauthorized.

Legacy env-var values (API_SECRET_KEY, MAGENTO_API_KEY) are auto-seeded into the dashboard's key database on first run, so any partner already integrated against those tokens keeps working — the values now show up as "Legacy" entries in the dashboard and can be revoked once partners have rotated to a freshly generated key.

Conventions

  • All requests and responses use JSON with UTF-8 encoding.
  • Field tags must be lowercase snake_case: ^[a-z0-9_]+$.
  • Colors can be expressed as { rgb: "#RRGGBB" } or with an explicit CMYK override { rgb: "#RRGGBB", cmyk: { c, m, y, k } }. The renderer picks the right space based on the template's colorProfile.
  • Asset URLs (logo, pic) must be publicly reachable HTTPS endpoints returning image/png, image/jpeg, or image/webp under 10 MB.
  • Timestamps are ISO 8601 UTC strings.

Errors

Errors return a non-2xx status code and a JSON body of the shape{ error: string, details?: object[] }. Validation errors include adetailsarray with per-field messages.

{
  "error": "Validation failed",
  "details": [
    { "field": "fields.0.tag", "message": "tag must be lowercase letters, numbers, and underscores only" }
  ]
}
POST/api/processAPI key

Render a PDF synchronously

Substitutes your field values into the template, renders the artwork with Puppeteer, converts to CMYK when the template's colorProfile is 'cmyk', and uploads the output to CDN storage. The response includes URLs for the final PDF, an editable .pst for the Studio, and a thumbnail PNG.

Body Parameters

NameTypeRequiredDescription
template_idstringrequiredID of a saved template (from GET /api/templates).
fieldsField[]requiredValues keyed by tag. Each field carries a text/URL value and optional color overrides (color / fillColor / strokeColor).
assets{ logo?, pic? }optionalPublic HTTPS URLs for image slots. Must be PNG/JPEG/WEBP under 10 MB.
palette{ primary?, secondary?, tertiary? }optionalSource of truth for role-colored shapes and fallback for text fields that declare a role.
notification{ name?, notify_email?, notify_slack? }optionalCustomer name (baked into output PDF / PST filenames) and optional email or Slack handle for downstream delivery notifications.

Request Body

{
  "template_id": "tpl_bb_mo8qvt9u",
  "notification": {
    "notify_email": "customer@example.com",
    "name": "Acme Coffee"
  },
  "fields": [
    { "tag": "headline",  "value": "Summer Sale" },
    { "tag": "tagline",   "value": "Up to 40% off" },
    {
      "tag": "brand_color",
      "fillColor": { "rgb": "#3A4B7C", "cmyk": { "c": 85, "m": 65, "y": 20, "k": 10 } }
    }
  ],
  "assets": {
    "logo": "https://cdn.example.com/brands/acme/logo.png",
    "pic":  "https://cdn.example.com/brands/acme/hero.jpg"
  },
  "palette": {
    "primary":   { "rgb": "#3A4B7C" },
    "secondary": { "rgb": "#C5D629" }
  }
}

Response (200 OK)

`outputUrl` is always present on success. `editableUrl` and `thumbnailUrl` may be null if PST generation fails (the print-ready PDF still uploads — PST is non-fatal).

{
  "success": true,
  "jobId": "job_mo9d1w6o_y8aj",
  "outputUrl":    "https://cdn.yourbucket.r2.dev/Outputs/job_mo9d1w6o_y8aj.pdf",
  "editableUrl":  "https://cdn.yourbucket.r2.dev/Jobs/job_mo9d1w6o_y8aj/job_mo9d1w6o_y8aj.pst",
  "thumbnailUrl": "https://cdn.yourbucket.r2.dev/Jobs/job_mo9d1w6o_y8aj/job_mo9d1w6o_y8aj.png"
}

Status Codes

200PDF rendered400Validation failed401Unauthorized404Template not found422Missing required fields / bad asset500Render pipeline failed
curl -X POST https://your-studio.example.com/api/process \
  -H "Authorization: Bearer $API_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "template_id": "tpl_bb_mo8qvt9u",
    "notification": { "name": "Acme Coffee", "notify_email": "ops@acme.co" },
    "fields": [
      { "tag": "headline", "value": "Summer Sale" },
      { "tag": "tagline",  "value": "Up to 40% off" }
    ],
    "assets": { "logo": "https://cdn.example.com/acme/logo.png" }
  }'
POST/api/jobsAPI key

Queue an async render job

Same request shape as /api/process, but the job is pushed onto the render queue and the endpoint returns immediately. Use this for bulk runs, long-running renders, or any flow where you'd rather poll than block.

Body Parameters

NameTypeRequiredDescription
template_idstringrequiredID of a saved template.
fieldsField[]requiredSame shape as POST /api/process.
assets{ logo?, pic? }optionalSame shape as POST /api/process.
palettePaletteoptionalSame shape as POST /api/process.
notificationobjectoptionalSame shape as POST /api/process.

Request Body

{
  "template_id": "tpl_bb_mo8qvt9u",
  "notification": {
    "notify_email": "customer@example.com",
    "name": "Acme Coffee"
  },
  "fields": [
    { "tag": "headline",  "value": "Summer Sale" },
    { "tag": "tagline",   "value": "Up to 40% off" },
    {
      "tag": "brand_color",
      "fillColor": { "rgb": "#3A4B7C", "cmyk": { "c": 85, "m": 65, "y": 20, "k": 10 } }
    }
  ],
  "assets": {
    "logo": "https://cdn.example.com/brands/acme/logo.png",
    "pic":  "https://cdn.example.com/brands/acme/hero.jpg"
  },
  "palette": {
    "primary":   { "rgb": "#3A4B7C" },
    "secondary": { "rgb": "#C5D629" }
  }
}

Response (200 OK)

HTTP 202 on accept — poll GET /api/jobs/{jobId} until status is completed or failed.

{
  "success": true,
  "jobId": "job_mo9d1w6o_y8aj"
}

Status Codes

202Queued400Validation failed401Unauthorized500Queue push failed
curl -X POST https://your-studio.example.com/api/jobs \
  -H "Authorization: Bearer $API_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "template_id": "tpl_bb_mo8qvt9u",
    "fields": [{ "tag": "headline", "value": "Hello" }],
    "notification": { "notify_email": "ops@acme.co" }
  }'
GET/api/jobs/{jobId}API key · Admin session

Get job status

Returns the current state of a queued job. Poll this endpoint every 1–3 seconds after queueing. When status is 'completed', outputUrl / editableUrl / thumbnailUrl are populated.

Path Parameters

NameTypeRequiredDescription
jobIdstringrequiredThe id returned from POST /api/jobs.

Response (200 OK)

status ∈ { waiting, active, completed, failed, delayed }.

{
  "success": true,
  "job": {
    "id": "job_mo9d1w6o_y8aj",
    "status": "completed",
    "templateId": "tpl_bb_mo8qvt9u",
    "name": "Acme Coffee",
    "attemptsMade": 1,
    "createdAt": "2026-04-22T10:15:00.000Z",
    "completedAt": "2026-04-22T10:15:06.221Z",
    "outputUrl":    "https://cdn.yourbucket.r2.dev/Outputs/job_mo9d1w6o_y8aj.pdf",
    "editableUrl":  "https://cdn.yourbucket.r2.dev/Jobs/job_mo9d1w6o_y8aj/job_mo9d1w6o_y8aj.pst",
    "thumbnailUrl": "https://cdn.yourbucket.r2.dev/Jobs/job_mo9d1w6o_y8aj/job_mo9d1w6o_y8aj.png"
  }
}

Status Codes

200OK401Unauthorized404Job not found
curl https://your-studio.example.com/api/jobs/job_mo9d1w6o_y8aj \
  -H "Authorization: Bearer $API_SECRET_KEY"
GET/api/templatesAPI key · Admin session

List available templates

Returns every saved template with its id, human-readable name, thumbnail URL, updatedAt, and the tags of required fields. First-load responses return fast and warm the in-process cache in the background — subsequent calls include full metadata. The `hits` and `cached` fields are for observability and can be ignored by integrators.

Response (200 OK)

On a cold start, name may equal the template id and requiredFields may be empty until the background warm completes (~1–2s after the first call).

{
  "success": true,
  "templates": [
    {
      "id": "tpl_bb_mo8qvt9u",
      "name": "Summer Flyer A4",
      "thumbnailUrl": "https://cdn.yourbucket.r2.dev/templates/tpl_bb_mo8qvt9u/thumbnail.png",
      "updatedAt": "2026-04-20T18:22:10.000Z",
      "requiredFields": ["headline", "tagline"],
      "hits": 12,
      "cached": true
    }
  ]
}

Status Codes

200OK401Unauthorized
curl https://your-studio.example.com/api/templates \
  -H "Authorization: Bearer $API_SECRET_KEY"
GET/api/templates/{templateId}API key · Admin session

Get a single template

Returns the full Template object (canvas config, manifest fields, color profile) and the raw Fabric.js canvasJson. The manifest is the source of truth for which tags are required when calling /api/process — use it to dynamically build the form you render to end users.

Path Parameters

NameTypeRequiredDescription
templateIdstringrequiredThe id from GET /api/templates.

Response (200 OK)

{
  "success": true,
  "template": {
    "id": "tpl_bb_mo8qvt9u",
    "name": "Summer Flyer A4",
    "canvas": { "width": 1240, "height": 1754, "bleed": 36, "safeZone": 24, "backgroundColor": "#ffffff" },
    "colorProfile": "cmyk",
    "manifest": {
      "fields": [
        { "tag": "headline", "type": "text",  "label": "Headline",  "role": "primary",   "required": true },
        { "tag": "tagline",  "type": "text",  "label": "Tagline",   "role": "secondary", "required": false },
        { "tag": "logo",     "type": "image", "label": "Logo",      "role": "none",      "required": false }
      ]
    },
    "createdAt": "2026-02-01T09:00:00.000Z",
    "updatedAt": "2026-04-20T18:22:10.000Z"
  },
  "canvasJson": { "/* Fabric.js canvas serialization */": true }
}

Status Codes

200OK401Unauthorized404Template not found
curl https://your-studio.example.com/api/templates/tpl_bb_mo8qvt9u \
  -H "Authorization: Bearer $API_SECRET_KEY"

Partner Integrations — Magento

A separate intake endpoint for the client's Magento storefront. The web team posts a fixed payload (ContactInfo1–8, Logo / AltLogo, Photo / AltPhoto, ComplimentsOf, CustomerId) and the design is queued under the CustomerId they supplied — so the identifier is the same on both sides.

Field mapping

Magento fieldPrint-studio tagNotes
ContactInfo1company_nametext
ContactInfo2phonetext
ContactInfo3websitetext
ContactInfo4slogantext
ContactInfo5emailtext
ContactInfo6custom_1text · “Other”
ContactInfo7custom_2text · “Other”
ContactInfo8custom_3text · “Other”
ComplimentsOfcompliments_oftext
Logoassets.logoimage · HTTPS URL
AltLogoassets.alt_logoimage · fallback or hidden layer
Photoassets.picimage · HTTPS URL
AltPhotoassets.alt_picimage · fallback or hidden layer

Alt images & overflow values

  • If Logo/Photo HEAD-validates successfully, the corresponding AltLogo/AltPhoto rides along as a visible: false hidden layer on the saved PST. The rendered PDF does NOT include hidden content.
  • If the primary URL fails (404, wrong content-type, >10 MB), the alt is promoted into the primary slot. The job is flagged Manual Review and the reason is recorded on the job record.
  • Text fields whose tag is not present in the chosen template (e.g. custom_1/2/3 when the template only has company_name and phone) become hidden text layers in the same way. Manual Review is flagged.
  • All such jobs appear in the Operations dashboard with an amber Review badge so the print team can open the PST and decide what to keep, move, or delete before sending to press.

CustomerId rules

  • The value of CustomerId is used verbatim as the BullMQ job id, the S3 storage prefix, and the identifier returned to the partner.
  • Allowed characters: [a-zA-Z0-9_-] (max 128 chars).
  • Posting the same CustomerId twice returns 409 Conflict — the partner must mint a new id to retry. We never silently overwrite a prior design.

Retries

A 5xx response means our backend was temporarily unavailable — retry the same request with exponential backoff (e.g. 1s, 5s, 30s). Retries are safe because of CustomerId deduplication: a CustomerId that was already queued by an earlier attempt returns 409 instead of creating a second job, so you will never accidentally render the same design twice.

POST/api/external/jobsAPI key

Queue an async job (Magento / external partners)

Async, fire-and-forget intake for the client's Magento storefront. Accepts the partner's fixed payload shape, maps it onto internal tags, performs HEAD-validation on supplied image URLs (with Alt fallback), and enqueues the job under the partner-supplied CustomerId. The partner does not need to poll — a future Zoho push surfaces the finished design on their side.

Body Parameters

NameTypeRequiredDescription
CustomerIdstringrequiredUsed verbatim as the print-studio job id. [a-zA-Z0-9_-]+, max 128 chars. Duplicates return 409.
template_idstringrequiredPrint Studio template id (from GET /api/templates).
ContactInfo1..8stringoptionalText fields. 1–5 map to canonical tags (company_name, phone, website, slogan, email); 6–8 to custom_1/2/3. Empty strings are dropped; unmapped values become hidden text layers + Manual Review.
Logo / AltLogostring (URL)optionalHTTPS URLs to PNG/JPEG/WEBP under 10 MB. If primary fails HEAD-validation, alt is promoted into the primary slot and the job is flagged Manual Review. If primary succeeds, alt rides along as a hidden layer.
Photo / AltPhotostring (URL)optionalSame fallback semantics as Logo / AltLogo.
ComplimentsOfstringoptionalMaps to the compliments_of text tag.

Request Body

{
  "CustomerId": "MAG-ORDER-1029384",
  "template_id": "tpl_bb_mo8qvt9u",
  "ContactInfo1": "Acme Coffee Roasters",
  "ContactInfo2": "+1 (415) 555-0142",
  "ContactInfo3": "acmecoffee.example.com",
  "ContactInfo4": "Roasted daily, brewed slowly.",
  "ContactInfo5": "hello@acmecoffee.example.com",
  "ContactInfo6": "Loyalty member since 2019",
  "ContactInfo7": "",
  "ContactInfo8": "",
  "Logo":     "https://cdn.example.com/acme/logo.png",
  "AltLogo":  "https://cdn.example.com/acme/logo-mono.png",
  "Photo":    "https://cdn.example.com/acme/storefront.jpg",
  "AltPhoto": "https://cdn.example.com/acme/team.jpg",
  "ComplimentsOf": "Compliments of Sam"
}

Response (200 OK)

HTTP 202 — the response is always { success, jobId } where jobId === CustomerId. There is no callback; status, if needed, can be polled at GET /api/jobs/{CustomerId}.

{
  "success": true,
  "jobId": "MAG-ORDER-1029384"
}

Status Codes

202Queued400Validation failed401Unauthorized409CustomerId already exists422All image URLs unreachable / invalid500Queue push failed
curl -X POST https://your-studio.example.com/api/external/jobs \
  -H "Authorization: Bearer $MAGENTO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "CustomerId": "MAG-ORDER-1029384",
    "template_id": "tpl_bb_mo8qvt9u",
    "ContactInfo1": "Acme Coffee Roasters",
    "ContactInfo2": "+1 (415) 555-0142",
    "ContactInfo3": "acmecoffee.example.com",
    "ContactInfo4": "Roasted daily, brewed slowly.",
    "ContactInfo5": "hello@acmecoffee.example.com",
    "Logo":     "https://cdn.example.com/acme/logo.png",
    "AltLogo":  "https://cdn.example.com/acme/logo-mono.png",
    "Photo":    "https://cdn.example.com/acme/storefront.jpg",
    "AltPhoto": "https://cdn.example.com/acme/team.jpg",
    "ComplimentsOf": "Compliments of Sam"
  }'