---
title: "Update card fields"
method: PATCH
path: "/api/cards/{cardId}"
tags: ["Cards"]
---

# Update card fields

`PATCH /api/cards/{cardId}`

## Request

- Base URL: `https://tre-app.com`
- URL: `https://tre-app.com/api/cards/{cardId}`
- Auth: HTTP bearer

Pass `expectedUpdatedAt` (the `updatedAt` you read) to refuse the write with 412 when someone changed the card in between, instead of silently replacing their work. Or pass `expectedSeq` (the card's `seq` from getCard, exclusive with `expectedUpdatedAt`) to also cover comment, checklist-item and subtask writes and moves across lists — not reorders, a move within one list, or an editor autosave inside its checkpoint window, which append no event (pass `expectedUpdatedAt` when a human may be typing in the card). A call that changes nothing — every field restated as the card holds it, assignees merely reordered — writes nothing: no event, no `Tre-Event-Seq`, and `updatedAt` stays, so the `expectedUpdatedAt` you hold remains valid; anything that does change moves `updatedAt`, a reorder of `links` included (reported as `changes.links.reordered`). A 412 carries `details.currentSeq` and `details.events` (missed events, oldest first, at most 20; `eventsTruncated` when there were more). While a subtask holds a planned date, the card's plan can be widened but neither cleared nor narrowed under it: the refusal (`400`, `details.rule` `CARD_HAS_NO_DATES` or `SUBTASK_OUT_OF_RANGE`) names the blocking subtask and the narrowest plan that would do — Tre never widens the card for you. A `description` over the 2000-character cap answers 400 VALIDATION_ERROR with `details: { rule: 'index-card', limit: 2000, hint }` and the same hint in the validation message (see createCard). To rewrite one block and leave the rest exactly as it is, send `sections` instead of `description`: `{ STATUS: { date: 'YYYY-MM-DD', text }, NEXT_STEP, PURPOSE, OTHER }` (or STATO / PROSSIMO_PASSO / SCOPO / ALTRO; null clears a block; both together answer 400). A block's text ends at the next line that opens with an uppercase `LABEL:`: a paragraph such as `SOURCES: …` or `CONTEXT: …` belongs to OTHER and survives a write to the other three blocks (one the stored document had filed inside the block you rewrite is moved to the front of OTHER, and the event names `other` too); a write never touches a block it does not name. For the same reason the text you send for PURPOSE, STATUS or NEXT STEP cannot itself carry such a line below its first: 400 BAD_REQUEST with `details: { rule: 'block-text', block, line }` — send the block without it. To change or remove the paragraph, write OTHER, which is replaced whole. The server composes the description from the blocks; the STATUS/STATO it replaces goes to the description history (`GET /api/cards/{cardId}/history`) and leaves no comment. On reference, digest and guidance cards the dated block is written as UPDATED/AGGIORNATO whichever of STATUS/STATO/UPDATED/AGGIORNATO you send. The event names the blocks in `sections`. The composed description, labels and the blank lines between blocks included, must fit the 2000-character cap: past it the write answers 400 BAD_REQUEST with `details: { rule: 'index-card', block, limit: 2000 }` naming the longest block, so a text accepted on create can be refused on update — shorten it or move content to checklist items, comments or links. On a call that sends `sections` alone, a stale expectedUpdatedAt no longer refuses outright: the write applies when none of the blocks you name changed in between, and the answer carries `merged.sections` (the blocks others changed); a named block that moved on answers 412 with `details.conflictingSections`, so re-read that block only. A stale call that also sends any other field (a title, a kind, a date, links, assignees — even one restated unchanged) is refused whole with 412 and writes nothing: send the blocks in their own call. The workspace guidance card is gated: rewriting its `description`, setting `kind: 'guidance'` on a card, or changing the kind of the card that is guidance is for a workspace owner or admin from the web app — or for a Bearer token that owner or admin has allowed for the workspace (grantGuidanceWrite; Settings → API tokens → Allow guidance writes), which must then send `confirm: 'guidance'` AND `expectedUpdatedAt` in the same call. Without a live grant the answer is 403 FORBIDDEN with `details: { reason, grant: 'guidance-write', how }` (reason is no_grant, grant_expired, grant_revoked or not_workspace_admin; `no_consent_path` is the refusal of the consent-less paths — createCard with `kind: 'guidance'` and restoreCardHistoryVersion — never of this operation); with a grant but without the consent it is 400 BAD_REQUEST with `details: { missing: [...] }`; a stale `expectedUpdatedAt` is the 412 above. A granted write's `card.updated` event carries `guidance: { grantId, grantedBy, grantedAt, expiresAt }` beside its diff.

## Request body

- CardUpdate
  - `expectedUpdatedAt` string, date-time: The `updatedAt` you read before deciding this change, passed back VERBATIM — the comparison is exact string equality against the stored ISO serialization, not a timestamp comparison. When the card has changed since, the update is refused (412) instead of overwriting the other writer — re-read and decide again. It guards the card row alone: comments, checklist items, subtasks and moves never bump `updatedAt`. Exclusive with `expectedSeq`.
  - `expectedSeq` integer: The card's `seq` from get_card (or the receipt of your last write to this card: `seq` runs across the workspace, so the receipt of a write to another card would pass over this card's events). The write is refused with 412 when any retained event of the card has seq greater than this — comment, checklist-item and subtask writes, moves across lists and `update_card`/PATCH writes to the card row. What appends no event is not seen: a reorder, a move within one list, or an editor autosave inside its checkpoint window (pass `expectedUpdatedAt` when a human may be typing in the card). Exclusive with `expectedUpdatedAt`. A card with no retained events has seq 0; expectedSeq 0 then passes.
  - `title` string
  - `description` string: Concise narrative context carrying only the current truth, written under fixed uppercase labels: `PURPOSE: …`, a dated `STATUS YYYY-MM-DD: …` line rewritten in place (preserve its date during editorial corrections), `NEXT STEP: action → who` (who acts: the name of a person, the name of a token, or `waiting for <name> (<organisation>)`; a date only when real) — or the Italian set SCOPO / STATO / PROSSIMO PASSO. The server reads that dated line for freshness and staleness; the colon is the canonical delimiter, a dash is accepted and normalized to it. A line break is stored as `\n`: `\r\n` or a lone `\r` is accepted and normalized to it, so the text you read back can differ from the text you sent by those characters. History belongs in dated card comments, never in layered corrections here. Do not place a mutable list of independently trackable records or steps here; use checklist items.
  - `sections` object: Rewrite one or more blocks of the description and leave the rest exactly as they are: { PURPOSE, STATUS: { date, text }, NEXT_STEP, OTHER } or the Italian keys (SCOPO, STATO, PROSSIMO_PASSO, ALTRO). null clears a block. Exclusive with `description`. A block ends at the next line that opens with an uppercase `LABEL:`: a paragraph such as `SOURCES: …` belongs to OTHER and survives a write to the other three, so the text of PURPOSE, STATUS or NEXT STEP cannot carry such a line below its first (400, details.rule `block-text`); write OTHER, replaced whole, to change or remove it. A line break in the text of a block is stored as `\n` (`\r\n` and a lone `\r` are normalized to it). With a stale expectedUpdatedAt, a call that sends sections alone still applies when none of the named blocks changed in between (the response carries `merged.sections`, the blocks others changed); a named block that moved on answers 412 with details.conflictingSections. A stale call that also sends any other field, even one restated unchanged, is refused whole (412) and writes nothing.
    - `SCOPO` string, nullable
    - `PURPOSE` string, nullable
    - `STATO` object, nullable
      - `date` string, date, required: The day the described state applies, YYYY-MM-DD.
      - `text` string, required
    - `STATUS` object, nullable
      - `date` string, date, required: The day the described state applies, YYYY-MM-DD.
      - `text` string, required
    - `AGGIORNATO` object, nullable
      - `date` string, date, required: The day the described state applies, YYYY-MM-DD.
      - `text` string, required
    - `UPDATED` object, nullable
      - `date` string, date, required: The day the described state applies, YYYY-MM-DD.
      - `text` string, required
    - `PROSSIMO_PASSO` string, nullable
    - `PROSSIMO PASSO` string, nullable
    - `NEXT_STEP` string, nullable
    - `NEXT STEP` string, nullable
    - `ALTRO` string, nullable
    - `OTHER` string, nullable
  - `color` string
  - `startDate` string, date, nullable: Start of the retained plan (not an observed start). For one exact operational deadline, pass the same ISO day in startDate and endDate. Never infer it from a reference date.
  - `endDate` string, date, nullable: End of the retained plan (not an observed completion). For one exact operational deadline, pass the same ISO day in startDate and endDate.
  - `scheduleState` 'unclassified' | 'planned' | 'suspended' | 'cancelled': Meaning of the retained schedule; one server rule on every surface. Omit it: a create with a planned date is stored as planned, one without as unclassified; on update, touching a planned date promotes unclassified to planned, clearing both planned dates resets to unclassified, any other stored state is kept. Send suspended or cancelled explicitly; a non-unclassified state with both planned dates null is refused (400 BAD_REQUEST).
  - `actualStartDate` string, date, nullable: Observed start, explicitly recorded from evidence. Independent of the schedule; null means unknown, never not started.
  - `actualEndDate` string, date, nullable: Observed end, explicitly recorded from evidence. May fall outside the plan. Null means unknown, never incomplete.
  - `verifiedOn` string, date, nullable: Day the named source was actually checked, never later than the server UTC day. Independent of updatedAt and the status date in the description; never advance for editorial work.
  - `verificationSource` string, nullable: Source reference and scope of the last factual verification (document, record, URL or cardKey). Set or clear together with verifiedOn.
  - `links` object[]
    - `label` string, required
    - `url` string, required
  - `assigneeUserIds` string[]
  - `kind` 'control' | 'work' | 'reference' | 'decision' | 'digest' | 'guidance', nullable: What the card is for; null clears it. Lint reads reference/digest/guidance. Setting `guidance`, changing the kind of the guidance card, or rewriting its description is for a workspace owner or admin from the web app — or for a token that owner or admin has allowed for it (Settings → API tokens → Allow guidance writes, time-limited and revocable), which must then also send confirm: 'guidance' and expectedUpdatedAt; any other caller answers 403.
  - `confirm` 'guidance': Required, together with expectedUpdatedAt, when a token rewrites the guidance card's description, promotes a card to kind guidance or changes the guidance card's kind under a guidance write grant: pass exactly 'guidance'. Ignored on every other write.

