Appearance
Responses & Errors
Successful responses
List endpoints return a { data, meta } envelope:
json
{
"data": [
{ "Id": 102345, "ProjectName": "Acme HQ Buildout", "BusinessState": "Open", "DateCreated": "2026-01-15T09:21:44Z" },
{ "Id": 102346, "ProjectName": "Riverside Renovation", "BusinessState": "Draft", "DateCreated": "2026-01-18T14:02:11Z" }
],
"meta": {
"total": 145,
"limit": 50,
"offset": 0,
"has_more": true
}
}meta.has_more is computed: total > offset + data.length. Pages cleanly.
Single-record endpoints (GET /:id, POST / create, PATCH /:id) return the entity directly — no envelope:
json
{ "Id": 102345, "ProjectName": "Acme HQ Buildout", "BusinessState": "Open", ... }Delete endpoints return 204 No Content with an empty body.
Create endpoints also set a Location header pointing at the new resource:
HTTP/1.1 201 Created
Location: /api/v2/projects/102345
Content-Type: application/json
{ "Id": 102345, ... }Error responses
All errors follow RFC 7807 Problem Details. Content type is application/problem+json.
json
{
"type": "about:blank",
"title": "Project not found",
"status": 404,
"code": "project_not_found",
"detail": "No project with Id 99999999 exists."
}Standard fields
| Field | Type | Purpose |
|---|---|---|
type | URI | Identifier for the problem class. Defaults to about:blank. |
title | string | Short, human-readable summary. Stable across occurrences. |
status | number | HTTP status code (also in the response line). |
code | string | Machine-readable error code, snake_case (project_not_found, invalid_scope). |
detail | string | Human-readable explanation specific to this occurrence. |
Additional fields may appear for specific error types — e.g. invalid_scope includes scope: '...' and allowed: [...]; forbidden includes resource and resource_id when the rejection is tied to a specific record:
json
{
"type": "about:blank",
"title": "Forbidden",
"status": 403,
"code": "forbidden",
"detail": "Projects in simple or advanced mode do not allow plan modifications.",
"resource": "project",
"resource_id": 102345
}This example is the business-rule check on Milestone create and update: a Project whose JobCostingMode is Simple or Advanced doesn't support plan modifications at all, so any attempt to add or change a Milestone under it is rejected with this forbidden code rather than insufficient_scope — retrying with a broader token won't help.
Common status codes
| Status | Meaning | Typical code values |
|---|---|---|
| 400 | Bad request | invalid_field, invalid_include, invalid_scope, unknown_resource |
| 401 | Unauthorized | kauth_missing, JWT validation failures |
| 403 | Forbidden | insufficient_scope (scope check failed), forbidden (business rule — e.g. tenant/ownership/lifecycle-state denial) |
| 404 | Not found | {resource}_not_found |
| 409 | Conflict | resource-specific |
| 422 | Unprocessable entity | upstream_missing_id |
| 503 | Read backend unavailable | query_engine_unavailable |
Validation errors
When the request body or query string fails Zod validation, the response includes an issues array:
json
{
"title": "Invalid query payload",
"status": 400,
"code": "invalid_request",
"detail": "The request failed validation. See `issues` for details.",
"issues": [
{ "path": ["where", "TenantId"], "message": "Unknown key \"TenantId\" in where clause.", "code": "custom" },
{ "path": ["limit"], "message": "Number must be less than or equal to 200", "code": "too_big" }
]
}path is an array following the JSON Pointer style for nested fields — useful for highlighting bad input in a form UI.
Batch query errors
A single bad entry in a POST /api/v2/query batch fails the whole batch with a 400. The response identifies which entry failed:
json
{
"title": "Invalid query payload in batch entry",
"status": 400,
"code": "invalid_batch_query",
"slug": "invoices",
"issues": [...]
}Validation runs before any DB work, so a bad entry doesn't waste resources on the good ones — but it also doesn't return partial results. Use one-at-a-time per-resource queries if you want each to succeed/fail independently.
Content-Type expectations
| Direction | Type |
|---|---|
| Request body | application/json |
| Successful response | application/json |
| Error response | application/problem+json |
| Empty response | (no body, e.g. 204) |
Clients should accept both application/json and application/problem+json on the response side.