The PrintShopCRM API

REST + signed webhooks, on every plan — no top-tier gate, no undisclosed fee, no "Enterprise only". Rate limit: 120 requests/min per shop, published right here.

Authentication

Create a key in Settings → Developers, then send it as a bearer token:

curl https://pro.printshopcrm.com/api/v1/me \
  -H "Authorization: Bearer psc_live_XXXXXXXXXXXX"

Keys are shop-scoped and can be rotated or revoked at any time. Rotating invalidates the old key immediately.

Separate agent connections

OpenAPI tool definitions describe 22 supported agent operations. Use Download tools on a connection in Setup for a file filtered to its permissions with this installation’s absolute API URL. Import it into a compatible agent and configure bearer authentication separately. The file contains no key or shop records. GET /api/v1/production/team returns active employee IDs and names for assignments under the production:read permission.

For a new agent, use Setup & connections → Connect your own agent. Named keys have independent permissions, expiry and revocation. They belong to their creator’s active manager account. GET /api/v1/me returns scopes and expiry. Copy the key once into your agent’s secret storage; the server stores only its hash.

Agent permissions cover pricing, customer/estimate reads and writes, invoice/payment reads, job stages, and production tasks. Unselected operations are refused even with a browser cookie. Agent keys cannot access internal endpoints, settings, credentials or webhook administration. Writes happen immediately; your agent must provide its own approval policy. Customer creation and stage changes may trigger configured automations.

GET /api/v1/pricingResolved price book, custom matrix summaries and selected pricing defaults; requires pricing:read
GET /api/v1/matrices/:idCustom matrix cells; pricing:read
GET /api/v1/matrices/:id/price?row=0&col=0&qty=24Matrix lookup; pricing:read. A missing price is null.
GET /api/v1/production/queue?department=QCDepartment queue; production:read
GET /api/v1/jobs/:id/workflowTasks and current revision; production:read
POST /api/v1/jobs/:id/tasks/:taskId/action{revision, action:"complete"}; production:write
PUT /api/v1/jobs/:id/tasks/:taskId/assignment{revision, assigned_id}; production:write. Null unassigns.
PUT /api/v1/jobs/:id/timing{revision, timing:{enabled:true,production_date:"YYYY-MM-DD"}, reason}; production:write

Use numeric record IDs. Workflow holds still apply. On a 409 conflict, reload the workflow and review before changing it again. Draft creation with a new inline customer also needs customers:write. For POST /api/v1/customers and /api/v1/estimates, persist a unique Idempotency-Key (8–128 letters, digits, dots, colons, underscores or hyphens) before sending. Reuse the same key, credential and body after a timeout: successful replays return the original 201 response with Idempotency-Replayed: true. Changed bodies return 409; rejected requests roll back and do not consume the key. Receipts persist across restarts without automatic expiry. A new credential starts a new namespace, and replayed data is the original response snapshot; use GET for current state. Without the header, creation has no retry protection. Customer hooks run after commit and are not guaranteed exactly-once delivery. This connects an existing runtime; it does not install a Slack or model agent.

Endpoints

RouteWhat it does
GET/api/v1/meShop info + rate limit
GET/api/v1/customersList customers — ?query=, ?limit= (≤100), ?offset=
GET/api/v1/customers/:idOne customer + recent jobs and invoices
POST/api/v1/customersCreate — {name, email?, phone?, company?}; 409 on duplicate email
GET/api/v1/estimatesList — ?status=draft|sent|approved|declined
POST/api/v1/estimatesCreate a draft quote — see below
GET/api/v1/invoicesList — ?status=unpaid|partial|paid|overdue
GET/api/v1/invoices/:idOne invoice + its payments
GET/api/v1/jobsList — ?stage=new|art_approval|prepress|production|qc|shipping|complete
POST/api/v1/jobs/:id/stageMove a job — {stage}; fires automations + webhooks
GET/api/v1/paymentsList payments
GET/api/v1/webhooksList webhook subscriptions
POST/api/v1/webhooksSubscribe — {url, events}; returns the signing secret once
DEL/api/v1/webhooks/:idUnsubscribe

Create an estimate

curl -X POST https://pro.printshopcrm.com/api/v1/estimates \
  -H "Authorization: Bearer psc_live_XXXX" -H "Content-Type: application/json" \
  -d '{
    "customer": { "name": "Jamie Rivera", "email": "jamie@example.edu", "company": "Lakeside High School" },
    "items": [{ "description": "Gildan 5000 Black", "sizes": { "S": 12, "M": 24, "L": 24, "XL": 12 },
                "unit_price": 11.50, "decoration": "Screen Print 3-color front" }],
    "notes": "From the website order form"
  }'

Pass customer_id instead of customer to attach to an existing record. List responses are {"data": [...], "has_more": true|false} — page with limit/offset.

Price matrices

Your own price sheets, under /api/matrices (session auth, not /api/v1). A matrix is a named grid with free-text headings and no built-in meaning — name it anything, label the rows and columns in your own words, and the cells are prices. cells is dense and positional (cells[row][col], null = no price set), so renaming a column never orphans a price.

GET /api/matricesAll matrices, plus the starter templates
GET /api/matrices/:idOne matrix, with every cell
POST /api/matricesCreate — a full matrix, or {"template":"dtf"}
PUT /api/matrices/:idSave
POST /api/matrices/:id/duplicateCopy it, to make a variant
POST /api/matrices/:id/defaultPre-select it on new quotes
DELETE /api/matrices/:idDelete it
POST /api/matrices/importRead a CSV upload or a pasted grid
GET /api/matrices/:id/priceLook one price up
curl "https://pro.printshopcrm.com/api/matrices/3/price?col=11%20oz&qty=30" -b cookies.txt

{ "price": 11.5, "row": "24+", "col": "11 oz", "unit": "piece", "amount": 345 }

row and col take an index or the heading text. Omit row and pass qty, and a matrix whose rows read as quantity bands picks its own row. An empty cell returns 200 with "price": null — that means we don't price that combination, not zero.

Webhooks

Subscribe a URL (Zapier, Make, or your own endpoint) to any of:

contact.created estimate.sent estimate.approved invoice.paid job.stage art.sent art.approved art.rejected opportunity.won opportunity.lost conversation.received — or * for everything.

POST (your URL)
X-PSC-Event: invoice.paid
X-PSC-Signature: t=1755763200,v1=6f1a…e3c9

{ "event": "invoice.paid", "at": "2026-08-21 06:10:00",
  "data": { "invoice": { "id": 41, "number": "INV-1041", "status": "paid",
            "amount_due": 1284, "amount_paid": 1284 },
            "contact": { "id": 7, "name": "Jamie Rivera", "company": "Lakeside High School" } } }

Verifying signatures

const [t, v1] = header.match(/t=(\d+),v1=([0-9a-f]+)/).slice(1)
const expected = crypto.createHmac('sha256', WEBHOOK_SECRET)
  .update(`${t}.${rawBody}`).digest('hex')
// compare with timingSafeEqual; reject if |now - t| > 300s

Failed deliveries retry twice (30 s, then 5 min). Every attempt — success or failure, with the reason — is visible in Settings → Developers.

Data freedom

Your data is yours. Full exports — customers, orders with line items and size grids, payments — are available in the app at any time, in documented CSV/JSON, including after you cancel. We keep customers by being good, not by holding their history hostage.

← Back to the app