API Reference · Overview & quickstart
API Reference

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

EnvironmentBase URLNotes
Docker deploymenthttp://localhost/apinginx 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 developmenthttp://localhost:4000The 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:

1 · Register — 201 returns the account and a JWT
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…"
}
2 · Issue a developer key with the JWT
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"
  }
}
3 · Call the public surface with the key
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

Signed in? 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:

Error response shape (shared ApiError type)
{
  "statusCode": 404,
  "error": "Not Found",
  "message": "Project not found."
}
statusCodeWhen you will see it
400Body or query failed schema validation; unknown template category or templateId; missing multipart file; dimension outside 1–3000 mm
401Missing/expired JWT; bad credentials; missing, invalid, or revoked API key
403Authenticated but not allowed — e.g. the 3-project free-tier cap, or a non-admin calling an admin route
404Resource missing — or owned by another account (foreign ids are indistinguishable from missing ones)
409Duplicate unique value, e.g. registering an email that already has an account
413Upload over the 25 MB artwork limit
415Upload that is not an image/* mime type
500Unexpected 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 /assets which is multipart/form-data by necessity. Dates are ISO 8601 strings; ids are cuid strings.
  • Machine-readable errors. Always the ApiError shape above — branch on statusCode, display message, never regex the body.
  • Strict schemas. Bodies and query strings reject unknown fields (additionalProperties: false). Send exactly the documented fields.
  • Small, stable catalog. /v1/templates returns 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

MethodPathAuthWhat it does
GET/healthLiveness probe
POST/auth/registerCreate an account; returns a JWT
POST/auth/loginExchange credentials for a JWT
GET/auth/meJWTThe account behind a token
GET/templatesTemplate catalog with filters
GET/templates/:idOne template definition
GET/projectsJWTList your projects
POST/projectsJWTCreate a project
GET/projects/:idJWTRead one project
PUT/projects/:idJWTPartial update
DELETE/projects/:idJWTDelete a project
POST/assetsJWTUpload an image (multipart)
GET/assetsJWTList your uploads
GET/files/:nameFetch a stored upload
POST/api-keysJWTIssue a developer key
GET/api-keysJWTList your keys
DELETE/api-keys/:idJWTRevoke a key
GET/v1/templatesx-api-keyPublic catalog surface
GET/v1/templates/:idx-api-keyOne template, by key

Continue to auth endpoints