Skip to content

API resources

Public API resources

All examples use the canonical production origin and an example token:

Terminal window
export HAMSTIK_TOKEN='hstk_pat_example_replace_me'
export ORG_SLUG='blackboard-studios'
export PROJECT_KEY='HAM'

Organizations

Terminal window
curl -H "Authorization: Bearer $HAMSTIK_TOKEN" \
"https://hamstik.com/api/v1/organizations?limit=50"
curl -H "Authorization: Bearer $HAMSTIK_TOKEN" \
"https://hamstik.com/api/v1/organizations/$ORG_SLUG"

The list is the intersection of the PAT’s resource grants and the user’s current memberships. A Project-restricted PAT may see only the parent Organization needed to address that Project. That minimal list entry contains only id, slug, name, and suspended; it does not authorize a direct Organization detail read. Suspended Organizations may still appear in the list with suspended: true, but resource use returns 403 ORGANIZATION_SUSPENDED.

Projects

Terminal window
curl -H "Authorization: Bearer $HAMSTIK_TOKEN" \
"https://hamstik.com/api/v1/organizations/$ORG_SLUG/projects"
curl -H "Authorization: Bearer $HAMSTIK_TOKEN" \
"https://hamstik.com/api/v1/organizations/$ORG_SLUG/projects/$PROJECT_KEY"

Project keys are stable public identifiers. Resolution is case-insensitive and the response returns the stored canonical key. A Project grant does not act as a Project role and cannot bypass Organization membership or suspension. Project projections include revision and nullable archivedAt; detail reads and successful lifecycle mutations return a quoted "project-<revision>" ETag. Project lists default to unarchived items and accept archived=true for only archived items. Archived Projects remain readable and their existing Work Items remain mutable, but new Work Items and Sprints fail with 409 PROJECT_ARCHIVED.

The acting user’s effective Project role bounds every Project-scoped call: Projects the user has no access to resolve as 404, reads require read-only access or above, and mutations (Work Items, Sprints, comments, attachments) require Contributor access or above. Project Admins may manage project-scoped resources; Organization-level operations keep their Organization role requirements. See Projects for the role model.

Create a Project with an Organization-granted PAT. The caller must be an Organization administrator or owner, and Idempotency-Key is required. Omit key to use Hamstik’s canonical suggestion from the Project name:

Terminal window
curl -i -X POST \
-H "Authorization: Bearer $HAMSTIK_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: project-20260906-01" \
-d '{"name":"Website Redesign","key":"WEB","description":"Project scope","color":"#3b82f6"}' \
"https://hamstik.com/api/v1/organizations/$ORG_SLUG/projects"

The response is the complete created Project projection. A Project-restricted PAT cannot create a Project because the new Project is not covered by that grant.

Release audit packages

An authorized Project reader can list generated packages with GET /api/v1/organizations/{organizationSlug}/projects/{projectKey}/release-audit-reports and page with the returned opaque cursor. A Project manager with project:write can generate a dossier by posting {"kind":"dossier","releaseVersionId":"..."} or a UTC change register by posting {"kind":"register","from":"2026-01-01T00:00:00Z","through":"2026-01-31T23:59:59Z"} to the same collection. Include an Idempotency-Key. Generation uses the current Project authorization and stores one immutable report package.

GET .../release-audit-reports/{reportId} returns the exact saved JSON document; ?format=csv returns a ZIP containing the same report.json, a manifest, and CSV tables. The X-Content-SHA256 response header is the digest of the exact JSON document bytes. A Project-restricted PAT can only access its granted Project. Read and export access is recorded. See Release audit reports for evidence provenance, bounds, format, and retention semantics.

Public user identity and Organization usernames

New user references use the immutable public ID, for example usr_cPbfeqnghA-RLpDVOMQhHg. Its exact format is usr_<22 Base64URL characters>: 128 cryptographically random bits encoded as unpadded Base64URL, validated by ^usr_[A-Za-z0-9_-]{21}[AQgw]$. It is an opaque URL/API identifier, not a secret. The legacy internal UUID remains only where v1 compatibility requires it. Email is a private account credential and is never a public user identifier. A removed account keeps its reserved public ID, but normal profile lookup no longer finds it.

Usernames are nullable membership identities: a username is unique only inside its Organization and may differ across Organizations. It is always displayed with its Organization, never as a global Hamstik handle. GET /api/v1/me includes publicId and the allowed organizations: [{id, slug, name, username}] context.

The cookie-authenticated account control PATCH /api/me/organizations/{organizationSlug}/username manages only the signed-in user’s current membership and is not a /api/v1 operation. Its strict body is { "username": string | null }; values are normalized to lowercase, limited to 1–64 ASCII letters/digits/./_/-, and cannot begin or end with punctuation. null clears the username. A collision within that Organization returns 409 USERNAME_TAKEN.

GET /api/v1/me retains its legacy top-level id for v1 compatibility and also returns the immutable publicId, the caller’s own email, PAT authentication context, defaultOrganization, and the allowed Organization array. The Organization array is ordered by Organization name and id. Email is not returned by member, profile, Work Item, link, activity, or attachment projections.

Labels

Organization-governed Work Item Attributes have a separate stable-key contract and standalone authorization scope. See the Attributes API guide for discovery, enablement, assignments, projections, and lifecycle.

List labels with GET /api/v1/organizations/$ORG_SLUG/projects/$PROJECT_KEY/labels. The response is ordered by name ASC, id ASC, uses the standard collection envelope, and caps limit at 100. Each item is {id, name, color, createdAt}. Organization administrators and owners can create a label with project:write; names are trimmed and stored lowercase and the default color is #6366f1.

Attach or detach a label at .../work-items/HAM-42/labels. Both operations require work_item:write, Idempotency-Key, and the Work Item ETag in If-Match. The complete Work Item projection and its new ETag are returned. An already-attached label is a successful no-op with no revision or activity change; an absent assignment cannot be detached (404). A label from another Project is also hidden as 404. Actual attach and detach each increment the Work Item revision and write one label activity event. Assignment accepts either {labelId: UUID} or the case-insensitive project label name as {label: "frontend"} (the labelId field also accepts a label name for first-party client compatibility). Detachment accepts either the label UUID or the URL-encoded label name in the {labelId} path segment.

Create a Project label with project:write as an Organization administrator or owner:

{"name":"frontend","color":"#6366f1"}

Names are trimmed, stored lowercase, and limited to 100 characters. Color is optional and defaults to #6366f1; it must be a six-digit hex color. A case-insensitive duplicate returns 409 CONFLICT.

