Versioning & deprecation
What changes without warning, what triggers a new version, and how long a retired version keeps working.
This policy governs the stability of the Public API (/api/public/v1/*, REST and
GraphQL). It does not apply to the internal routes the first-party web app uses,
which carry no stability guarantee.
Version scheme
The Public API is versioned in the URL path:
/api/public/v1/...
/api/public/v2/... (a future major version)- The path segment (
v1,v2, …) is the major version. A new major version ships under a new path; the old path keeps serving its own contract unchanged. - The OpenAPI document's
info.versionis a semantic version whose MAJOR equals the path version (v1→1.x.x). MINOR increments on backward-compatible additions; PATCH on documentation fixes with no behavioral change. - GraphQL shares the same surface and version.
Non-breaking (within a major version)
Clients must tolerate these without breaking:
- New endpoints, queries, mutations, and resources. (A new resource also adds its
:read/:writescope tokens. Because scopes carry no wildcards, existing keys do not auto-gain access; they keep working and are re-issued to opt in.) - New optional request fields, query parameters, or GraphQL arguments.
- New fields on response objects; REST clients must ignore unknown fields.
- New enum values: clients must tolerate values they do not recognize.
- New error
codes in the catalog. - Relaxed validation, and performance or wording changes.
Breaking (requires a new major version)
Never made in place; shipped under a new version path:
- Removing or renaming an endpoint, field, query parameter, enum value, or error code.
- Changing a field's type, or making an optional field required.
- Changing the shape of the pagination envelope, the cursor format, or the error envelope.
- Tightening validation so previously-accepted input is rejected.
- Changing authentication, scoping, or default behavior in an observable way.
Deprecation signaling
When something is slated for removal, we signal it well in advance:
Deprecationheader (RFC 8594) on affected responses.Sunsetheader (RFC 8594) once a removal date is scheduled.- The OpenAPI document marks deprecated operations with
deprecated: true; GraphQL uses the@deprecateddirective.
Support window
After a new major version reaches stable release, the previous major is supported for at least 6 months before its sunset date:
v(N+1)ships and is documented as stable.vNresponses begin carryingDeprecation(and, once scheduled,Sunset) headers.vNkeeps serving its unchanged contract for ≥ 6 months from thev(N+1)stable date.- On or after the
Sunsetdate,vNmay be removed.
The window is a minimum; a longer one may be granted and announced.
Changelog
v1
-
1.6.0: adds
GET /tasks/:id,GET /activities/:id, andGET /assignments/:id, plus the matching GraphQL singulars (task(id),activity(id),assignment(id)) and MCP getters (get_taskwas already present;get_activityandget_assignmentjoin it). A missing id and another team's id both 404 asnot_foundon REST and both resolve tonullon GraphQL. Additive; existing keys are unaffected. -
1.5.0: adds
organization_cleared_atto the time-entry resource (REST and MCP; the GraphQL field isorganizationClearedAt). It is set when the client an entry named was deleted, which is what separates "this hour never had a client" from "this hour's client is gone" -- both serialize withorganization_id: null, so before this the two were identical over the wire. An entry carrying the stamp is not attributed to its project's client; it waits to be assigned one. The field is a timestamp and deliberately not the deleted client's id, so it carries no identity. Additive; existing clients are unaffected. -
1.4.0: a
cursorthat will not decode is now400 bad_requestinstead of silently serving the first page. Previously any unparseable value was treated as "no cursor was sent", so a client whose cursor was truncated or mangled in its own plumbing got page one back withhasMore: trueand a freshnextCursor, and the ordinary paging loop ran forever with nothing on either side reporting a fault. An absent or emptycursorstill means the first page, unchanged.This is an observable behavior change, so it gets its own entry and a MINOR bump rather than passing as a documentation patch. It is deliberately not treated as the "tightening validation" case that the rules above make breaking: the only caller who can observe it is one sending a cursor Steerd never issued, and
cursorhas always been documented as an opaque value taken from a previouspage.nextCursor. The neighboring case, a cursor that decodes but names a differentsortororder, has always been a 400. This closes the one hole in that rule rather than adding a new restriction, and it does not reuse the spent 1.3.0 exception. -
1.3.0: renames the
documentsresource tofiles./documentsbecomes/files, and thedocuments:read/documents:writescope tokens becomefiles:read/files:write. The name was given up so that "documents" can mean accounting records (Belege) in a later release, rather than arbitrary uploaded files.By the rules above this is a breaking change and would require a new major version. It was made in place, once, because it predates the first issued API key and any public launch, so there is no client that can observe it. That exception is spent: the rule stands for everything after this entry.
-
trips, added 2026-07-25, with no version of its own. A read-only trips resource over REST (/trips,/trips/{id},/trips/{id}/expenses) and MCP, with a single new scope token,trips:read. There is deliberately notrips:writeand no GraphQL field: a trip is created, submitted and approved in the app, where approval seals its numbers.The MINOR bump the rules above require was not made at the time, so this shipped inside
1.2.0, which the document had reported since 2026-07-23, and is recorded here after the fact rather than renumbered. Nothing about it was breaking, and a version number no client ever saw is not worth inventing. The rule stands; this is the entry that should have existed. -
1.2.0: adds the
documentsandattachmentsresources (REST-only: list, get/download, multipart upload, delete) with newdocuments:read/documents:writeandattachments:read/attachments:writescope tokens. Uploads are multipart and not idempotent. Additive; existing keys are unaffected until re-issued with the new scopes. (Renamed tofilesin 1.3.0; the names here are the ones 1.2.0 actually shipped.) -
1.1.0: adds the
time_entriesresource (REST + GraphQL: list, get, create, update, delete, and a range summary) with newtime_entries:read/time_entries:writescope tokens. Additive; existing keys are unaffected until they are re-issued with the new scope. -
1.0.0: initial
v1surface (organizations, contacts, projects, employees, cvs, tasks, activities, assignments, search), with cursor pagination, idempotency, API-key authentication, OpenAPI + Swagger UI, and the GraphQL interface.