Bearer-token auth, rate-limited, signed webhooks. This page documents exactly what ships today, nothing more.
All API requests use Laravel Sanctum personal access tokens. Create one in the admin under API tokens, copy it once, and pass it as a bearer token in the Authorization header. Tokens act with the full permissions of the user who created them, so create them from an account with the access you want the integration to have. Revoking a token from the admin takes effect immediately, and deactivating the staff member who owns it cuts API access at the same time.
curl https://printersfriend.com/api/v1/me \
-H "Authorization: Bearer <your-token>"
The base URL is https://printersfriend.com/api/v1. The API is versioned by URL prefix; breaking changes will ship in a new major version. There is no separate sandbox environment today: use a trial workspace for testing.
Errors follow the standard Laravel JSON format: 401 unauthenticated, 403 forbidden, 404 not found, 422 validation, 429 rate limited, 5xx server.
// 422 example
{
"message": "The given data was invalid.",
"errors": {
"to_stage": ["The selected to stage is invalid."]
}
}
List endpoints use standard Laravel page-number pagination. Pass ?page=2 to move through results. Responses include the usual current_page, last_page, total and links fields. Page size is fixed per endpoint: 50 for customers and orders, 100 for inventory.
60 requests per minute on read endpoints, 20 per minute on write endpoints, counted per authenticated user. Hitting a limit returns 429 with a Retry-After header in seconds.
Returns the id, name and email of the user the token belongs to. Useful as a connectivity check.
List customer organisations, ordered by name, 50 per page.
| Param | Type | Description |
|---|---|---|
search | string | Filter by organisation name |
page | integer | Page number |
{
"data": [
{
"id": 12,
"name": "Northside FC",
"slug": "northside-fc",
"price_list": "Club rate",
"discount": 10,
"orders_count": 14,
"open_orders": 2
}
],
"current_page": 1,
"last_page": 1,
"total": 1
}
Get a single organisation with contacts, price list, payment terms, default carrier and lifetime paid invoice value (lifetime_value_cents).
List orders, newest first, 50 per page, with lines and customer attached.
| Param | Type | Description |
|---|---|---|
stage | string | artwork, print_queue, in_production, qc_pack, dispatched, cancelled |
customer | string | Filter by customer organisation slug |
page | integer | Page number |
Get a single order with lines, totals (including currency), stage history and invoices.
Create an order in the artwork stage. Body: { "customer_id": 12, "lines": [{ "description": "Crew tee, 2 col front", "quantity": 60, "unit_price_cents": 1850 }], "due_by": "2026-07-01", "notes": "Club drop 3" }. Totals and tax are calculated server side.
Advance an order to another production stage. Body: { "to_stage": "in_production" }. Fires the standard side effects: customer comms, stock consumption, and the order.stage_changed or order.dispatched webhook.
List SKUs with live stock, 100 per page. Each SKU includes on_hand, reserved, available, reorder_point and a status field.
| Param | Type | Description |
|---|---|---|
search | string | Filter by SKU code |
low | boolean | Only SKUs at or below their reorder point |
page | integer | Page number |
Create a subscription in the admin under Webhook subscriptions. Each subscription has a target URL, an event list (or * for all events), and a per-subscription signing secret generated on create. Subscriptions can optionally be scoped to a single customer organisation.
Two events are delivered today, both fired when an order moves stage:
order.stage_changed: an order moved to any non-dispatched stageorder.dispatched: an order was dispatchedThe payload carries order_number, from_stage, to_stage, customer and due_by. The subscription form also lists invoice.issued, invoice.paid and artwork.approved: you can subscribe to them now, and delivery for those events is planned. We will update this page when they go live.
Every webhook POST includes:
X-PF-Event: the event nameX-PF-Signature: HMAC-SHA256 of the request body using the subscription's signing secret, prefixed with sha256=User-Agent: PrintersFriend-Webhooks/1.0Verify with:
// Node.js verification
const crypto = require('crypto');
const sig = req.headers['x-pf-signature'];
const expected = 'sha256=' + crypto.createHmac('sha256', secret).update(req.rawBody).digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) {
return res.status(401).end();
}
If your endpoint returns anything other than 2xx, we retry with exponential backoff: 1m, 5m, 15m, 1h, 4h (5 attempts in total). Every delivery attempt is recorded with its response status, visible in the admin. After 20 consecutive failures the subscription is deactivated; re-enable it from the admin once your endpoint is healthy.
There are no official SDKs yet. The API is plain REST with bearer-token auth, so any HTTP client works out of the box. Email developers@printersfriend.com to tell us which language you want first.
API questions go to developers@printersfriend.com with response within 1 business day. Status and incidents live on the status page.