Teams

Teams come in two distinct kinds, and neither expands Project access:

  • Access Teams are Organization-scoped groups of active Organization members. They are prepared for future permissions; in this release they grant nothing. Reads require organization:read; create, update, member add/remove, and delete require the Organization-administration write ceiling (attribute:write) plus owner/admin authorization enforced in-transaction.
  • Delivery Teams are Project-scoped groups used for planning, ownership, and reporting. They never grant access. Reads require project:read; writes require project:write plus effective Project admin, re-derived from live policy and grants inside the transaction.

Routes (replace $ORG_SLUG, $PROJECT_KEY, $TEAM_UUID, and $USER_UUID):

GET /api/v1/organizations/$ORG_SLUG/access-teams
POST /api/v1/organizations/$ORG_SLUG/access-teams
GET /api/v1/organizations/$ORG_SLUG/access-teams/$TEAM_UUID
PATCH /api/v1/organizations/$ORG_SLUG/access-teams/$TEAM_UUID
DELETE /api/v1/organizations/$ORG_SLUG/access-teams/$TEAM_UUID
GET /api/v1/organizations/$ORG_SLUG/access-teams/$TEAM_UUID/members
POST /api/v1/organizations/$ORG_SLUG/access-teams/$TEAM_UUID/members
DELETE /api/v1/organizations/$ORG_SLUG/access-teams/$TEAM_UUID/members/$USER_UUID
GET /api/v1/organizations/$ORG_SLUG/projects/$PROJECT_KEY/delivery-teams[?includeArchived=true]
POST /api/v1/organizations/$ORG_SLUG/projects/$PROJECT_KEY/delivery-teams
GET /api/v1/organizations/$ORG_SLUG/projects/$PROJECT_KEY/delivery-teams/$TEAM_UUID
PATCH /api/v1/organizations/$ORG_SLUG/projects/$PROJECT_KEY/delivery-teams/$TEAM_UUID
DELETE /api/v1/organizations/$ORG_SLUG/projects/$PROJECT_KEY/delivery-teams/$TEAM_UUID
POST /api/v1/organizations/$ORG_SLUG/projects/$PROJECT_KEY/delivery-teams/$TEAM_UUID/archive
POST /api/v1/organizations/$ORG_SLUG/projects/$PROJECT_KEY/delivery-teams/$TEAM_UUID/unarchive
GET /api/v1/organizations/$ORG_SLUG/projects/$PROJECT_KEY/delivery-teams/$TEAM_UUID/members
POST /api/v1/organizations/$ORG_SLUG/projects/$PROJECT_KEY/delivery-teams/$TEAM_UUID/members
DELETE /api/v1/organizations/$ORG_SLUG/projects/$PROJECT_KEY/delivery-teams/$TEAM_UUID/members/$USER_UUID

Lists accept limit (1–100) and cursor, returning the standard {items, page} envelope. Each Team item is {id, name, description, revision, memberCount, createdAt, updatedAt} plus projectId and archivedAt for Delivery Teams. Membership items are {userId, publicId, name, email, addedAt}. Create bodies are {name, description?}; update bodies are {name?, description?, reason?} and require an exact If-Match Team ETag ("team-<revision>" or *). A duplicate normalized name — case and Unicode folding are compared, including archived teams — returns 409 CONFLICT within its scope (Organization for Access Teams, Project for Delivery Teams); the same name in another Project or in the other team type is allowed.

Mutations are idempotent through Idempotency-Key, and replays re-run the live authority check: a manager demoted or de-scoped after the original request cannot replay a cached success. Membership adds return {userId, added} with 201 when created and 200 when the membership already existed; removals return 204 and are idempotent.

Only active members can join. Access Team members must be active Organization members; Delivery Team members must additionally hold effective access to the Project, including inherited default access. Losing Organization access or Project access removes the affected memberships transactionally and keeps audit history; restoring access or unarchiving a team never restores members silently. Deleting an Access Team is refused with 409 CONFLICT while a registered authorization reference still exists; a Delivery Team can be hard-deleted only while it has no members and no business-data references (capacity reporting in particular) — otherwise archive it. Archived Delivery Teams stay readable and keep their reporting identity but cannot gain members or be newly selected. Every lifecycle and membership change is recorded in an append-only audit trail with stable team and member identifiers. See Teams for the product-level overview.

Boards

GET /api/v1/organizations/$ORG_SLUG/projects/$PROJECT_KEY/boards lists a Project’s board configuration for project:read. It accepts limit and cursor and returns the standard {items, page} envelope ordered by name ASC, id ASC; each item is {id, name, revision, sortOrder, groupBy, columns, filter, createdAt, updatedAt}. A column is {id, title, status, wipLimit} where status is a canonical Work Item status or null for a status-less column, and wipLimit is a whole number from 1 to 10000 or null for no limit. groupBy is the board’s persisted swimlane default — one of none, assignee, priority, parent, or epic (none for boards that predate grouping); a viewer’s temporary URL override is never persisted and never appears here.

The official CLI can read this resource through Public API passthrough:

Terminal window
hamstik --json --no-input api request "/api/v1/organizations/$ORG_SLUG/projects/$PROJECT_KEY/boards" --query limit=50

Pass the returned page.nextCursor as --query cursor=... for the next page.

WIP limits are advisory configuration. They drive the per-column count/limit and the At limit/Over limit indicator in the board UI; they never block a transition, drag, keyboard move, bulk change, import, automation, or API write. The projection is scoped to a Project the caller is authorized to read and does not include an Organization identifier. See Boards for the counting and authorization semantics.

Sprints

GET .../projects/$PROJECT_KEY/sprints lists Sprints with dated Sprints first (startDate ASC, id ASC) and undated Sprints last. The cap is 100. Each Sprint is {id, name, state, startDate, endDate, goal, targetPoints, createdAt, updatedAt, revision} and reads return an ETag such as "sprint-1".

Organization administrators and owners create Sprints with project:write and an Idempotency-Key. New Sprints start in future; only future → active and active → done are legal. Completing a Sprint with unfinished Work Items requires {completionAction: {mode: "backlog"}} or a future Sprint target. The create body is strict: {name, startDate?, endDate?, goal?, targetPoints?}; dates accept RFC 3339 offsets, explicit null clears an optional field, and endDate cannot precede startDate when both are present. The Sprint collection limit is 100 and the default page size is 50. For compatibility, existing Work Item create and update routes still accept a Sprint in the same Project even when that Sprint is already done; this is a documented limitation and is not changed by the Sprint API.

