API Reference · Overview & quickstart
User Guide
Developer
A small JSON api, documented to the byte
Every endpoint below exists in the running api and was verified with real requests. Request schemas are enforced server-side; responses match the TypeScript types in @pacfully/shared. Point a script — or an AI agent — at it with confidence.
Base URLs
| Environment | Base URL | Notes |
|---|---|---|
| Docker deployment | http://localhost/api | nginx proxies /api/* to the api with the prefix stripped — /api/v1/templates is the same route as /v1/templates. Every example on these pages uses this base; substitute your own domain for http://localhost in a real deployment. |
| Local development | http://localhost:4000 | The bare-metal Fastify dev server, started with npm run dev:api — no /api prefix in this mode. |
Two authentication mechanisms
The account surface (projects, assets, key management) uses a JWT sent as Authorization: Bearer <token>. Tokens come from register/login and live for 7 days. The public v1 surface uses a developer key sent as x-api-key: pf_…. Keys are issued from the account surface and never expire until revoked.
Quickstart: zero to a keyed request
This full flow runs verbatim against the Docker deployment (see deployment) — substitute your domain for http://localhost. On a bare-metal dev api, drop the /api prefix and use http://localhost:4000 instead. Each step shows the real response shape:
curl -X POST http://localhost/api/auth/register \
-H "Content-Type: application/json" \
-d '{"email":"dev@example.com","password":"s3cure-passphrase","name":"Dev"}'
{
"user": {
"id": "cml4k9x2b0000jq8f3h5a1d2e",
"email": "dev@example.com",
"name": "Dev",
"createdAt": "2026-08-06T10:15:30.000Z"
},
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9…"
}curl -X POST http://localhost/api/api-keys \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9…"
{
"apiKey": {
"id": "cml4ka7cd0001jq8f9b2c4e6a",
"userId": "cml4k9x2b0000jq8f3h5a1d2e",
"key": "pf_9f2c4e6a8b0d1f3e5a7c9b8d7e6f5a4b3c2d1e0f9a8b7c6d",
"createdAt": "2026-08-06T10:16:02.000Z"
}
}curl http://localhost/api/v1/templates \
-H "x-api-key: pf_9f2c4e6a8b0d1f3e5a7c9b8d7e6f5a4b3c2d1e0f9a8b7c6d"
{
"templates": [ { "id": "mailer-box", "name": "Mailer Box", … }, … ],
"total": 11,
"categories": [ { "id": "folding-cartons", "label": "Folding Cartons", "count": 1 }, … ]
}Keys are managed from the dashboard too
Dashboard → API keys mints, lists, and revokes keys without touching curl. The API path above does the same thing for scripts. Keys are returned in full and can be re-read with GET /api-keys — treat them like passwords anyway.The error contract: ApiError
Every failure — validation, auth, not-found, oversized uploads, unexpected server errors — returns the same JSON shape. A single error handler normalizes Fastify, ajv, JWT, multipart, and Prisma errors into it:
{
"statusCode": 404,
"error": "Not Found",
"message": "Project not found."
}| statusCode | When you will see it |
|---|---|
| 400 | Body or query failed schema validation; unknown template category or templateId; missing multipart file; dimension outside 1–3000 mm |
| 401 | Missing/expired JWT; bad credentials; missing, invalid, or revoked API key |
| 403 | Authenticated but not allowed — e.g. the 3-project free-tier cap, or a non-admin calling an admin route |
| 404 | Resource missing — or owned by another account (foreign ids are indistinguishable from missing ones) |
| 409 | Duplicate unique value, e.g. registering an email that already has an account |
| 413 | Upload over the 25 MB artwork limit |
| 415 | Upload that is not an image/* mime type |
| 500 | Unexpected server error — message is a fixed string, details go to the server log |
Notes for AI agents
This api is built to be consumed programmatically, including by LLM-driven agents:
- JSON-first. Every request and response body is JSON, except
POST /assetswhich is multipart/form-data by necessity. Dates are ISO 8601 strings; ids are cuid strings. - Machine-readable errors. Always the
ApiErrorshape above — branch onstatusCode, displaymessage, never regex the body. - Strict schemas. Bodies and query strings reject unknown fields (
additionalProperties: false). Send exactly the documented fields. - Small, stable catalog.
/v1/templatesreturns all 11 templates in one call — no pagination to choreograph. - Typed contract. Response shapes are the exported types of
@pacfully/shared(User,Project,Asset,ApiKey,ParametricTemplate,ApiError) — import them if you are in TypeScript, mirror them otherwise.
Endpoint index
| Method | Path | Auth | What it does |
|---|---|---|---|
| GET | /health | — | Liveness probe |
| POST | /auth/register | — | Create an account; returns a JWT |
| POST | /auth/login | — | Exchange credentials for a JWT |
| GET | /auth/me | JWT | The account behind a token |
| GET | /templates | — | Template catalog with filters |
| GET | /templates/:id | — | One template definition |
| GET | /projects | JWT | List your projects |
| POST | /projects | JWT | Create a project |
| GET | /projects/:id | JWT | Read one project |
| PUT | /projects/:id | JWT | Partial update |
| DELETE | /projects/:id | JWT | Delete a project |
| POST | /assets | JWT | Upload an image (multipart) |
| GET | /assets | JWT | List your uploads |
| GET | /files/:name | — | Fetch a stored upload |
| POST | /api-keys | JWT | Issue a developer key |
| GET | /api-keys | JWT | List your keys |
| DELETE | /api-keys/:id | JWT | Revoke a key |
| GET | /v1/templates | x-api-key | Public catalog surface |
| GET | /v1/templates/:id | x-api-key | One template, by key |