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.
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.
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/pricing | Resolved price book, custom matrix summaries and selected pricing defaults; requires pricing:read |
GET /api/v1/matrices/:id | Custom matrix cells; pricing:read |
GET /api/v1/matrices/:id/price?row=0&col=0&qty=24 | Matrix lookup; pricing:read. A missing price is null. |
GET /api/v1/production/queue?department=QC | Department queue; production:read |
GET /api/v1/jobs/:id/workflow | Tasks 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.
| Route | What it does |
|---|---|
GET/api/v1/me | Shop info + rate limit |
GET/api/v1/customers | List customers — ?query=, ?limit= (≤100), ?offset= |
GET/api/v1/customers/:id | One customer + recent jobs and invoices |
POST/api/v1/customers | Create — {name, email?, phone?, company?}; 409 on duplicate email |
GET/api/v1/estimates | List — ?status=draft|sent|approved|declined |
POST/api/v1/estimates | Create a draft quote — see below |
GET/api/v1/invoices | List — ?status=unpaid|partial|paid|overdue |
GET/api/v1/invoices/:id | One invoice + its payments |
GET/api/v1/jobs | List — ?stage=new|art_approval|prepress|production|qc|shipping|complete |
POST/api/v1/jobs/:id/stage | Move a job — {stage}; fires automations + webhooks |
GET/api/v1/payments | List payments |
GET/api/v1/webhooks | List webhook subscriptions |
POST/api/v1/webhooks | Subscribe — {url, events}; returns the signing secret once |
DEL/api/v1/webhooks/:id | Unsubscribe |
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.
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/matrices | All matrices, plus the starter templates |
GET /api/matrices/:id | One matrix, with every cell |
POST /api/matrices | Create — a full matrix, or {"template":"dtf"} |
PUT /api/matrices/:id | Save |
POST /api/matrices/:id/duplicate | Copy it, to make a variant |
POST /api/matrices/:id/default | Pre-select it on new quotes |
DELETE /api/matrices/:id | Delete it |
POST /api/matrices/import | Read a CSV upload or a pasted grid |
GET /api/matrices/:id/price | Look 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.
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" } } }
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.
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.