POST .../sprints/{sprintId}/transitions also requires If-Match with the Sprint ETag. GET .../transitions reports requiresCompletionAction for the active → done edge. Invalid edges return INVALID_SPRINT_TRANSITION, a second active Sprint returns ACTIVE_SPRINT_EXISTS, missing completion data returns SPRINT_HAS_UNFINISHED_WORK_ITEMS, and an invalid target returns INVALID_COMPLETION_TARGET.

Read delivery metrics with GET .../sprints/{sprintId}/report. See Sprint reports for the response fields, denominator semantics, history reconstruction rules, and the paginated change feed.

Read the aggregate capacity planning view with GET .../sprints/{sprintId}/capacity-planning (scope project:read). The response carries per-member point and time ledgers (capacity, committed, assigned, remaining, variance, overload status, unknown counts), the unassigned bucket, Delivery Team roll-ups, the points forecast (mean rate of the last three completed Sprints) and time projection, derived risk, the recorded planning baseline, assumptions, and the as-of moment. The forecast is informational and never blocks workflow.

Manage declared minute availability with GET/PUT .../sprints/{sprintId}/availability (project:read / project:write; the write keeps capacity’s admin ceiling). The PUT body is a replacement set: { "members": [{ "userId", "timezone", "optedIn", "minutes": {monday..sunday}, "overrides": [{"date", "minutes"}] }] } with integer minutes 0–1,440. All rows apply or none do. See Sprints for the full planning contract.

Release Versions

GET .../projects/$PROJECT_KEY/releases lists the Project’s canonical Release Versions. The collection supports limit, an opaque cursor, state, and includeArchived=true; archived versions are hidden by default. A Release Version has a stable UUID, name, optional display version, description, owner, target date, release date, state, first release timestamp, archive state, and revision. A Project cannot have two Release Versions with the same name or display version after trimming and case folding.

Organization administrators and owners use project:write to create or change Release Versions. Creation starts in planned. Metadata updates require a quoted "release-<revision>" If-Match header; mutations also require an Idempotency-Key. The endpoints are:

GET .../releases
POST .../releases
GET .../releases/{releaseId}
PATCH .../releases/{releaseId}
GET .../releases/{releaseId}/transitions
POST .../releases/{releaseId}/transitions
POST .../releases/{releaseId}/archive
POST .../releases/{releaseId}/restore
GET .../releases/{releaseId}/scope

The transition path accepts { "targetState": "in_progress" }, with optional reason. Allowed edges are planned → in_progress, in_progress → planned, in_progress → released, and archive from any non-archived state. An archived Release Version can be restored to its previous state. Release time is retained across archive and restore. The explicit archive and restore operations take {} or { "reason": "..." }. Each successful change records an immutable event in the same transaction. Release Versions start with a basic workflow; optional governance settings do not affect this default path.

If the Release Version contains unfinished Work Items, a release attempt returns RELEASE_HAS_INCOMPLETE_SCOPE with a count and sample keys. Retry with { "targetState": "released", "confirmIncompleteScope": true } to acknowledge the incomplete scope. The first release freezes an original Work Item scope snapshot, including an empty scope, independently of any announcement.

Projects with an opt-in quality gate (see Quality) additionally evaluate the gate on released. An unmet gate returns QUALITY_GATE_FAILED with every unmet condition and its evidence under details.unmet; the Release Version state is unchanged. An administrator may retry with a mandatory qualityGateOverrideReason (1–2000 characters). The override snapshots the unmet conditions, the reason, the actor, and the gate revision in immutable Quality history and never changes test or verification evidence.

GET .../scope returns metadata, complete current-scope counts, and up to 100 Work Items per page. Use its opaque nextCursor to continue; cursors are tied to the selected filters and sort. q, status, type, priority, and assigneeId filter the rows. The response includes the frozen original-scope count and number removed from current scope after first release. Completion is based on Work Item status; it does not imply deployment.

Work Item memberships are many-to-many. Read them with GET .../work-items/{workItemKey}/releases. Use POST .../work-items/{workItemKey}/releases with the Work Item’s "wi-<revision>" ETag and an Idempotency-Key to add, remove, or replace assignments:

{"mode":"add","releaseVersionIds":["00000000-0000-4000-8000-000000000001"]}

This mutation requires work_item:write, rejects versions outside the same Project or archived versions, and returns the updated Work Item revision and all current Release Version summaries. Correcting membership after first release also requires project:write, Project management access, confirmReleasedScopeCorrection: true, and a nonempty reason. Such a correction creates a new audit event and leaves the first-release snapshot unchanged.

For a bounded batch, POST .../projects/$PROJECT_KEY/release-memberships/bulk accepts up to 50 operations with workItemKey, revision, mode, and releaseVersionIds. It returns one result per input, including status and an error for failed items, plus partial: true whenever any item failed. A failed item rolls back its own changes without discarding successful items.

Release announcements

GET .../releases/{releaseId}/announcements returns the latest published announcement. GET .../releases/{releaseId}/announcements/{revision} returns a specific immutable revision. Both require project:read and enforce Project access. They include the rendered Markdown, Release metadata, categorized Work Item snapshot, publisher, UTC publication time, and differences from the original release scope. A later Work Item edit or membership correction does not change an existing revision. No anonymous announcement endpoint exists.

Project managers with project:write can use the draft workflow:

GET .../releases/{releaseId}/announcements/draft
POST .../releases/{releaseId}/announcements/draft
PATCH .../releases/{releaseId}/announcements/draft
DELETE .../releases/{releaseId}/announcements/draft
POST .../releases/{releaseId}/announcements/publish

POST draft accepts {} to generate the first draft, or the current revision to refresh it. A refresh that replaces manual category organization requires confirmReplaceOrganization: true. The draft response includes diff.added, diff.removed, and diff.changed Work Item keys measured against current scope. PATCH draft takes the draft revision, introduction, highlights, categories, and included Work Item IDs. Publishing takes the draft revision, optional reason, and confirmEmpty: true when the scope is empty. Changed Work Item facts or scope return a conflict until the draft is refreshed. Publication creates a new revision and never overwrites previous content. Drafts are bounded at 10,000 Work Items; the server rejects larger scopes rather than omitting rows. Generate and publish requests require Idempotency-Key.

Organization Milestones

