Skip to content

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 ​

FieldTypePurpose
typeURIIdentifier for the problem class. Defaults to about:blank.
titlestringShort, human-readable summary. Stable across occurrences.
statusnumberHTTP status code (also in the response line).
codestringMachine-readable error code, snake_case (project_not_found, invalid_scope).
detailstringHuman-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 ​

StatusMeaningTypical code values
400Bad requestinvalid_field, invalid_include, invalid_scope, unknown_resource
401Unauthorizedkauth_missing, JWT validation failures
403Forbiddeninsufficient_scope (scope check failed), forbidden (business rule — e.g. tenant/ownership/lifecycle-state denial)
404Not found{resource}_not_found
409Conflictresource-specific
422Unprocessable entityupstream_missing_id
503Read backend unavailablequery_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 ​

DirectionType
Request bodyapplication/json
Successful responseapplication/json
Error responseapplication/problem+json
Empty response(no body, e.g. 204)

Clients should accept both application/json and application/problem+json on the response side.