The Case object
A case is an issue somebody is working, with a severity and a resolution clock. It exists alongside the shared inbox rather than replacing it: the inbox already handles assignment, open and closed, and internal comments for email. A case adds the three things it does not have: a severity, a resolution target distinct from first response, and a way to record an issue that never had an email behind it.
The Cases API is read-only. Cases are opened and moved in the app or through the AI connector.
| Field | Type | Notes |
|---|---|---|
id | string | CUID (read-only) |
number | integer | Per-workspace case number, shown to people as #31 |
subject | string | What the issue is |
description | string | null | The detail: what happened, what was tried |
status | string | One of NEW, OPEN, PENDING, RESOLVED, CLOSED |
severity | string | One of LOW, MEDIUM, HIGH, URGENT |
source | string | MANUAL, EMAIL or LEAD. Which intake door it came through |
open | boolean | Computed. True while the status is NEW, OPEN or PENDING |
ageMins | integer | Computed. Whole minutes the case has been open, frozen at resolvedAt once resolved |
overdue | boolean | Computed. True when a HIGH or URGENT case has been open longer than three days |
company | object | null | The account it is on: { id, name } |
contact | object | null | The person who raised it: { id, name } |
assignee | object | null | The rep who has it: { id, name }, or null when it is still in the queue |
threadKey | string | null | The email conversation it came from, when it came from one |
firstResponseAt | string (ISO 8601) | null | Stamped once, when the case first leaves NEW |
resolvedAt | string (ISO 8601) | null | When it was resolved |
createdAt | string (ISO 8601) | Assigned on create (read-only) |
updatedAt | string (ISO 8601) | Updated on every write (read-only) |
Example object
{
"id": "cmq2case0alk34567890",
"number": 31,
"subject": "Pallet arrived damaged",
"description": "Two of six bags split in transit. Photos sent.",
"status": "OPEN",
"severity": "HIGH",
"source": "EMAIL",
"open": true,
"ageMins": 4320,
"overdue": true,
"company": { "id": "cmq2comp0alk34567890", "name": "Acme Corp" },
"contact": { "id": "cmq2cont0alk34567891", "name": "Jane Buyer" },
"assignee": { "id": "cmq9user0alk34567890", "name": "Kevin Rep" },
"threadKey": "AAMkAGI2...",
"firstResponseAt": "2026-09-04T08:12:00.000Z",
"resolvedAt": null,
"createdAt": "2026-09-04T07:55:00.000Z",
"updatedAt": "2026-09-06T14:02:00.000Z"
}Notes on specific fields
open, ageMins and overdue are computed on read, never stored. A stored age is wrong the moment nobody writes to the row, and everything needed to work it out is already on the record. That means a queue you poll every minute always reports the current age, with no job keeping a column in step with the clock.
The clock stops at resolvedAt. A case resolved in an hour still reads as an hour a month later, which is what makes ageMins usable as a resolution-time measure and not just "how long ago was this".
overdue only applies to HIGH and URGENT. The target is three days. LOW and MEDIUM cases carry no target and are never flagged, because a target nobody believes is worse than no target.
threadKey is the inbox's own conversation key. A case opened from an email keeps that key and links back to the conversation rather than copying it: the thread stays where it lives, and the case is the thing carrying a severity. Opening a case twice on the same conversation returns the same case, so history is never split in half.
firstResponseAt is stamped once, when somebody picks the case up and moves it out of NEW. Later status changes do not touch it. Moving a case from RESOLVED to CLOSED likewise keeps the original resolvedAt, because filing is not a second resolution; reopening a case clears it, because it genuinely is not resolved any more.