Organization Milestones group canonical Project Release Versions for planning. They never grant Project access. The following routes require milestone:read for reads and milestone:write plus Organization administration for changes:

GET .../organizations/{organizationSlug}/milestones
POST .../organizations/{organizationSlug}/milestones
GET .../milestones/{milestoneId}
PATCH .../milestones/{milestoneId}
GET .../milestones/{milestoneId}/transitions
POST .../milestones/{milestoneId}/transitions
GET .../milestones/{milestoneId}/releases
POST .../milestones/{milestoneId}/releases
GET .../milestones/{milestoneId}/events

Milestones have planned, in_progress, completed, and archived states. An archived Milestone can return to its preceding state. Mutations require a numbered "milestone-<revision>" If-Match header and an Idempotency-Key. Membership mutation accepts releaseVersionId and mode: "add" | "remove"; removal also requires confirmRemove: true. The service rejects a Release Version outside the Organization or outside the caller’s authorized Projects. List and member-Release reads are capped at 100 rows per page. Milestone progress counts distinct Work Items across its accessible Release Versions, even when one Work Item belongs to multiple versions. Responses label their counts authorized_projects. Restricted Project names, Release metadata, and counts are not included. A Project-scoped token grant does not itself grant Organization Milestone access. History pages use the opaque nextBeforeEventId UUID as the next request’s beforeEventId query parameter. The cursor must name a visible event; history omits inaccessible Project membership events and their global sequence values.

Work Items

List Work Items with repeatable exact filters:

Terminal window
curl -G \
-H "Authorization: Bearer $HAMSTIK_TOKEN" \
--data-urlencode "status=todo" \
--data-urlencode "status=in_progress" \
--data-urlencode "scope=open" \
--data-urlencode "assignee=me" \
--data-urlencode "limit=50" \
"https://hamstik.com/api/v1/organizations/$ORG_SLUG/projects/$PROJECT_KEY/work-items"

Supported filters are q, repeatable status, type, and priority, scope=all|open|closed, assignee=<legacy user UUID>|me|none, sprint=<sprint UUID>|none, repeatable label, repeatable labelName, parent=<work-item key>, topLevel=true|false, and updatedAfter=<RFC 3339 timestamp>. The collections also accept overdue=true|false, exclusive dueBefore and dueAfter RFC 3339 timestamps, sort=updated|dueDate|priority|rank, archived=true|false, and fields=. Organization and My Work collections add repeatable project; My Work also accepts only the PAT owner’s current assignment and defaults to scope=open.

Work Item collection limits are 1–200, with a default of 50. The valid sparse summary fields are title, type, status, priority, assignee, sprint, parentId, storyPoints, voteCount, hasVoted, effortRisk, effortRiskReason, dueDate, archivedAt, createdAt, and updatedAt. id, key, and revision are always present; Organization and My Work rows also always include project and organization context. A literal empty fields= selects the complete summary, while an empty member such as fields=title,,status is invalid. Authenticated member summaries also expose voteCount and the caller’s own hasVoted (HAM-59); anonymous public projections never include personalized vote state.

New Work Item request bodies should use assigneePublicId with the canonical usr_ value. assigneeId remains accepted for v1 compatibility; the two fields are mutually exclusive. A public ID resolves only to an active member of the requested Organization. Unknown enum values, invalid timestamps, foreign references, and conflicting parent plus topLevel=true are rejected.

All query names are strict. Scalar filters may appear once, and each repeatable filter accepts at most 100 values. topLevel also accepts 1 and 0 as aliases for true and false.

Read a complete Work Item and preserve its revision ETag:

Terminal window
curl -i \
-H "Authorization: Bearer $HAMSTIK_TOKEN" \
"https://hamstik.com/api/v1/organizations/$ORG_SLUG/projects/$PROJECT_KEY/work-items/HAM-42"

Create one. Idempotency-Key is required and should be newly generated for each logical mutation:

Terminal window
curl -i -X POST \
-H "Authorization: Bearer $HAMSTIK_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 83e2744d-7609-4b98-a1ec-056cfa93fa72" \
-d '{"title":"Implement CLI authentication","type":"feature","priority":"high"}' \
"https://hamstik.com/api/v1/organizations/$ORG_SLUG/projects/$PROJECT_KEY/work-items"

The server supplies the reporter. type, status, and priority default to task, backlog, and medium. Descriptions are canonical Hamstik Markdown; existing v1 detail summaries retain the legacy {id, name} shape for compatibility and never contain an email address. Organization, My Work, profile, link, activity, and new Work Item mutation projections introduced by this work use {publicId, name} instead. Existing comment and attachment user summaries also retain their legacy shape for v1 compatibility; new link and activity creator/actor summaries use public IDs.

Create and update bodies are strict objects. Create accepts title, optional Markdown description, type, status, priority, assigneePublicId (or legacy assigneeId), sprintId, parentId (a parent UUID or Work Item key), storyPoints, originalEstimateMinutes, remainingEstimateMinutes, dueDate, effortRisk with effortRiskReason, and isPrivate. Estimates are whole elapsed minutes; null is unknown and 0 is explicitly none. isPrivate defaults to false; set it to true at creation to keep the item hidden from guests in a public Project. Update accepts the same mutable fields except status; at least one field is required. Sending originalEstimateMinutes alone never copies the value into remaining effort: remainingEstimateMinutes is only changed when it is present in the request. Dates must be RFC 3339 timestamps with offsets, and explicit null clears nullable fields.

Update only the documented mutable fields. Use the ETag from the latest read:

Terminal window
curl -i -X PATCH \
-H "Authorization: Bearer $HAMSTIK_TOKEN" \
-H "Content-Type: application/json" \
-H 'If-Match: "wi-1"' \
-d '{"priority":"urgent","description":"Updated Markdown"}' \
"https://hamstik.com/api/v1/organizations/$ORG_SLUG/projects/$PROJECT_KEY/work-items/HAM-42"

Status is intentionally not a PATCH field. Use transition discovery and the transition endpoint instead. Protected fields and unknown fields fail strict validation.

Cloning a Work Item

Read a same-Project clone preview with work_item:read, then create the clone with work_item:write and a new Idempotency-Key:

