Skip to main content

API quickstart

The intranet exposes a versioned JSON API at /api/v1, sharing the app's RBAC, billing engine, and audit trail. The interactive Swagger UI lives at /api-docs, and the OpenAPI 3.1 document at GET /api/v1/openapi (public).

Get a token​

Two options, both created by an admin:

  • Personal token — acts as a user, inherits their role, optional scopes. See Personal API tokens. Starts with cxp_.
  • Service client — exchange client credentials for a short-lived token. See Service clients.
curl -s -X POST https://intranet.example/api/v1/auth/token \
-H 'content-type: application/json' \
-d '{"grant_type":"client_credentials","client_id":"accounting-sync","client_secret":"..."}'

Call the API​

export TOKEN=cxp_...
curl -s https://intranet.example/api/v1/me \
-H "Authorization: Bearer $TOKEN"

Create a time entry:

curl -s https://intranet.example/api/v1/time-entries \
-H "Authorization: Bearer $TOKEN" \
-H 'content-type: application/json' \
-d '{ "projectId": "...", "serviceDate": "2026-10-05", "hours": 2, "minutes": 30,
"description": "Terraform review" }'

Conventions​

  • Durations are integer seconds; money is a decimal string in the record's currency; dates are YYYY-MM-DD.

  • Lists accept limit (max 200, default 50) and offset and return { "data": [...], "meta": { "total", "limit", "offset" } }.

  • Errors use a stable envelope:

    { "error": { "code": "validation_failed", "message": "...", "details": [] } }

    Codes: bad_request, invalid_json, unauthorized, forbidden, not_found, conflict, validation_failed, internal_error.

Endpoint map​

AreaEndpoints
IdentityGET /me, POST /auth/token
CustomersGET/POST /customers, GET/PATCH/DELETE /customers/{id}
ProjectsGET/POST /projects, GET/PATCH/DELETE /projects/{id}, GET/POST /projects/{id}/assignments, DELETE /projects/{id}/assignments/{engineerId}
TimeGET/POST /time-entries, GET/PATCH/DELETE /time-entries/{id}, POST /time-entries/{id}/submit, POST .../approve, POST .../reject
AgreementsGET/POST /agreements, GET/PATCH/DELETE /agreements/{id}, GET/POST /agreements/{id}/allowances, DELETE .../allowances/{allowanceId}
RatesGET/POST /rates, DELETE /rates/{id}
BillingGET /billing/periods, GET /billing/periods/{id}, POST .../close, POST .../reopen, POST /billing/recalculate
ReportsGET /reports/timesheet, GET /reports/sla, GET /reports/export
AdminGET /admin/users, PATCH /admin/users/{id}, GET /admin/audit, GET/POST /admin/tokens, DELETE /admin/tokens/{id}, GET/POST /admin/service-clients, DELETE /admin/service-clients/{id}

Scopes​

customers:read, customers:write, projects:read, projects:write, time:read, time:write, time:review, agreements:read, agreements:write, billing:read, billing:write, reports:read, admin:read, admin:write, or *.

Scopes need the role too

Effective permission = scopes ∩ role. A scope alone never exceeds the role.

Local demo credentials​

Seeded for local testing only:

KindValueRoleScopes
Personal tokencxp_demo_admin_000000000000000000000000admin*
Service clientdemo-accounting / demo-accounting-secretfinancebilling:read, reports:read

Not yet built​

  • Rate limiting / per-token quotas.
  • Token introspection endpoint.
  • Service-client secret rotation.