## Response `200`

Success

- CardUpdated
  - `id` string, uuid, required
  - `boardId` string, uuid, required
  - `listId` string, uuid, required
  - `cardNumber` integer, required
  - `cardKey` string, required
  - `title` string, required
  - `description` string, required
  - `color` string, required
  - `order` integer, required
  - `startDate` string, date, nullable, required
  - `endDate` string, date, nullable, required
  - `scheduleState` 'unclassified' | 'planned' | 'suspended' | 'cancelled', required: Meaning of the retained schedule. Unclassified means its intent has not been reviewed; suspended/cancelled retain the plan for retrospective comparison.
  - `actualStartDate` string, date, nullable, required: Observed start, explicitly recorded from evidence. Independent of the schedule; null means unknown, never not started.
  - `actualEndDate` string, date, nullable, required: Observed end, explicitly recorded from evidence. May fall outside the plan. Null means unknown, never incomplete.
  - `verifiedOn` string, date, nullable, required: Day the named source was actually checked, never later than the server UTC day. Independent of updatedAt and the status date in the description; never advance for editorial work.
  - `verificationSource` string, nullable, required: Source reference and scope of the last factual verification (document, record, URL or cardKey). Set or clear together with verifiedOn.
  - `links` object[], required
    - `label` string, required
    - `url` string, required
  - `createdAt` string, date-time, required
  - `updatedAt` string, date-time, required
  - `createdByUserId` string, uuid, nullable, required
  - `assigneeUserIds` string[], required
  - `kind` 'control' | 'work' | 'reference' | 'decision' | 'digest' | 'guidance', nullable, required
  - `merged` object: Present when a stale expectedUpdatedAt was reconciled per block (S-D5): the blocks others changed in between.
    - `sections` string[], required

## Other responses

- `400`: Invalid request
- `401`: Unauthenticated
- `403`: Forbidden: insufficient role or token scope; scope denials name the held scope and the operation
- `404`: Not found
- `409`: Conflict
- `412`: A stale `expectedUpdatedAt` or `expectedSeq`: details.currentUpdatedAt, details.currentSeq and details.events (missed events, oldest first, at most 20; eventsTruncated when there were more). expectedUpdatedAt is an exact string comparison; when it is stale on a call that sends `sections` alone, details.conflictingSections names the blocks that moved on, and a stale call that also sends any other field, even one restated unchanged, is refused whole and writes nothing. expectedSeq also covers comment, checklist-item and subtask writes and moves across lists, not reorders, a move within one list or an editor autosave inside its checkpoint window.
- `429`: Rate limited
- `500`: Internal server error
- `503`: Temporarily unavailable

---

[API](https://skmtc.dev/tre-app/apis/tre-rest-api.md) · [All operations](https://skmtc.dev/tre-app/apis/tre-rest-api/llms.txt) · [OpenAPI document](https://skmtc.dev/tre-app/apis/tre-rest-api/revisions/26a7187a05fc?raw)