Terminal window
curl -H "Authorization: Bearer $HAMSTIK_TOKEN" \
"https://hamstik.com/api/v1/organizations/$ORG_SLUG/projects/$PROJECT_KEY/work-items/HAM-42/clone"
curl -i -X POST \
-H "Authorization: Bearer $HAMSTIK_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 1f8b4c2e-2a7f-4b1e-9c3a-9a0f6e5d4c3b" \
-d '{"expectedSourceRevision":7,"title":"Copy of Implement CLI authentication","copyRelatedLinks":true,"linkToOriginal":true}' \
"https://hamstik.com/api/v1/organizations/$ORG_SLUG/projects/$PROJECT_KEY/work-items/HAM-42/clone"

The preview response contains source (including the revision to echo back as expectedSourceRevision), editable defaults, copyableLinks, skippedLinks, and an attachmentNotice when inline attachment references were stripped. expectedSourceRevision is required; a mismatch fails with 412 PREVIEW_STALE and no clone is created. Optional fields (title, description, type, priority, storyPoints, labelIds, assigneeId/assigneePublicId, sprintId, parentId, dueDate, and attributes) override the defaults. copyRelatedLinks defaults to false; linkToOriginal defaults to true. Only relates links whose other end remains authorized in the same Project are copied, and a selected link that becomes invalid fails the transaction atomically. The response is the created Work Item with its revision ETag.

Work Item voting

HAM-59 adds an equal-weight vote to every Work Item. A vote never changes priority, rank, status, Sprint, severity, or assignment, and it emits no notification or automation. Any active Organization member with read access may vote, including read-only Project members; there is no voter roster endpoint.

Read the retained count and your own state (work_item:read):

Terminal window
curl -H "Authorization: Bearer $HAMSTIK_TOKEN" \
"https://hamstik.com/api/v1/organizations/$ORG_SLUG/projects/$PROJECT_KEY/work-items/HAM-42/vote"

The response is { "workItemId", "voteCount", "hasVoted" }.

Set or remove your vote idempotently with work_item:write, an Idempotency-Key, and the desired state (there is no toggle endpoint):

Terminal window
curl -X PUT \
-H "Authorization: Bearer $HAMSTIK_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 83e2744d-7609-4b98-a1ec-056cfa93fa72" \
-d '{"voted":true}' \
"https://hamstik.com/api/v1/organizations/$ORG_SLUG/projects/$PROJECT_KEY/work-items/HAM-42/vote"

The response is authoritative; retries and concurrent opposing requests commit one desired state without creating duplicates or underflowing the count. Any status, including Done, accepts votes while the item and Project are active. Archived items and Projects keep their count but return 409 for a vote change. Losing membership revokes authority while retaining prior votes; deleting an account removes that person’s active votes. Use the SqueakQL search endpoint with numeric vote_count comparisons (=, !=, <, <=, >, >=) and ORDER BY vote_count ASC or DESC. Equal counts order by immutable Work Item ID ascending. Basic collection sort values remain unchanged. Concurrent vote changes can reorder live pages; use a snapshot export when a stable population is required.

Time tracking

Estimates and worklogs are canonical elapsed effort in whole minutes. Story points remain a separate planning estimate and are unchanged. null means the estimate is unknown; 0 means there is explicitly none.

Read the canonical effort summary for a Work Item:

Terminal window
curl -H "Authorization: Bearer $HAMSTIK_TOKEN" \
"https://hamstik.com/api/v1/organizations/$ORG_SLUG/projects/$PROJECT_KEY/work-items/HAM-42/effort"

The response carries originalEstimateMinutes, remainingEstimateMinutes, timeSpentMinutes (the derived sum of authorized worklogs, never a writable counter), outstandingMinutes (0 for canonical done), doneWithStoredRemaining, and a separately labelled descendantTimeSpentMinutes plus descendantLogCount. The descendant rollup counts each authorized child’s own logs once and never adds an intermediate rollup. Unknown estimates stay unknown rather than becoming zero.

Record effort with work_item:write and a new Idempotency-Key. The authenticated caller is the author; normal creation cannot impersonate anyone:

Terminal window
curl -i -X POST \
-H "Authorization: Bearer $HAMSTIK_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 89f9a0f1-4c77-4f16-9b45-2f2b1f2f9c31" \
-d '{"minutes":120,"workDate":"2026-01-05","comment":"Pairing on the parser"}' \
"https://hamstik.com/api/v1/organizations/$ORG_SLUG/projects/$PROJECT_KEY/work-items/HAM-42/worklogs"

minutes is 1–1,440. workDate is a YYYY-MM-DD calendar date supplied by the user and is never converted between timezones; it defaults to today in the caller’s configured timezone (UTC fallback). Future dates are rejected; backdating is allowed and overlapping entries are allowed. The optional comment is Markdown under the standard comment limit. Supply remainingEstimateMinutes to change the remaining estimate atomically with the entry; a worklog write never changes remaining effort on its own.

List authorized worklogs with work_item:read:

Terminal window
curl -H "Authorization: Bearer $HAMSTIK_TOKEN" \
"https://hamstik.com/api/v1/organizations/$ORG_SLUG/projects/$PROJECT_KEY/work-items/HAM-42/worklogs?limit=50"

Read authorization requires both current Work Item access and access to each row’s recorded Project. A log recorded in a Project the reader can no longer see is omitted from rows, own-item and descendant totals, and exports; no hidden numeric bucket or subtractable global total is exposed. Read-only Project access may read totals and logs but cannot write.

Edit or delete your own entry with work_item:write. Use the worklog ETag (If-Match: "wl-2") and a new Idempotency-Key:

Terminal window
curl -i -X PATCH \
-H "Authorization: Bearer $HAMSTIK_TOKEN" \
-H "Content-Type: application/json" \
-H 'If-Match: "wl-2"' \
-H "Idempotency-Key: 5d6b0e64-7a2b-4d2f-bc3c-2f8b1a9a5f6d" \
-d '{"minutes":90,"remainingEstimateMinutes":360}' \
"https://hamstik.com/api/v1/organizations/$ORG_SLUG/projects/$PROJECT_KEY/work-items/HAM-42/worklogs/$WORKLOG_ID"
Terminal window
curl -i -X DELETE \
-H "Authorization: Bearer $HAMSTIK_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: a4c1b7d0-3e2f-4a91-8c55-7b0b1d2e3f40" \
-d '{}' \
"https://hamstik.com/api/v1/organizations/$ORG_SLUG/projects/$PROJECT_KEY/work-items/HAM-42/worklogs/$WORKLOG_ID"

A contributor may edit and delete their own entries while they retain write access. An effective Project admin, or an Organization owner/admin, may correct another author’s entry with a required correctionReason; the correction is recorded on the row and in the Work Item activity. Deleting or editing a log never reverses or invents a remaining-effort adjustment. Archived items retain readable logs, but new or edited logs and estimates require an active writable Item and Project. See the official CLI for supported passthrough of these operations.

Transitions

Discover current valid targets before changing status:

Terminal window
curl -H "Authorization: Bearer $HAMSTIK_TOKEN" \
"https://hamstik.com/api/v1/organizations/$ORG_SLUG/projects/$PROJECT_KEY/work-items/HAM-42/transitions"

Then send the target status with both concurrency and idempotency headers:

Terminal window
curl -i -X POST \
-H "Authorization: Bearer $HAMSTIK_TOKEN" \
-H "Content-Type: application/json" \
-H 'If-Match: "wi-1"' \
-H "Idempotency-Key: transition-HAM-42-20260906-01" \
-d '{"targetStatus":"in_progress"}' \
"https://hamstik.com/api/v1/organizations/$ORG_SLUG/projects/$PROJECT_KEY/work-items/HAM-42/transitions"

The response is the updated full Work Item. A replay has Idempotency-Replayed: true and does not create another activity or history event.

Work Item watchers

Read the authenticated member’s watcher state:

Terminal window
curl -H "Authorization: Bearer $HAMSTIK_TOKEN" \
"https://hamstik.com/api/v1/organizations/$ORG_SLUG/projects/$PROJECT_KEY/work-items/HAM-42/watcher"

The response includes watched, manualWatch, assigneeOrigin, muted, and assigned. Use work_item:write and a new Idempotency-Key for each logical action:

Terminal window
curl -i -X POST \
-H "Authorization: Bearer $HAMSTIK_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: watcher-HAM-42-20260906-01" \
-d '{"action":"watch"}' \
"https://hamstik.com/api/v1/organizations/$ORG_SLUG/projects/$PROJECT_KEY/work-items/HAM-42/watcher"

Supported actions are watch, unwatch, mute, and unmute. Only new comments and status changes produce watched notifications. Assignment adds an assignee-origin watch unless the assignee has muted the item; reassignment removes only that origin, preserving a manual watch. Muting does not suppress direct assignment, mention, or reply notifications. Watcher reads and writes require current Organization membership and reject archived or deleted sources.

Comments

Comments are a flat, oldest-first collection. Threading is represented by parentCommentId; clients can render the tree if desired. Deleted comments remain as placeholders with body: null and deleted: true.

Each comment also includes mentions, containing the resolved organization username and its occurrence count. Comment bodies use @username syntax, where the username belongs to the Organization containing the target Work Item. Resolution is server-side and is shared by web, Public API, and CLI-originated requests; clients must not treat a supplied username or user id as an authorization grant. Only active members who can read the target Project and Work Item can be resolved. Consequently, some people will not appear in web autocomplete, and the API never exposes an alternate member-search result for an ineligible person.

Mention parsing follows Markdown structure. Mentions in inline/fenced code, URLs, email addresses, escaped or malformed tokens, and unknown usernames are ignored. Only organization-scoped @username mentions are supported; @all, @here, teams, groups, arbitrary email addresses, Work Item description mentions, and external-App mentions are unsupported.

Terminal window
curl -G \
-H "Authorization: Bearer $HAMSTIK_TOKEN" \
--data-urlencode "limit=50" \
"https://hamstik.com/api/v1/organizations/$ORG_SLUG/projects/$PROJECT_KEY/work-items/HAM-42/comments"
curl -i -X POST \
-H "Authorization: Bearer $HAMSTIK_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: comment-HAM-42-20260906-01" \
-d '{"body":"Implementation complete."}' \
"https://hamstik.com/api/v1/organizations/$ORG_SLUG/projects/$PROJECT_KEY/work-items/HAM-42/comments"

Comment bodies use Markdown and are limited to 2,000 characters. A reply parent must belong to the same Work Item. Comment creation does not change the Work Item revision. A new comment notifies each newly mentioned eligible member except its author; duplicate occurrences produce one mention reason. Editing notifies only newly added mentions, never repeats unchanged mentions, and does not retract a prior notification. Re-adding a removed mention later is a new event only after the persisted history records the removal. Replies notify the stored parent author, including nested replies, and mention/reply/watcher reasons are deduplicated into one recipient projection.

Changing a username does not retarget existing references. A departed member is rendered with only safe historical mention text. Current authorization is checked again when notification content is read or delivered; notifications do not grant access. If the source comment is removed, its body is omitted from the comment and notification projection, while the thread placeholder and safe unavailable navigation state remain.

Delete an authored comment only when it has no replies:

Terminal window
curl -i -X DELETE \
-H "Authorization: Bearer $HAMSTIK_TOKEN" \
"https://hamstik.com/api/v1/organizations/$ORG_SLUG/projects/$PROJECT_KEY/work-items/HAM-42/comments/$COMMENT_ID"

Successful deletion returns 204 No Content. The comment is soft-deleted so its placeholder remains available to preserve the thread shape. A comment with replies returns 409 CONFLICT; a comment authored by another user returns 403 FORBIDDEN.

Reactions

Reactions are lightweight, notification-free acknowledgements on a Work Item or a comment. They are a closed set with stable machine keys and Unicode display: thumbs_up (👍), heart (❤️), laugh (😂), celebrate (🎉), confused (😕), and eyes (👀). Custom uploads and arbitrary emoji are not supported.

Reactions are deliberately distinct from votes. They never contribute to vote counts, priority, ranking, workflow, or automation triggers, and a reaction never changes the Work Item revision, updatedAt, or activity feed. Any active Organization member with read access to the target may react, including a read-only Project member. A user may apply each distinct reaction once per target and several different reactions to the same target; applying or removing the same reaction twice is idempotent, and removing a reaction that is not present succeeds without lowering any count.

Reads return reactions as an array of {key, count, reacted} entries on each Work Item and comment. count is the live total and reacted states whether the authenticated caller has that reaction. Counts are aggregated per page, so listing comments never performs a per-comment query. There is no voter or reactor directory in this version.

Add or remove a Work Item reaction with work_item:write:

Terminal window
curl -i -X PUT \
-H "Authorization: Bearer $HAMSTIK_TOKEN" \
-H "Idempotency-Key: reaction-HAM-42-20261004-01" \
"https://hamstik.com/api/v1/organizations/$ORG_SLUG/projects/$PROJECT_KEY/work-items/HAM-42/reactions/heart"
curl -i -X DELETE \
-H "Authorization: Bearer $HAMSTIK_TOKEN" \
-H "Idempotency-Key: reaction-HAM-42-20261004-02" \
"https://hamstik.com/api/v1/organizations/$ORG_SLUG/projects/$PROJECT_KEY/work-items/HAM-42/reactions/heart"

Add or remove a comment reaction with comment:write. The comment must be on the Work Item in the path and must not be removed:

Terminal window
curl -i -X PUT \
-H "Authorization: Bearer $HAMSTIK_TOKEN" \
-H "Idempotency-Key: reaction-HAM-42-comment-01" \
"https://hamstik.com/api/v1/organizations/$ORG_SLUG/projects/$PROJECT_KEY/work-items/HAM-42/comments/$COMMENT_ID/reactions/eyes"

Both mutations return 200 OK with the target’s authoritative {reactions} aggregate. An unsupported key returns 400 VALIDATION_ERROR. Inaccessible items, hidden comments, comments in another Work Item, and foreign Organization identifiers return 404 NOT_FOUND and reveal no count or state. An archived Work Item or Project returns 409 CONFLICT and blocks changes while its reactions remain readable. Removing a comment hides its reactions; a removed comment is no longer a valid reaction target. Permanent disposal of a Work Item, Project, comment, Organization, or account removes the affected relationships.

Work Item Attachments

Upload an attachment as multipart form data. The file field is required, the normal per-file hosting limit applies, and Idempotency-Key is required:

Terminal window
curl -i -X POST \
-H "Authorization: Bearer $HAMSTIK_TOKEN" \
-H "Idempotency-Key: attachment-HAM-42-20260907-01" \
-F "file=@./design.png" \
"https://hamstik.com/api/v1/organizations/$ORG_SLUG/projects/$PROJECT_KEY/work-items/HAM-42/attachments"

The response is 201 Created with the attachment id, sanitized fileName, contentType, size, createdBy, and createdAt. List metadata and download the original bytes with the same Work Item path:

Terminal window
curl -H "Authorization: Bearer $HAMSTIK_TOKEN" \
"https://hamstik.com/api/v1/organizations/$ORG_SLUG/projects/$PROJECT_KEY/work-items/HAM-42/attachments"
curl -L -H "Authorization: Bearer $HAMSTIK_TOKEN" \
"https://hamstik.com/api/v1/organizations/$ORG_SLUG/projects/$PROJECT_KEY/work-items/HAM-42/attachments/$ATTACHMENT_ID" \
-o ./design.png

Use the attachment id to remove it:

Terminal window
curl -i -X DELETE \
-H "Authorization: Bearer $HAMSTIK_TOKEN" \
"https://hamstik.com/api/v1/organizations/$ORG_SLUG/projects/$PROJECT_KEY/work-items/HAM-42/attachments/$ATTACHMENT_ID"

The caller must have attachment:read to list or download attachments, attachment:write to upload them, and attachment:delete to delete them. Both attachment mutation scopes include attachment:read. A member may delete only an attachment they uploaded; an Organization administrator may delete any attachment in the Organization. Successful deletion returns 204 No Content.

Work Item links use GET|POST .../work-items/{workItemKey}/links and DELETE .../links/{linkId}. Create requests target one Work Item by UUID or key and use blocks, blocked_by, or relates. Same-Organization visibility and both PAT resource grants are enforced. Conflicts are LINK_DUPLICATE or LINK_CONTRADICTION.

The stored blocks relation is rendered as blocks from the source Work Item and as blocked_by from the inverse Work Item. A stored relates relation is rendered as relates from either side. Link list entries include the visible other Work Item, its Project summary, the creator’s {publicId, name} or null, and createdAt.

GET .../work-items/{workItemKey}/activity returns chronological redacted activity. GET .../projects/{projectKey}/activity returns newest-first activity and includes a Work Item summary. Stored detail is never returned verbatim; deleted or inaccessible references become null.

Organization Work, My Work, and lifecycle

GET /api/v1/organizations/$ORG_SLUG/work-items returns Work Items with Organization and Project context. GET /api/v1/my/work fixes assignment to the PAT owner. Both support repeatable Project/status/type/priority/label filters, due-date and overdue filters, sort=updated|dueDate|priority|rank, and opaque keyset cursors. asOf is captured on the first page and retained in the cursor.

Work Items can be soft-deleted by an Organization owner with DELETE .../work-items/{key} and archived/unarchived by an administrator. Deleted items are hidden and return NOT_FOUND; archived items remain readable but are excluded from collections unless archived=true. Archived items reject Work Item mutations until unarchived. updatedAfter is not a deletion feed; use the Project activity feed to observe soft deletions incrementally.

Projects expose revision ETags, update, archive, and unarchive operations. Archived Projects and their existing Work Items remain readable. Normal child writes, including new Work Items and Sprints, return PROJECT_ARCHIVED. Archiving or restoring a Project never changes any child Work Item’s archivedAt.

Authenticated profiles and member directories

GET /api/v1/users/{publicId}, /work, /activity, and /avatar require profile:read and a PAT. A non-self profile is addressable only through a current shared Organization that remains visible under the PAT’s resource grants. The profile summary contains {publicId, name}, an optional canonical avatar URL, joined date, Organization-paired usernames, and visibility-scoped statistics. It never contains email, an internal UUID, or hidden Organization/work data.

GET /api/v1/organizations/$ORG_SLUG/users requires organization:members:read and returns active {publicId, name, username} rows ordered by name and public ID. q is a literal substring filter over name or that Organization’s username, trimmed to 1–200 characters. A Project-restricted PAT may use this directory only when it has at least one granted Project in that Organization.

Profile activity includes only Work Item creation, comment creation/replies whose comments still exist, and status transitions to the canonical done status. Historical rows without enough identifiers to prove comment visibility are omitted. Each profile activity item also includes the authorized Organization, Project, and Work Item summaries and is ordered newest first.

Bulk operations and sparse fieldsets

Bulk create, update, and transition routes accept 1–50 strict operations and return per-item results in input order. Update and transition requests default to require-revision; last-write-wins explicitly omits revisions.

All Work Item collections accept fields=title,status,.... It is a comma-separated list of top-level summary fields; unknown or empty members are validation errors, while literal fields= means all fields. id, key, and revision are always present, and Organization/My Work context remains present regardless of fieldset.

Project lifecycle

PATCH /api/v1/organizations/$ORG_SLUG/projects/$PROJECT_KEY requires project:write, Organization administrator/owner role, Idempotency-Key, and If-Match: "project-<revision>" (or *). The strict body contains at least one of name, description, or color; the Project key cannot change. Successful update, archive, and unarchive responses return the Project projection and its new Project ETag. Archive and unarchive use strict {} bodies. Repeating the current archive state is a successful no-op.

Work Item deletion and archival

Delete requires work_item:delete, Organization owner role, an idempotency key, and If-Match. The optional strict body is { "cascade": false }; omitting the body has the same meaning. An empty or malformed present body is invalid. Without cascade, non-deleted descendants return 409 WORK_ITEM_HAS_CHILDREN. With cascade, the complete descendant tree is soft-deleted atomically and each deleted Work Item receives its own activity event. Deleted keys are never reused and direct reads return 404 NOT_FOUND.

Archive and unarchive require work_item:delete, administrator/owner role, an idempotency key, If-Match, and a strict empty-object body. They return the updated Work Item and ETag. An already-current state is a no-op without a revision or activity change. Archived Work Items remain readable and are returned only with archived=true; scope=all does not include them. Every other Work Item-scoped mutation is rejected with 409 WORK_ITEM_ARCHIVED. A Work Item cannot be unarchived while its Project is archived: the request returns 409 PROJECT_ARCHIVED and the Project must be unarchived first. Restoring a Project does not unarchive its individually archived Work Items.

Create a link with exactly one of targetKey or targetId:

{"targetKey":"HAM-43","relation":"blocks"}

The target must be a visible Work Item in the same Organization, and both Projects must be covered by the PAT. A self-link is validation failure; a duplicate is LINK_DUPLICATE; an opposing blocks relationship is LINK_CONTRADICTION. Create/delete require idempotency keys and write one activity event only after an actual change. Link deletion returns 204 and requires the link to touch the requested Work Item.

Activity entries are projections, never stored JSON. The stable detail shapes are:

Actions detail
created, comment_added, comment_replied, comment_edited, comment_deleted, description_changed, archived, unarchived, work_item_deleted null
status_changed {from, to}
assigned `{from: user
priority_changed {from, to}
sprint_changed `{from: sprint
label_added, label_removed `{label: label
link_added, link_removed `{relation, otherWorkItem: {id, key, title}
due_date_changed {from, to} with RFC 3339 or null
title_changed {from: string, to: string}
parent_changed `{from: work-item
attachment_added, attachment_removed `{attachment: {id, fileName}

since is a strictly-after RFC 3339 filter. Work Item activity is oldest first; Project and profile activity are newest first. A deleted actor is rendered as actor: null, and inaccessible referenced resources are redacted to null.

Profile response and filters

The profile summary has this shape:

{
"publicId":"usr_cPbfeqnghA-RLpDVOMQhHg",
"name":"Steven Hildreth",
"avatarUrl":"https://hamstik.com/api/v1/users/usr_cPbfeqnghA-RLpDVOMQhHg/avatar",
"joinedAt":"2026-01-15T14:30:00.000Z",
"isCurrentUser":false,
"sharedOrganizations":[{"organization":{"id":"...","slug":"blackboard-studios","name":"Blackboard Studios"},"username":"steven"}],
"stats":{"projects":6,"workItemsAssigned":7,"workItemsCreated":42,"workItemsCompleted":18,"comments":27}
}

The four profile endpoints share the same visibility rule. A malformed, unknown, removed, or non-addressable target is indistinguishable as 404. The avatar URL is null when no current avatar exists; the avatar endpoint otherwise returns the current validated avatar image with private no-store caching. Hamstik-hosted avatars are served as the optimized 96-pixel WebP UI derivative, so the response is small and the Content-Type may be image/webp; avatars originally hosted by an OAuth provider are proxied in their original format. The endpoint does not redirect API clients to the provider. A canonical avatar URL may carry only validated content-version parameters needed by the existing avatar storage behavior. It never exposes an internal UUID or storage key.

The five statistics are computed only after the viewer’s membership, suspension, PAT grant, deletion, and archive predicates are applied: distinct visible Projects involving the target as assignee/reporter/comment author; visible Work Items currently assigned to the target; visible Work Items reported by the target; visible assigned Work Items in canonical done; and visible non-deleted comments authored by the target. Each row counts once per statistic, and the self-profile has the same access restrictions as any other profile request.

Profile Work supports the Work Item collection filters except caller-selected assignee (the target is fixed by the profile URL), plus repeatable organization=<organizationSlug> and involvement=assigned|created|commented; involvement defaults to all three and scope=open is the default. It is deduplicated before pagination. Profile Work uses its own projection: embedded assignee and reporter identities are {publicId, name} (the reporter is included in the complete projection; it is omitted when a sparse fields selection is supplied because reporter is not a Work Item summary field, and Project/Organization context is always present. The same top-level fields selectors and 200-item limit apply to this collection. Profile activity is limited to the target’s visible created, live comment creation/reply, and status_changed events whose public to state is canonical done.

Bulk request envelopes

All three bulk routes require work_item:write and Idempotency-Key:

{"operations":[{"projectKey":"HAM","title":"First"}]}
{"concurrency":"require-revision","operations":[{"projectKey":"HAM","workItemKey":"HAM-42","revision":4,"changes":{"priority":"high"}}]}
{"concurrency":"last-write-wins","operations":[{"projectKey":"HAM","workItemKey":"HAM-42","targetStatus":"done"}]}

Top-level envelopes require 1–50 operations and reject unknown fields. Each operation is validated independently, so a malformed item becomes an embedded VALIDATION_ERROR while other items continue. The outer status is 200, there is no page, and each result contains its input index plus either the per-item success status/projection or an embedded error. Replays return the stored result array without repeating any item.

Comment editing

PATCH .../comments/{commentId} requires comment:write, an idempotency key, and a strict { "body": "..." } body using the same 2,000-character limit as creation. Only the comment author may edit; administrators do not receive public moderator edit authority. Deleted comments return 404. A successful edit returns 200 and sets editedAt; creation and deletion do not set or change that field. The edit response uses the new public author projection {publicId, name}. Existing comment list, create, and delete responses retain the legacy v1 {id, name} author summary for compatibility.

Advanced reports and dashboards

Advanced Reporting API provides Organization-scoped definitions, personal mutations, shared governance, report/dashboard runs, and short-lived exact selections. It uses report:read / report:write with current Premium, App, capability, and PAT Project restrictions. Standard report resources retain Core availability and project:read.