{"openapi":"3.0.3","info":{"title":"IntelliDesk API","version":"1.0.0","description":"The IntelliDesk public REST API lets you create and manage support tickets programmatically. Authenticate using a desk-scoped API key generated from Help Desk Settings.","contact":{"name":"IntelliDesk Support","url":"https://intellidesk.co"}},"servers":[{"url":"https://intellidesk.co/api/v1","description":"IntelliDesk API v1"}],"security":[{"BearerAuth":[]}],"components":{"securitySchemes":{"BearerAuth":{"type":"http","scheme":"bearer","description":"Pass your API key in the `Authorization` header as `Bearer idk_<key>`. Use a **server-side** API key for reads, updates, attachment downloads, and admin operations — **widget keys and MCP keys are rejected with `403`** on those endpoints. Keys are **scoped to a single help desk**: requesting a ticket (or its attachments) that belongs to a different help desk returns `404 \"Ticket not found\"` even though it exists, so make sure the key's help desk matches the ticket. Keys are generated from Help Desk Settings."}},"schemas":{"Tag":{"type":"object","description":"A workspace-scoped label. The same tag vocabulary is shared by tickets and project cards. Labels are stored lowercased and matched case-insensitively, so `Billing` and `billing` are one tag.","properties":{"id":{"type":"string","description":"Internal tag document ID.","example":"tg7abc123"},"label":{"type":"string","description":"The tag label, lowercased.","example":"billing"},"color":{"type":"string","enum":["slate","sky","emerald","amber","rose","violet","indigo","teal","orange"],"description":"Chip swatch used when rendering the tag."}},"required":["id","label","color"]},"Ticket":{"type":"object","properties":{"id":{"type":"string","description":"Internal Convex document ID.","example":"k97abc123def456"},"publicId":{"type":"string","description":"Human-readable ticket reference.","example":"HD-1042"},"subject":{"type":"string","description":"Ticket subject line.","example":"Login button not working"},"status":{"type":"string","enum":["open","pending","on_hold","closed"],"description":"Customer-facing ticket status."},"priority":{"type":"string","enum":["low","normal","high","urgent"],"description":"Ticket priority level."},"type":{"type":"string","enum":["question","incident","problem","task"],"nullable":true,"description":"Ticket type category."},"workflowState":{"type":"string","enum":["todo","in_progress","in_review","changes_requested","done","needs_human"],"nullable":true,"description":"Internal workflow state for tickets being processed by an automated worker. Independent of the customer-facing `status` field. `null` on tickets not being worked by a bot."},"repo":{"type":"string","nullable":true,"description":"Git repository path. Accepts 2–5 path segments separated by `/` (each segment is letters, digits, dot, dash, or underscore). Examples: `acme/web` (GitHub), `group/sub/repo` (GitLab nested groups), `org/project/_git/repo` (Azure DevOps).","example":"acme/web"},"metadata":{"type":"object","nullable":true,"additionalProperties":{"oneOf":[{"type":"string"},{"type":"number"},{"type":"boolean"}]},"description":"Arbitrary key-value metadata attached to the ticket. Keys are namespaced by convention (e.g. `harness.attempts`). Max 4 KB serialized. On `PATCH`, the entire map is replaced — read-modify-write to update one key.","example":{"harness.attempts":2,"harness.last_branch":"bot/HD-1042-attempt-2"}},"assignee":{"type":"string","nullable":true,"description":"User-id string of the agent (human or bot) assigned to this ticket. Free-form identifier. On writes, the same string is stored as the assignee display name when no separate name is known.","example":"u_bot_harness_01"},"assigneeId":{"type":"string","nullable":true,"readOnly":true,"description":"Convenience flat copy of the assignee user id. Identical to `assignee`. Provided so consumers can read a single field instead of navigating into the legacy nested `assignee` object that also exists on responses.","example":"u_bot_harness_01"},"requesterName":{"type":"string","description":"Name of the ticket requester.","example":"Jane Smith"},"requesterEmail":{"type":"string","format":"email","description":"Email address of the requester.","example":"jane@example.com"},"tags":{"type":"array","readOnly":true,"items":{"$ref":"#/components/schemas/Tag"},"description":"Tags currently on the ticket. Write them with the `tags` (replace) or `addTags`/`removeTags` (incremental) fields on `POST /tickets` and `PATCH /tickets/{id}`."},"createdAt":{"type":"integer","format":"int64","description":"Unix timestamp (ms) when the ticket was created.","example":1711929600000},"updatedAt":{"type":"integer","format":"int64","description":"Unix timestamp (ms) of the last update.","example":1711933200000}},"required":["id","publicId","subject","status","priority","requesterName","requesterEmail","createdAt"]},"Message":{"type":"object","properties":{"id":{"type":"string","description":"Internal message document ID.","example":"m12xyz789"},"ticketId":{"type":"string","description":"ID of the parent ticket.","example":"k97abc123def456"},"body":{"type":"string","description":"Message body text (may contain HTML).","example":"<p>Thanks for reaching out. We are looking into this.</p>"},"authorName":{"type":"string","nullable":true,"description":"Display name of the message author.","example":"Support Agent"},"direction":{"type":"string","enum":["inbound","outbound"],"description":"Whether the message came from the requester or the agent."},"visibility":{"type":"string","enum":["public","internal"],"description":"`internal` messages are only returned with `includeInternal=true` and are never shown to customers."},"createdAt":{"type":"integer","format":"int64","description":"Unix timestamp (ms) when the message was created.","example":1711929900000}},"required":["id","ticketId","body","direction","createdAt"]},"TicketList":{"type":"object","properties":{"tickets":{"type":"array","items":{"$ref":"#/components/schemas/Ticket"}},"nextCursor":{"type":"string","nullable":true,"description":"Opaque cursor for the next page. Pass as `cursor` in the next request. `null` means no more pages."}},"required":["tickets"]},"Error":{"type":"object","properties":{"error":{"type":"string","description":"Machine-readable error code.","example":"not_found"},"message":{"type":"string","description":"Human-readable description of the error.","example":"Ticket not found."}},"required":["error","message"]},"TimeEntry":{"type":"object","required":["id","startedAt","source"],"properties":{"id":{"type":"string","description":"Internal document ID.","example":"k97abc123def456"},"startedAt":{"type":"integer","format":"int64","description":"Unix ms when the entry started.","example":1748736000000},"endedAt":{"type":"integer","format":"int64","nullable":true,"description":"Unix ms when the entry ended. `null` for a running timer."},"durationMs":{"type":"integer","nullable":true,"description":"Computed duration in milliseconds. `null` while the timer is still running."},"ticketId":{"type":"string","nullable":true,"description":"Attached ticket ID, if any."},"cardId":{"type":"string","nullable":true,"description":"Attached project card ID, if any."},"projectId":{"type":"string","nullable":true,"description":"Attached project ID, if any."},"categoryId":{"type":"string","nullable":true,"description":"Attached time category ID, if any."},"freeformLabel":{"type":"string","nullable":true,"description":"Free-text label used when no structured attachment applies."},"description":{"type":"string","nullable":true,"description":"Optional longer description."},"source":{"type":"string","enum":["timer","manual","suggested","pomodoro","api"],"description":"How the entry was created."},"chargeRateAtEntry":{"type":"number","nullable":true,"description":"Hourly charge rate snapshotted at entry creation time."},"currencyAtEntry":{"type":"string","nullable":true,"description":"Currency code snapshotted at entry creation time (ISO 4217).","example":"USD"},"submissionId":{"type":"string","nullable":true,"description":"ID of the timesheet submission this entry belongs to, once submitted."}}},"TimesheetSubmission":{"type":"object","required":["id","fromDate","toDate","submittedAt","totalDurationMs","status"],"properties":{"id":{"type":"string","description":"Internal document ID.","example":"s12abc345def"},"fromDate":{"type":"integer","format":"int64","description":"Start of the submission period (Unix ms, inclusive).","example":1748649600000},"toDate":{"type":"integer","format":"int64","description":"End of the submission period (Unix ms, inclusive).","example":1749254400000},"submittedAt":{"type":"integer","format":"int64","description":"Unix ms when the submission was first created.","example":1749254500000},"lastUpdatedAt":{"type":"integer","format":"int64","nullable":true,"description":"Unix ms of the last resubmit, or `null` if never resubmitted."},"totalDurationMs":{"type":"integer","description":"Sum of `durationMs` across all included entries.","example":108000000},"totalAmount":{"type":"number","nullable":true,"description":"Total billable amount computed from entries and charge rates. `null` if rates are not configured."},"currency":{"type":"string","nullable":true,"description":"ISO 4217 currency code for `totalAmount`. `null` if amounts are not applicable.","example":"USD"},"status":{"type":"string","enum":["submitted","stale","withdrawn"],"description":"`submitted` — active submission; `stale` — entries changed after submission, resubmit required; `withdrawn` — removed from approval flow."},"note":{"type":"string","nullable":true,"description":"Optional free-text note attached at submission time."},"xlsxStatus":{"type":"string","enum":["ready","failed","csv_fallback"],"nullable":true,"description":"Status of the XLSX export. `null` while generating; `ready` once the download URL is available; `failed` if generation errored; `csv_fallback` if the workbook fell back to CSV."}}},"TimeCategory":{"type":"object","required":["id","label","color","archived","position"],"properties":{"id":{"type":"string","description":"Internal document ID.","example":"c34def678ghi"},"label":{"type":"string","description":"Display name of the category.","example":"Client Calls"},"color":{"type":"string","description":"6-digit hex colour without a leading `#`.","example":"3b82f6"},"archived":{"type":"boolean","description":"Whether the category is archived and hidden from default lists."},"position":{"type":"integer","description":"Zero-based display sort position."}}}},"responses":{"Unauthorized":{"description":"Missing or invalid API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"unauthorized","message":"Invalid or missing API key."}}}},"NotFound":{"description":"The requested resource was not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"not_found","message":"Ticket not found."}}}},"ValidationError":{"description":"Request body failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"validation_error","message":"Missing required fields: subject, body, requesterName, requesterEmail."}}}},"InternalError":{"description":"Unexpected server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"internal_error","message":"An unexpected error occurred."}}}}}},"paths":{"/tickets":{"post":{"operationId":"createTicket","summary":"Create a ticket","description":"Creates a new support ticket in the help desk associated with the API key.","tags":["Tickets"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["subject","body","requesterName","requesterEmail"],"properties":{"subject":{"type":"string","description":"Ticket subject line.","example":"Cannot reset my password"},"body":{"type":"string","description":"Initial message body for the ticket.","example":"I tried resetting my password three times and keep getting an error."},"requesterName":{"type":"string","description":"Full name of the person submitting the ticket.","example":"Jane Smith"},"requesterEmail":{"type":"string","format":"email","description":"Email address of the requester.","example":"jane@example.com"},"priority":{"type":"string","enum":["low","normal","high","urgent"],"default":"normal","description":"Ticket priority. Defaults to `normal`."},"type":{"type":"string","enum":["question","incident","problem","task"],"description":"Ticket type category."},"workflowState":{"type":"string","enum":["todo","in_progress","in_review","changes_requested","done","needs_human"],"description":"Initial workflow state for automated worker tickets."},"repo":{"type":"string","description":"Git repository slug in `owner/name` form (e.g. `acme/web`).","example":"acme/web"},"metadata":{"type":"object","additionalProperties":{"oneOf":[{"type":"string"},{"type":"number"},{"type":"boolean"}]},"description":"Arbitrary key-value metadata. Max 4 KB serialized.","example":{"harness.attempts":0}},"assignee":{"type":"string","nullable":true,"description":"User-id string of the agent (human or bot) to assign on creation.","example":"u_bot_harness_01"},"tags":{"type":"array","items":{"type":"string"},"description":"Labels to attach to the new ticket. Labels that don't exist in the workspace are created. Matched case-insensitively.","example":["billing","urgent"]}}}}}},"responses":{"201":{"description":"Ticket created successfully.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"Internal document ID of the new ticket.","example":"k97abc123def456"},"publicId":{"type":"string","description":"Human-readable ticket reference.","example":"HD-1042"}},"required":["id","publicId"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"422":{"$ref":"#/components/responses/ValidationError"},"500":{"$ref":"#/components/responses/InternalError"}}},"get":{"operationId":"listTickets","summary":"List tickets","description":"Returns a paginated list of tickets for the help desk associated with the API key. Results are ordered newest first.","tags":["Tickets"],"parameters":[{"name":"status","in":"query","schema":{"type":"string","enum":["open","pending","on_hold","closed"]},"description":"Filter by ticket status."},{"name":"tag","in":"query","schema":{"type":"string"},"description":"Filter by tag label. Accepts a comma-separated list; matched case-insensitively. Aliased as `tags`. By default every listed tag must be present — see `tagMatch`.","example":"billing,urgent"},{"name":"tagMatch","in":"query","schema":{"type":"string","enum":["all","any"],"default":"all"},"description":"How multiple `tag` values combine. `all` (default) returns tickets carrying every listed tag; `any` returns tickets carrying at least one."},{"name":"priority","in":"query","schema":{"type":"string","enum":["low","normal","high","urgent"]},"description":"Filter by priority."},{"name":"workflowState","in":"query","schema":{"type":"string","enum":["todo","in_progress","in_review","changes_requested","done","needs_human"]},"description":"Filter by workflow state."},{"name":"type","in":"query","schema":{"type":"string","enum":["question","incident","problem","task"]},"description":"Filter by ticket type."},{"name":"repo","in":"query","schema":{"type":"string"},"description":"Filter by git repository slug (exact match, e.g. `acme/web`).","example":"acme/web"},{"name":"assignee","in":"query","schema":{"type":"string"},"description":"Filter by assignee user-id (exact match). Supports the special value `me`, which resolves to the agent user bound to the API key. `me` returns 422 if the key has no bound agent user.","example":"u_bot_harness_01"},{"name":"from","in":"query","schema":{"type":"string","format":"date-time"},"description":"Return tickets created at or after this ISO 8601 timestamp.","example":"2024-01-01T00:00:00Z"},{"name":"to","in":"query","schema":{"type":"string","format":"date-time"},"description":"Return tickets created before this ISO 8601 timestamp.","example":"2024-12-31T23:59:59Z"},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":50},"description":"Maximum number of tickets to return (1–100, default 50)."},{"name":"cursor","in":"query","schema":{"type":"string"},"description":"Pagination cursor returned by a previous response as `nextCursor`."}],"responses":{"200":{"description":"Paginated list of tickets.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TicketList"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/tickets/{id}":{"get":{"operationId":"getTicket","summary":"Get a ticket","description":"Returns a single ticket by its internal ID or public reference (e.g. `HD-1042`).","tags":["Tickets"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Internal ticket ID or public reference such as `HD-1042`.","example":"HD-1042"}],"responses":{"200":{"description":"Ticket details.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Ticket"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/InternalError"}}},"patch":{"operationId":"updateTicket","summary":"Update a ticket","description":"Partially update a ticket. Only the fields present in the request body are modified. Fields not present are left unchanged. To clear a nullable field, send it explicitly as `null`. The `metadata` and `tags` fields use replace semantics — a PATCH carrying either replaces the whole map/set. Use `addTags`/`removeTags` for incremental tag edits. Safe to retry: PATCH is idempotent — repeating the same body has no additional side effects beyond the final field state.","tags":["Tickets"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Internal ticket ID or public reference such as `HD-1042`.","example":"HD-1042"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","enum":["open","pending","on_hold","closed"]},"priority":{"type":"string","enum":["low","normal","high","urgent"]},"type":{"type":"string","enum":["question","incident","problem","task"],"nullable":true},"workflowState":{"type":"string","enum":["todo","in_progress","in_review","changes_requested","done","needs_human"],"nullable":true},"repo":{"type":"string","nullable":true,"description":"Git repository slug in `owner/name` form (e.g. `acme/web`)."},"metadata":{"type":"object","nullable":true,"additionalProperties":{"oneOf":[{"type":"string"},{"type":"number"},{"type":"boolean"}]},"description":"Replaces the entire metadata map (not merged). Send `null` to clear all metadata. Max 4 KB serialized."},"assignee":{"type":"string","nullable":true,"description":"User-id string of the agent assigned to this ticket. Send `null` to unassign."},"tags":{"type":"array","items":{"type":"string"},"description":"Replace the ticket's whole tag set with these labels. Labels that don't exist yet are created; matched case-insensitively. Diffed server-side, so tags common to both sets are left untouched. Mutually exclusive with `addTags`/`removeTags`. Send `[]` to clear every tag.","example":["billing","urgent"]},"addTags":{"type":"array","items":{"type":"string"},"description":"Attach these labels, leaving existing tags in place. Mutually exclusive with `tags`.","example":["escalated"]},"removeTags":{"type":"array","items":{"type":"string"},"description":"Detach these labels. Labels the ticket isn't carrying are ignored, and the tag itself stays in the workspace. Mutually exclusive with `tags`.","example":["needs-triage"]}}}}}},"responses":{"200":{"description":"Updated ticket.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Ticket"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/ValidationError"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/tickets/{id}/messages":{"get":{"operationId":"listMessages","summary":"List messages on a ticket","description":"Returns the ticket's public messages in chronological order. Pass `includeInternal=true` to also return internal notes, forwards and system messages, each marked with `visibility`.","tags":["Messages"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Internal ticket ID or public reference such as `HD-1042`.","example":"HD-1042"},{"name":"includeInternal","in":"query","required":false,"schema":{"type":"boolean","default":false},"description":"When `true`, internal messages are included alongside public ones. Don't forward these to customers."}],"responses":{"200":{"description":"List of messages on the ticket.","content":{"application/json":{"schema":{"type":"object","properties":{"messages":{"type":"array","items":{"$ref":"#/components/schemas/Message"}}},"required":["messages"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/InternalError"}}},"post":{"operationId":"addMessage","summary":"Add a message to a ticket","description":"Adds a new outbound message to an existing ticket. The message is attributed to the API caller.","tags":["Messages"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Internal ticket ID or public reference such as `HD-1042`.","example":"HD-1042"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["body"],"properties":{"body":{"type":"string","description":"Message body text or HTML.","example":"We have identified the issue and will have a fix out shortly."},"authorName":{"type":"string","description":"Display name to attribute the message to. Defaults to the API key name.","example":"Support Bot"},"attachments":{"type":"array","maxItems":10,"description":"Optional file attachments anchored to this message. Each item must already have been uploaded via POST /upload; pass the resulting storageId for each file.","items":{"type":"object","required":["storageId","filename","mimeType","size"],"properties":{"storageId":{"type":"string"},"filename":{"type":"string","example":"screenshot.png"},"mimeType":{"type":"string","example":"image/png"},"size":{"type":"integer","example":48720}}}}}}}}},"responses":{"201":{"description":"Message created successfully.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"Internal ID of the new message.","example":"m12xyz789"}},"required":["id"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/ValidationError"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/tickets/{id}/notes":{"post":{"operationId":"addInternalNote","summary":"Add an internal note to a ticket","description":"Adds an internal (agent-only) note to a ticket. Internal notes are NEVER sent to the customer — they remain on the agent-side timeline alongside notes typed in the dashboard. Useful for surfacing context from external automations (alerts, deploy events, runbook output). Requires a server-side API key; widget keys are rejected.","tags":["Messages"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Internal ticket ID or public reference such as `HD-1042`.","example":"HD-1042"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["body"],"properties":{"body":{"type":"string","description":"Note body. Plain text or HTML — auto-detected. Customers never see this.","example":"Deploy 2025.06.01-A finished at 14:02 UTC. Webhook delivery should resume."},"authorName":{"type":"string","description":"Display name to attribute the note to. Defaults to `API`.","example":"Deploy Bot"}}}}}},"responses":{"201":{"description":"Internal note created successfully.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"Internal ID of the new note.","example":"m12xyz789"}},"required":["id"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Returned when a widget API key is used. Internal notes require a server-side key."},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/ValidationError"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/tickets/{id}/attachments":{"post":{"operationId":"attachToTicket","summary":"Attach files to an existing ticket","description":"Adds one or more ticket-level attachments to an existing ticket. The bytes must already be uploaded via POST /upload first; pass the resulting storageId for each file. Up to 10 attachments per call. Requires the `attachments.attach` scope — server-side API keys only; widget keys are rejected (widget keys only hold `attachments.upload` which covers the presigned-URL step, not attaching to existing records).","tags":["Attachments"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Internal ticket ID or public reference such as `HD-1042`.","example":"HD-1042"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["attachments"],"properties":{"attachments":{"type":"array","minItems":1,"maxItems":10,"items":{"type":"object","required":["storageId","filename","mimeType","size"],"properties":{"storageId":{"type":"string","description":"Convex storage ID returned by POST /upload after the byte PUT completes."},"filename":{"type":"string","example":"deploy-log.txt"},"mimeType":{"type":"string","example":"text/plain"},"size":{"type":"integer","example":12480}}}}}}}}},"responses":{"201":{"description":"Attachments created.","content":{"application/json":{"schema":{"type":"object","required":["ids","downloadUrls"],"properties":{"ids":{"type":"array","items":{"type":"string"},"description":"Internal IDs of the newly-created attachments, in input order."},"downloadUrls":{"type":"array","items":{"type":"string"},"description":"Convenience array of download paths matching `ids`."}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Returned when a widget API key is used. Attaching to existing records requires the server-only `attachments.attach` scope."},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/ValidationError"},"500":{"$ref":"#/components/responses/InternalError"}}},"get":{"operationId":"listAttachments","summary":"List attachments on a ticket","description":"Returns metadata for every attachment linked to a ticket — both ticket-level uploads and attachments inside individual messages within the thread. Each item includes a `downloadUrl` pointing at the proxy endpoint, which re-authenticates every fetch. Requires a server-side API key; widget keys are rejected. Note: emailed tickets often include inline images embedded in the message body (e.g. signature logos like `image001.png`); these appear here with `contentDisposition: \"inline\"` alongside any real file attachments — filter on `contentDisposition` if you only want user-attached files.","tags":["Attachments"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Internal ticket ID or public reference such as `HD-1042`.","example":"HD-1042"}],"responses":{"200":{"description":"Attachment list.","content":{"application/json":{"schema":{"type":"object","required":["attachments"],"properties":{"attachments":{"type":"array","items":{"type":"object","required":["id","filename","mimeType","size","createdAt","source","downloadUrl"],"properties":{"id":{"type":"string","example":"k57xyz789..."},"filename":{"type":"string","example":"invoice.pdf"},"mimeType":{"type":"string","example":"application/pdf"},"size":{"type":"integer","description":"Size in bytes.","example":184320},"createdAt":{"type":"integer","description":"Unix epoch millis at upload time.","example":1748448120000},"messageId":{"type":["string","null"],"description":"When the attachment was added with a specific message, the ID of that message; otherwise null.","example":"m12abc"},"source":{"type":"string","enum":["upload","email_inbound","email_outbound","whatsapp_inbound"],"description":"How the attachment got onto the ticket."},"contentDisposition":{"type":["string","null"],"enum":["inline","attachment",null],"description":"Whether the file was meant to render inline (typical for images in HTML emails) or to be downloaded."},"downloadUrl":{"type":"string","description":"Relative path to the byte-streaming download endpoint. Authenticate the GET with the same bearer key.","example":"/api/v1/tickets/HD-1042/attachments/k57xyz789..."}}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Returned when a widget API key is used. Attachment listings require a server-side key."},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/tickets/{id}/attachments/{attachmentId}":{"get":{"operationId":"downloadAttachment","summary":"Download a ticket attachment","description":"Streams the raw bytes of a single attachment, proxied through this endpoint so every download is auth-gated. Response carries `Content-Type` matching the stored MIME, `Content-Length` matching the recorded size, and a `Content-Disposition: attachment` so browsers save rather than render. Requires a server-side API key; widget keys are rejected.","tags":["Attachments"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Internal ticket ID or public reference such as `HD-1042`.","example":"HD-1042"},{"name":"attachmentId","in":"path","required":true,"schema":{"type":"string"},"description":"Attachment ID from the list endpoint. Must belong to the same ticket; the endpoint 404s otherwise.","example":"k57xyz789..."}],"responses":{"200":{"description":"Raw file bytes.","content":{"application/octet-stream":{"schema":{"type":"string","format":"binary"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Returned when a widget API key is used. Attachment downloads require a server-side key."},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/InternalError"},"502":{"description":"Storage backend was unreachable or returned an error. Retry shortly."}}}},"/cards/{id}/attachments":{"get":{"operationId":"listCardAttachments","summary":"List attachments on a project card","description":"Returns metadata for every attachment linked to a project card. Cards are tenant-scoped, so any server-side key in the tenant can read attachments on any card in the same tenant. Each item includes a `downloadUrl` pointing at the proxy endpoint. Server-side API key only.","tags":["Attachments"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Internal card ID or public reference such as `PM-1042`.","example":"PM-1042"}],"responses":{"200":{"description":"Attachment list.","content":{"application/json":{"schema":{"type":"object","required":["attachments"],"properties":{"attachments":{"type":"array","items":{"type":"object","required":["id","filename","mimeType","size","createdAt","source","downloadUrl"],"properties":{"id":{"type":"string"},"filename":{"type":"string","example":"spec.pdf"},"mimeType":{"type":"string","example":"application/pdf"},"size":{"type":"integer","example":184320},"createdAt":{"type":"integer"},"source":{"type":"string","enum":["upload","email_inbound","email_outbound","whatsapp_inbound"]},"contentDisposition":{"type":["string","null"],"enum":["inline","attachment",null]},"downloadUrl":{"type":"string","example":"/api/v1/cards/PM-1042/attachments/k57xyz789..."}}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Returned when a widget API key is used. Card attachment listings require a server-side key."},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/InternalError"}}},"post":{"operationId":"addCardAttachments","summary":"Attach files to a card","description":"Attaches one or more files (up to 10) to a card. PUT the bytes to a Convex storage URL first via POST /upload, then call this endpoint with the resulting storageId per file. Requires the `attachments.attach` scope — server-side API keys only; widget keys are rejected.","tags":["Cards"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["attachments"],"properties":{"attachments":{"type":"array","minItems":1,"maxItems":10,"items":{"type":"object","required":["storageId","filename","mimeType","size"],"properties":{"storageId":{"type":"string"},"filename":{"type":"string"},"mimeType":{"type":"string"},"size":{"type":"integer"}}}}}}}}},"responses":{"201":{"description":"Attachments created.","content":{"application/json":{"schema":{"type":"object","properties":{"ids":{"type":"array","items":{"type":"string"}},"downloadUrls":{"type":"array","items":{"type":"string"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Widget keys cannot attach to existing records (`attachments.attach` scope required)."},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/ValidationError"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/cards/{id}/attachments/{attachmentId}":{"get":{"operationId":"downloadCardAttachment","summary":"Download a card attachment","description":"Streams the raw bytes of a single card attachment, proxied through this endpoint so every download is auth-gated. Server-side API key only.","tags":["Attachments"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Internal card ID or public reference such as `PM-1042`.","example":"PM-1042"},{"name":"attachmentId","in":"path","required":true,"schema":{"type":"string"},"description":"Attachment ID from the list endpoint. Must belong to the same card; the endpoint 404s otherwise."}],"responses":{"200":{"description":"Raw file bytes.","content":{"application/octet-stream":{"schema":{"type":"string","format":"binary"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Returned when a widget API key is used. Card attachment downloads require a server-side key."},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/InternalError"},"502":{"description":"Storage backend was unreachable or returned an error. Retry shortly."}}}},"/projects/{id}/attachments":{"get":{"operationId":"listProjectAttachments","summary":"List every attachment under a project","description":"Aggregated roll-up of every file stored under a project — direct project-level uploads (the Library → Files tab) **plus** the attachments of every card in the project. Each row's `attachedTo` field tells you where the file actually lives (kind: `project` or `card`, with the card id/public id/title when applicable). Sorted newest-first. Each row carries a `downloadUrl` pointing at this project's proxy, so callers don't need to know the underlying card. Server-side API key only.","tags":["Attachments"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Project ID (Convex id)."}],"responses":{"200":{"description":"Aggregated attachment list.","content":{"application/json":{"schema":{"type":"object","required":["attachments"],"properties":{"attachments":{"type":"array","items":{"type":"object","required":["id","filename","mimeType","size","createdAt","source","attachedTo","downloadUrl"],"properties":{"id":{"type":"string"},"filename":{"type":"string","example":"datasheet.pdf"},"mimeType":{"type":"string","example":"application/pdf"},"size":{"type":"integer","example":27418},"createdAt":{"type":"integer"},"source":{"type":"string","enum":["upload","email_inbound","email_outbound","whatsapp_inbound"]},"contentDisposition":{"type":["string","null"],"enum":["inline","attachment",null]},"attachedTo":{"type":"object","required":["kind","projectId","cardId","cardPublicId","cardTitle"],"properties":{"kind":{"type":"string","enum":["project","card"]},"projectId":{"type":"string"},"cardId":{"type":["string","null"]},"cardPublicId":{"type":["string","null"],"example":"PM-1042"},"cardTitle":{"type":["string","null"]}}},"downloadUrl":{"type":"string","example":"/api/v1/projects/r57.../attachments/k57..."}}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Returned when a widget API key is used. Project attachment listings require a server-side key."},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/InternalError"}}},"post":{"operationId":"addProjectAttachments","summary":"Attach files directly to a project","description":"Uploads one or more files (up to 10) as project-level documents. They appear in the Library → Files tab and in subsequent GET responses with `attachedTo.kind = \"project\"`. PUT the bytes to a Convex storage URL first via POST /upload, then call this endpoint with the resulting storageId per file. Requires the `attachments.attach` scope — server-side API keys only; widget keys are rejected.","tags":["Attachments"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Project ID (Convex id)."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["attachments"],"properties":{"attachments":{"type":"array","minItems":1,"maxItems":10,"items":{"type":"object","required":["storageId","filename","mimeType","size"],"properties":{"storageId":{"type":"string"},"filename":{"type":"string"},"mimeType":{"type":"string"},"size":{"type":"integer"}}}}}}}}},"responses":{"201":{"description":"Attachments created.","content":{"application/json":{"schema":{"type":"object","properties":{"ids":{"type":"array","items":{"type":"string"}},"downloadUrls":{"type":"array","items":{"type":"string"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Widget keys cannot attach to existing records (`attachments.attach` scope required)."},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/ValidationError"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/projects/{id}/attachments/{attachmentId}":{"get":{"operationId":"downloadProjectAttachment","summary":"Download any attachment under a project","description":"Streams the bytes of any attachment that lives under the project — direct project uploads **and** attachments on any card in the project. The resolver verifies ownership in both directions so the proxy never leaks an attachment that belongs to another project or tenant. Server-side API key only.","tags":["Attachments"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Project ID (Convex id)."},{"name":"attachmentId","in":"path","required":true,"schema":{"type":"string"},"description":"Attachment ID from the list endpoint. Must be project-level or attached to a card in this project; otherwise 404."}],"responses":{"200":{"description":"Raw file bytes.","content":{"application/octet-stream":{"schema":{"type":"string","format":"binary"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Returned when a widget API key is used. Project attachment downloads require a server-side key."},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/InternalError"},"502":{"description":"Storage backend was unreachable or returned an error. Retry shortly."}}}},"/widget/config":{"get":{"operationId":"getWidgetConfig","summary":"Get widget configuration","description":"Returns the help desk name and description for use by the embeddable widget. Authenticated with the same desk-scoped API key.","tags":["Widget"],"responses":{"200":{"description":"Widget configuration.","content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","description":"Help desk name.","example":"Acme Support"},"description":{"type":"string","nullable":true,"description":"Help desk description."},"tenantName":{"type":"string","nullable":true,"description":"Organization name.","example":"Acme Corp"}},"required":["name"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/InternalError"}}}},"https://intellidesk.co/api/mcp":{"servers":[{"url":"https://intellidesk.co"}],"post":{"tags":["MCP"],"summary":"Model Context Protocol endpoint","description":"Connect any MCP client (Claude Code, OpenAI Codex CLI, Cortex, custom agents) to IntelliDesk. Speaks JSON-RPC 2.0 over Streamable HTTP. Supports two authentication paths: Clerk OAuth 2.1 with PKCE (interactive, browser flow — full tool surface) and `idk_mcp_*` API keys (headless — 30 of 37 tools). Tool catalog, skills, and setup snippets for each client are available in the in-app docs at **Settings → Developer → MCP server**. The endpoint advertises Clerk as its authorization server via `/.well-known/oauth-protected-resource`.","operationId":"mcpJsonRpc","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","description":"JSON-RPC 2.0 request envelope.","properties":{"jsonrpc":{"type":"string","enum":["2.0"]},"id":{"oneOf":[{"type":"string"},{"type":"number"},{"type":"null"}]},"method":{"type":"string","description":"MCP method name. Common: `initialize`, `tools/list`, `tools/call`, `resources/list`, `resources/read`, `ping`."},"params":{"type":"object","description":"Method-specific parameters."}},"required":["jsonrpc","method"]},"examples":{"initialize":{"summary":"Initialize handshake (unauthenticated)","value":{"jsonrpc":"2.0","id":1,"method":"initialize"}},"callTool":{"summary":"Call a tool (Bearer auth required)","value":{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"list_tickets","arguments":{"status":"open","limit":10}}}}}}}},"responses":{"200":{"description":"JSON-RPC 2.0 response envelope. On `tools/call` the `result.content` array contains a single text element holding the profiled-result JSON (success, status, data, reviewUrl, context).","content":{"application/json":{"schema":{"type":"object","properties":{"jsonrpc":{"type":"string","enum":["2.0"]},"id":{"oneOf":[{"type":"string"},{"type":"number"},{"type":"null"}]},"result":{"type":"object"},"error":{"type":"object","properties":{"code":{"type":"integer"},"message":{"type":"string"}}}}}}}},"401":{"description":"Returned for methods other than `initialize` / `ping` when no Bearer token is supplied (or it's invalid). Includes a `WWW-Authenticate: Bearer resource_metadata=…` header so MCP clients can trigger the OAuth flow automatically."},"405":{"description":"Returned on GET. Use POST with a JSON-RPC body."}}}},"https://intellidesk.co/.well-known/oauth-protected-resource":{"servers":[{"url":"https://intellidesk.co"}],"get":{"tags":["MCP"],"summary":"Protected Resource Metadata (RFC 9728)","description":"Points MCP clients at Clerk as the OAuth 2.1 authorization server for `/api/mcp`. Clients fetch this once, then run PKCE against Clerk to obtain a Bearer token.","operationId":"oauthProtectedResource","security":[],"responses":{"200":{"description":"Discovery JSON.","content":{"application/json":{"schema":{"type":"object","properties":{"resource":{"type":"string","example":"https://intellidesk.co"},"authorization_servers":{"type":"array","items":{"type":"string"},"example":["https://clerk.intellidesk.co"]},"bearer_methods_supported":{"type":"array","items":{"type":"string"},"example":["header"]},"scopes_supported":{"type":"array","items":{"type":"string"},"example":["openid","profile","email"]}}}}}}}}},"/projects":{"get":{"operationId":"listProjects","summary":"List projects","description":"Returns projects in the tenant with cursor pagination. Active (non-archived) projects only by default; pass `?archived=true` to list archived projects.","tags":["Projects"],"parameters":[{"name":"archived","in":"query","schema":{"type":"boolean"},"description":"Filter by archived state. Omit to return active projects."},{"name":"cursor","in":"query","schema":{"type":"string"},"description":"Pagination cursor from a previous response. Omit for the first page."},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":200,"default":50},"description":"Maximum number of projects to return per page."}],"responses":{"200":{"description":"List of projects.","content":{"application/json":{"schema":{"type":"object","properties":{"projects":{"type":"array","items":{"type":"object"}},"cursor":{"type":"string","description":"Opaque cursor for the next page."},"isDone":{"type":"boolean","description":"True when no more pages are available."}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Widget API keys cannot read projects."},"500":{"$ref":"#/components/responses/InternalError"}}},"post":{"operationId":"createProject","summary":"Create a project","description":"Creates a new project in the tenant with the default workflow (todo / in_progress / done). API-created projects start with no members — assign them from the dashboard.","tags":["Projects"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name"],"properties":{"name":{"type":"string","minLength":1,"maxLength":100},"description":{"type":"string"}}}}}},"responses":{"201":{"description":"Project created.","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Widget keys cannot create projects."},"422":{"$ref":"#/components/responses/ValidationError"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/projects/{id}":{"get":{"operationId":"getProject","summary":"Get a project","description":"Returns project metadata plus card and member counts.","tags":["Projects"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Project details.","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Widget keys cannot read projects."},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/InternalError"}}},"patch":{"operationId":"updateProject","summary":"Update a project","description":"Partially updates project name, description, defaultView, or defaultLayout. Only fields present in the body are modified.","tags":["Projects"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":100},"description":{"type":"string"},"defaultView":{"type":"string","enum":["board","table"]},"defaultLayout":{"type":"string","enum":["grouped","expanded"]}}}}}},"responses":{"200":{"description":"Project updated.","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Widget keys cannot update projects."},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/ValidationError"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/projects/{id}/archive":{"post":{"operationId":"archiveProject","summary":"Archive or restore a project","description":"Sets `isArchived` to the supplied boolean. Pass `{ archived: true }` to archive, `{ archived: false }` to restore.","tags":["Projects"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["archived"],"properties":{"archived":{"type":"boolean"}}}}}},"responses":{"200":{"description":"Updated project.","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Widget keys cannot update projects."},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/ValidationError"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/tags":{"get":{"operationId":"listTags","summary":"List workspace tags","description":"Every tag in the workspace with usage counts split by tickets and cards, most-used first. Tags are shared across tickets and project cards, so this is the vocabulary the `?tag=` filters and the `tags`/`addTags`/`removeTags` write fields accept. Matching is case-insensitive — prefer reusing a label listed here over creating a near-duplicate.","tags":["Tags"],"responses":{"200":{"description":"Workspace tags.","content":{"application/json":{"schema":{"type":"object","properties":{"tags":{"type":"array","items":{"allOf":[{"$ref":"#/components/schemas/Tag"},{"type":"object","properties":{"ticketCount":{"type":"integer","description":"Tickets carrying this tag."},"cardCount":{"type":"integer","description":"Project cards carrying this tag."},"usageCount":{"type":"integer","description":"ticketCount + cardCount."},"usageCapped":{"type":"boolean","description":"True when the tag is used more times than the counting cap, so the counts are a floor rather than a total."}}}]}}},"required":["tags"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Widget and MCP keys cannot read tags."},"500":{"$ref":"#/components/responses/InternalError"}}}},"/projects/{projectId}/cards":{"get":{"operationId":"listProjectCards","summary":"List cards in a project","description":"Returns the cards in a project, ordered by board position. Filter by `?stage=`, `?status=` (sub-status slug), or `?tag=`. Each card carries its `tags`.","tags":["Cards"],"parameters":[{"name":"projectId","in":"path","required":true,"schema":{"type":"string"}},{"name":"stage","in":"query","schema":{"type":"string","enum":["todo","in_progress","done"]}},{"name":"status","in":"query","schema":{"type":"string"}},{"name":"tag","in":"query","schema":{"type":"string"},"description":"Filter by tag label. Comma-separated, case-insensitive. Aliased as `tags`.","example":"backend,p1"},{"name":"tagMatch","in":"query","schema":{"type":"string","enum":["all","any"],"default":"all"},"description":"How multiple `tag` values combine. `all` (default) requires every listed tag; `any` requires at least one."}],"responses":{"200":{"description":"List of cards.","content":{"application/json":{"schema":{"type":"object","properties":{"cards":{"type":"array","items":{"type":"object"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Widget keys cannot read cards."},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/ValidationError"},"500":{"$ref":"#/components/responses/InternalError"}}},"post":{"operationId":"createCard","summary":"Create a card","description":"Create a card in a project. Defaults stage to `todo` and priority to `normal`.","tags":["Cards"],"parameters":[{"name":"projectId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["title"],"properties":{"title":{"type":"string","minLength":1,"maxLength":300},"description":{"type":"string"},"stage":{"type":"string","enum":["todo","in_progress","done"]},"status":{"type":"string","description":"Project status slug."},"priority":{"type":"string","enum":["low","normal","high","urgent"]},"dueDate":{"type":"integer","format":"int64","description":"Unix ms timestamp."},"tags":{"type":"array","items":{"type":"string"},"description":"Labels to attach to the new card. Stored as project labels, so they show on the board. Created if the project doesn't have them yet.","example":["backend","p1"]}}}}}},"responses":{"201":{"description":"Card created.","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Widget keys cannot create cards."},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/ValidationError"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/cards/{id}":{"get":{"operationId":"getCard","summary":"Get a card","description":"Returns full card detail — description, labels, tags, members, checklists, comments, attachment metadata, and the linked ticket if any. `labels` is the board-native per-project view; `tags` is the same set in the shared tag system that `?tag=` filters on. Accepts a Convex id or a `PM-####` reference.","tags":["Cards"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"example":"PM-1042"}],"responses":{"200":{"description":"Card detail.","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Widget keys cannot read cards."},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/InternalError"}}},"patch":{"operationId":"updateCard","summary":"Update a card","description":"Updates title, description, priority, dueDate, sub-status, or tags. Send `null` for description / dueDate / status to clear the field. `tags` replaces the whole set; `addTags`/`removeTags` are incremental.","tags":["Cards"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"title":{"type":"string","minLength":1,"maxLength":300},"description":{"type":"string","nullable":true},"priority":{"type":"string","enum":["low","normal","high","urgent"]},"dueDate":{"type":"integer","format":"int64","nullable":true},"status":{"type":"string","nullable":true},"tags":{"type":"array","items":{"type":"string"},"description":"Replace the card's whole tag set. Card tags are stored as project labels, so they appear on the board as well as in the shared tag system. Labels that don't exist on the project are created. Mutually exclusive with `addTags`/`removeTags`; send `[]` to clear.","example":["backend","p1"]},"addTags":{"type":"array","items":{"type":"string"},"description":"Attach these labels, leaving existing ones in place. Mutually exclusive with `tags`."},"removeTags":{"type":"array","items":{"type":"string"},"description":"Detach these labels. Ones the card isn't carrying are ignored, and the label stays available on the project. Mutually exclusive with `tags`."}}}}}},"responses":{"200":{"description":"Card updated.","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Widget keys cannot update cards."},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/ValidationError"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/cards/{id}/move":{"post":{"operationId":"moveCard","summary":"Move a card between stages","description":"Moves the card to the supplied stage and appends an activity comment to the timeline.","tags":["Cards"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["stage"],"properties":{"stage":{"type":"string","enum":["todo","in_progress","done"]}}}}}},"responses":{"200":{"description":"Card moved.","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Widget keys cannot move cards."},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/ValidationError"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/cards/{id}/comments":{"post":{"operationId":"addCardComment","summary":"Comment on a card","description":"Append a comment to the card timeline. Visible to every member of the parent project.","tags":["Cards"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["body"],"properties":{"body":{"type":"string","minLength":1,"maxLength":10000}}}}}},"responses":{"201":{"description":"Comment created.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Widget keys cannot post card comments."},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/ValidationError"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/cards/{id}/complete":{"post":{"operationId":"completeCard","summary":"Complete a card","description":"Shortcut for moving a card to the `done` stage with an optional completion summary posted as a comment.","tags":["Cards"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"summary":{"type":"string","maxLength":10000}}}}}},"responses":{"200":{"description":"Card marked done.","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Widget keys cannot complete cards."},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/ValidationError"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/projects/{projectId}/notes":{"get":{"operationId":"listProjectNotes","summary":"List project notes","description":"Returns project notes (Google-Keep-style). Pinned first, then newest. Pass `?includeArchived=true` to list archived notes instead.","tags":["Notes"],"parameters":[{"name":"projectId","in":"path","required":true,"schema":{"type":"string"}},{"name":"includeArchived","in":"query","schema":{"type":"boolean"}}],"responses":{"200":{"description":"List of notes.","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Widget keys cannot read notes."},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/InternalError"}}},"post":{"operationId":"createProjectNote","summary":"Create a project note","description":"Create a note. At least one of `title` or `body` is required.","tags":["Notes"],"parameters":[{"name":"projectId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"title":{"type":"string","maxLength":200},"body":{"type":"string","maxLength":50000},"color":{"type":"string"}}}}}},"responses":{"201":{"description":"Note created.","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Widget keys cannot create notes."},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/ValidationError"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/notes/{id}":{"get":{"operationId":"getProjectNote","summary":"Get a project note","description":"Return a single project note by id.","tags":["Notes"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Note details.","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Widget keys cannot read notes."},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/companies":{"get":{"operationId":"listCompanies","summary":"List companies","description":"List companies in the tenant with cursor pagination. Optional `?search=` (substring of name or domain), `?limit=` (max 500, default 100), and `?cursor=` for the next page.","tags":["Companies"],"parameters":[{"name":"search","in":"query","schema":{"type":"string"}},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":500,"default":100}},{"name":"cursor","in":"query","schema":{"type":"string"},"description":"Pagination cursor from a previous response."}],"responses":{"200":{"description":"List of companies.","content":{"application/json":{"schema":{"type":"object","properties":{"companies":{"type":"array","items":{"type":"object"}},"cursor":{"type":"string","description":"Opaque cursor for the next page."},"isDone":{"type":"boolean","description":"True when no more pages are available."}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Widget keys cannot read companies."},"422":{"$ref":"#/components/responses/ValidationError"},"500":{"$ref":"#/components/responses/InternalError"}}},"post":{"operationId":"createCompany","summary":"Create a company","description":"Create a company in the tenant. The domain is lower-cased before storage.","tags":["Companies"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name","domain"],"properties":{"name":{"type":"string","minLength":1,"maxLength":200},"domain":{"type":"string","minLength":1,"maxLength":200}}}}}},"responses":{"201":{"description":"Company created.","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Widget keys cannot create companies."},"422":{"$ref":"#/components/responses/ValidationError"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/companies/{id}":{"get":{"operationId":"getCompany","summary":"Get a company","description":"Returns the company with the count of contacts linked to it.","tags":["Companies"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Company details.","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Widget keys cannot read companies."},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/InternalError"}}},"patch":{"operationId":"updateCompany","summary":"Update a company","description":"Update name and/or domain.","tags":["Companies"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":200},"domain":{"type":"string","minLength":1,"maxLength":200}}}}}},"responses":{"200":{"description":"Company updated.","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Widget keys cannot update companies."},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/ValidationError"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/companies/{id}/contacts":{"post":{"operationId":"linkContactToCompany","summary":"Link an existing contact to this company","description":"Sets the contact's `companyId` to this company. Does NOT create the contact — use POST /contacts to create one first.","tags":["Companies"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["contactId"],"properties":{"contactId":{"type":"string"}}}}}},"responses":{"200":{"description":"Contact linked.","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Widget keys cannot modify companies."},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/ValidationError"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/contacts":{"get":{"operationId":"listContacts","summary":"List contacts","description":"List contacts in the tenant with cursor pagination. Optional `?search=`, `?companyId=`, `?limit=`, and `?cursor=` for the next page.","tags":["Contacts"],"parameters":[{"name":"search","in":"query","schema":{"type":"string"}},{"name":"companyId","in":"query","schema":{"type":"string"}},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":500,"default":100}},{"name":"cursor","in":"query","schema":{"type":"string"},"description":"Pagination cursor from a previous response."}],"responses":{"200":{"description":"List of contacts.","content":{"application/json":{"schema":{"type":"object","properties":{"contacts":{"type":"array","items":{"type":"object"}},"cursor":{"type":"string","description":"Opaque cursor for the next page."},"isDone":{"type":"boolean","description":"True when no more pages are available."}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Widget keys cannot read contacts."},"422":{"$ref":"#/components/responses/ValidationError"},"500":{"$ref":"#/components/responses/InternalError"}}},"post":{"operationId":"createContact","summary":"Create a contact","description":"Create a contact. If the email already exists in the tenant, the existing contact is updated in-place (no duplicate inserted).","tags":["Contacts"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name","email"],"properties":{"name":{"type":"string","minLength":1},"email":{"type":"string","format":"email"},"phone":{"type":"string"},"companyId":{"type":"string"}}}}}},"responses":{"201":{"description":"Contact created or updated.","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Widget keys cannot create contacts."},"422":{"$ref":"#/components/responses/ValidationError"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/contacts/{id}":{"get":{"operationId":"getContact","summary":"Get a contact","description":"Returns the contact plus `ticketCount` and `openTicketCount`.","tags":["Contacts"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Contact details.","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Widget keys cannot read contacts."},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/InternalError"}}},"patch":{"operationId":"updateContact","summary":"Update a contact","description":"Update name, email, phone, or companyId. Send `null` for phone or companyId to clear.","tags":["Contacts"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1},"email":{"type":"string","format":"email"},"phone":{"type":"string","nullable":true},"companyId":{"type":"string","nullable":true}}}}}},"responses":{"200":{"description":"Contact updated.","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Widget keys cannot update contacts."},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/ValidationError"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/kb/articles":{"get":{"operationId":"listKbArticles","summary":"List KB articles","description":"Lists KB articles in the tenant with cursor pagination. Optional `?categorySlug=`, `?status=draft|published`, `?limit=`, and `?cursor=` for the next page. Agent-only — widget keys are rejected via scope so a leaked widget key cannot read draft or internal content.","tags":["Knowledge Base"],"parameters":[{"name":"categorySlug","in":"query","schema":{"type":"string"}},{"name":"status","in":"query","schema":{"type":"string","enum":["draft","published"]}},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":500,"default":100}},{"name":"cursor","in":"query","schema":{"type":"string"},"description":"Pagination cursor from a previous response."}],"responses":{"200":{"description":"List of articles.","content":{"application/json":{"schema":{"type":"object","properties":{"articles":{"type":"array","items":{"type":"object"}},"cursor":{"type":"string","description":"Opaque cursor for the next page."},"isDone":{"type":"boolean","description":"True when no more pages are available."}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Widget keys cannot access KB articles."},"422":{"$ref":"#/components/responses/ValidationError"},"500":{"$ref":"#/components/responses/InternalError"}}},"post":{"operationId":"createKbArticle","summary":"Create a KB article","description":"Create an article. Defaults to `status: draft`. The slug is derived from the title and made unique within the tenant.","tags":["Knowledge Base"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["title","body","categoryId"],"properties":{"title":{"type":"string","minLength":1,"maxLength":300},"body":{"type":"string","minLength":1},"categoryId":{"type":"string"},"status":{"type":"string","enum":["draft","published"],"default":"draft"},"slug":{"type":"string"}}}}}},"responses":{"201":{"description":"Article created.","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Widget keys cannot create KB articles."},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/ValidationError"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/kb/articles/{id}":{"get":{"operationId":"getKbArticle","summary":"Get a KB article","description":"Returns a single article. Accepts a Convex id OR a slug — id is tried first, slug is used as a fallback.","tags":["Knowledge Base"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Article.","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Widget keys cannot access KB articles."},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/InternalError"}}},"patch":{"operationId":"updateKbArticle","summary":"Update a KB article","description":"Update title / body / categoryId / status / slug. The path must be a Convex id — slug-only updates are rejected because slug is itself mutable.","tags":["Knowledge Base"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"title":{"type":"string","minLength":1,"maxLength":300},"body":{"type":"string","minLength":1},"categoryId":{"type":"string"},"status":{"type":"string","enum":["draft","published"]},"slug":{"type":"string"}}}}}},"responses":{"200":{"description":"Article updated.","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Widget keys cannot update KB articles."},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/ValidationError"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/time-entries":{"get":{"operationId":"listTimeEntries","summary":"List time entries","description":"Returns all time entries for the agent bound to the API key, ordered newest first. Optionally filter by a time window using `from` and `to` (Unix ms).","tags":["Timesheet"],"parameters":[{"name":"from","in":"query","schema":{"type":"integer","format":"int64"},"description":"Return entries with `startedAt` at or after this Unix ms timestamp.","example":1748736000000},{"name":"to","in":"query","schema":{"type":"integer","format":"int64"},"description":"Return entries with `startedAt` before this Unix ms timestamp.","example":1748822400000}],"responses":{"200":{"description":"List of time entries.","content":{"application/json":{"schema":{"type":"object","required":["entries"],"properties":{"entries":{"type":"array","items":{"$ref":"#/components/schemas/TimeEntry"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Returned when a widget API key is used."},"500":{"$ref":"#/components/responses/InternalError"}}},"post":{"operationId":"createTimeEntry","summary":"Create a time entry","description":"Creates a manual time entry for the agent bound to the API key. Exactly one of `ticketId`, `cardId`, `projectId`, `categoryId`, or `freeformLabel` must be supplied to categorise the entry. The API key must be bound to an agent user (`boundAgentUserId`).","tags":["Timesheet"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["startedAt"],"properties":{"startedAt":{"type":"integer","format":"int64","description":"Unix ms timestamp when the entry started.","example":1748736000000},"endedAt":{"type":"integer","format":"int64","nullable":true,"description":"Unix ms timestamp when the entry ended. Omit or send `null` for a running timer.","example":1748739600000},"ticketId":{"type":"string","nullable":true,"description":"Attach to a ticket."},"cardId":{"type":"string","nullable":true,"description":"Attach to a project card."},"projectId":{"type":"string","nullable":true,"description":"Attach to a project."},"categoryId":{"type":"string","nullable":true,"description":"Attach to a time category."},"freeformLabel":{"type":"string","nullable":true,"description":"Free-text label when no structured attachment applies."},"description":{"type":"string","nullable":true,"description":"Optional longer description."},"source":{"type":"string","enum":["timer","manual","suggested","pomodoro","api"],"default":"api","description":"How the entry was created. Defaults to `api`."}}}}}},"responses":{"201":{"description":"Time entry created.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TimeEntry"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Returned when a widget API key is used, or when the key has no `boundAgentUserId` (code: `agent_binding_required`). The API key must be bound to an agent user for time tracking write operations."},"422":{"$ref":"#/components/responses/ValidationError"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/time-entries/{id}":{"get":{"operationId":"getTimeEntry","summary":"Get a time entry","description":"Returns a single time entry by its internal ID.","tags":["Timesheet"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Internal time entry document ID."}],"responses":{"200":{"description":"Time entry details.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TimeEntry"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Returned when a widget API key is used."},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/InternalError"}}},"patch":{"operationId":"updateTimeEntry","summary":"Update a time entry","description":"Partially updates a time entry. All fields are optional; send `null` for nullable fields to clear them. The key must be bound to an agent user.","tags":["Timesheet"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Internal time entry document ID."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"startedAt":{"type":"integer","format":"int64"},"endedAt":{"type":"integer","format":"int64","nullable":true},"ticketId":{"type":"string","nullable":true},"cardId":{"type":"string","nullable":true},"projectId":{"type":"string","nullable":true},"categoryId":{"type":"string","nullable":true},"freeformLabel":{"type":"string","nullable":true},"description":{"type":"string","nullable":true},"source":{"type":"string","enum":["timer","manual","suggested","pomodoro","api"]}}}}}},"responses":{"200":{"description":"Updated time entry.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TimeEntry"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Returned when a widget API key is used, or when the key has no `boundAgentUserId` (code: `agent_binding_required`)."},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/ValidationError"},"500":{"$ref":"#/components/responses/InternalError"}}},"delete":{"operationId":"deleteTimeEntry","summary":"Delete a time entry","description":"Soft-deletes a time entry. The entry is marked deleted and excluded from future list and submission queries. The key must be bound to an agent user.","tags":["Timesheet"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Internal time entry document ID."}],"responses":{"204":{"description":"Entry deleted. No body."},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Returned when a widget API key is used, or when the key has no `boundAgentUserId` (code: `agent_binding_required`)."},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/time-entries/start":{"post":{"operationId":"startTimer","summary":"Start a timer","description":"Creates a running time entry (no `endedAt`) for the bound agent user. Any existing running timer for the same agent is automatically stopped before the new one starts. Exactly one of `ticketId`, `cardId`, `projectId`, `categoryId`, or `freeformLabel` is required.","tags":["Timesheet"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"ticketId":{"type":"string","nullable":true,"description":"Attach to a ticket."},"cardId":{"type":"string","nullable":true,"description":"Attach to a project card."},"projectId":{"type":"string","nullable":true,"description":"Attach to a project."},"categoryId":{"type":"string","nullable":true,"description":"Attach to a time category."},"freeformLabel":{"type":"string","nullable":true,"description":"Free-text label."},"description":{"type":"string","nullable":true,"description":"Optional longer description."}}}}}},"responses":{"201":{"description":"Timer started. Returns the new running time entry.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TimeEntry"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Returned when a widget API key is used, or when the key has no `boundAgentUserId` (code: `agent_binding_required`)."},"422":{"$ref":"#/components/responses/ValidationError"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/time-entries/{id}/stop":{"post":{"operationId":"stopTimer","summary":"Stop a running timer","description":"Sets `endedAt` to the current server time — or to the `endedAt` you pass — and computes `durationMs`. Returns 204 — fetch the entry via GET /time-entries/{id} if you need the final values. The key must be bound to an agent user. A timer that has been running for more than 24 hours can't be stopped at \"now\": pass `endedAt` (when the work actually stopped), otherwise the request is refused with 422.","tags":["Timesheet"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Internal time entry document ID of the running timer."}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"endedAt":{"type":"string","format":"date-time","description":"When the work actually stopped — ISO 8601 with a time zone (e.g. `2026-10-05T17:30:00Z`). Must be after the start, not in the future, and at most 24 hours after the start. Omit to stop now; required when the timer has run over 24 hours."}}}}}},"responses":{"204":{"description":"Timer stopped. No body."},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Returned when a widget API key is used, or when the key has no `boundAgentUserId` (code: `agent_binding_required`)."},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/ValidationError"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/time-entries/{id}/heartbeat":{"post":{"operationId":"heartbeatTimer","summary":"Send a tab-active heartbeat","description":"Signals that the browser tab is still active and the timer should continue. The server advances an internal `lastHeartbeat` timestamp used to detect stale/abandoned timers. Returns 204. The key must be bound to an agent user.","tags":["Timesheet"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Internal time entry document ID of the running timer."}],"responses":{"204":{"description":"Heartbeat recorded. No body."},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Returned when a widget API key is used, or when the key has no `boundAgentUserId` (code: `agent_binding_required`)."},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/timesheet/submissions":{"get":{"operationId":"listSubmissions","summary":"List timesheet submissions","description":"Returns all timesheet submissions for the agent bound to the API key, ordered newest first.","tags":["Timesheet"],"responses":{"200":{"description":"List of submissions.","content":{"application/json":{"schema":{"type":"object","required":["submissions"],"properties":{"submissions":{"type":"array","items":{"$ref":"#/components/schemas/TimesheetSubmission"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Returned when a widget API key is used."},"500":{"$ref":"#/components/responses/InternalError"}}},"post":{"operationId":"createSubmission","summary":"Create a timesheet submission","description":"Creates a new timesheet submission covering the specified date range. All completed (non-deleted) time entries for the bound agent user that fall within `[fromDate, toDate]` are included. Totals and an XLSX report are computed asynchronously. The key must be bound to an agent user.","tags":["Timesheet"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["fromDate","toDate"],"properties":{"fromDate":{"type":"integer","format":"int64","description":"Start of the submission period (Unix ms, inclusive).","example":1748649600000},"toDate":{"type":"integer","format":"int64","description":"End of the submission period (Unix ms, inclusive).","example":1749254400000},"note":{"type":"string","nullable":true,"description":"Optional free-text note attached to the submission."}}}}}},"responses":{"201":{"description":"Submission created.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TimesheetSubmission"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Returned when a widget API key is used, or when the key has no `boundAgentUserId` (code: `agent_binding_required`)."},"422":{"$ref":"#/components/responses/ValidationError"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/timesheet/submissions/{id}":{"get":{"operationId":"getSubmission","summary":"Get a timesheet submission","description":"Returns a single timesheet submission by its internal ID.","tags":["Timesheet"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Internal submission document ID."}],"responses":{"200":{"description":"Submission details.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TimesheetSubmission"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Returned when a widget API key is used."},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/timesheet/submissions/{id}/resubmit":{"post":{"operationId":"resubmitSubmission","summary":"Resubmit a timesheet","description":"Recomputes totals and regenerates the XLSX report for a submission. Use after adding or editing time entries in the submission window. Status is reset from `stale` back to `submitted`. Returns 204. The key must be bound to an agent user.","tags":["Timesheet"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Internal submission document ID."}],"responses":{"204":{"description":"Resubmit queued. No body."},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Returned when a widget API key is used, or when the key has no `boundAgentUserId` (code: `agent_binding_required`)."},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/ValidationError"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/timesheet/submissions/{id}/withdraw":{"post":{"operationId":"withdrawSubmission","summary":"Withdraw a timesheet submission","description":"Marks the submission as `withdrawn`. A withdrawn submission can no longer be approved; resubmit to restart the approval flow. Returns 204. The key must be bound to an agent user.","tags":["Timesheet"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Internal submission document ID."}],"responses":{"204":{"description":"Submission withdrawn. No body."},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Returned when a widget API key is used, or when the key has no `boundAgentUserId` (code: `agent_binding_required`)."},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/ValidationError"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/timesheet/submissions/{id}/download":{"get":{"operationId":"downloadSubmission","summary":"Download submission XLSX","description":"Redirects (302) to a short-lived signed URL for the XLSX export of the submission. Fetch the URL in the `Location` header to stream the file. Returns 404 if the XLSX has not finished generating yet (`xlsxStatus` is not `ready`) or if the submission does not exist.","tags":["Timesheet"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Internal submission document ID."}],"responses":{"302":{"description":"Redirect to a signed XLSX download URL.","headers":{"Location":{"schema":{"type":"string","format":"uri"},"description":"Signed URL for the XLSX file. Valid for a short window."}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Returned when a widget API key is used."},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/time-categories":{"get":{"operationId":"listTimeCategories","summary":"List time categories","description":"Returns all workspace-level time categories. Active (non-archived) categories are returned by default; pass `?includeArchived=true` to include archived ones.","tags":["Time Categories"],"parameters":[{"name":"includeArchived","in":"query","schema":{"type":"boolean","default":false},"description":"Set `true` to include archived categories in the response."}],"responses":{"200":{"description":"List of time categories.","content":{"application/json":{"schema":{"type":"object","required":["categories"],"properties":{"categories":{"type":"array","items":{"$ref":"#/components/schemas/TimeCategory"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Returned when a widget API key is used."},"500":{"$ref":"#/components/responses/InternalError"}}},"post":{"operationId":"createTimeCategory","summary":"Create a time category","description":"Creates a new workspace-level time category. The `color` must be a 6-digit hex string without the leading `#` (e.g. `3b82f6`). The key must be bound to an agent user.","tags":["Time Categories"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["label","color"],"properties":{"label":{"type":"string","minLength":1,"maxLength":100,"description":"Display name for the category.","example":"Client Calls"},"color":{"type":"string","pattern":"^[0-9a-fA-F]{6}$","description":"6-digit hex colour without a leading `#`.","example":"3b82f6"}}}}}},"responses":{"201":{"description":"Time category created.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TimeCategory"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Returned when a widget API key is used, or when the key has no `boundAgentUserId` (code: `agent_binding_required`)."},"422":{"$ref":"#/components/responses/ValidationError"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/time-categories/{id}":{"patch":{"operationId":"updateTimeCategory","summary":"Update a time category","description":"Partially updates a time category. All fields are optional. Send `archived: true` to archive a category; archived categories are hidden from the default list. The key must be bound to an agent user.","tags":["Time Categories"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Internal time category document ID."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"label":{"type":"string","minLength":1,"maxLength":100,"description":"New display name."},"color":{"type":"string","pattern":"^[0-9a-fA-F]{6}$","description":"6-digit hex colour without a leading `#`."},"archived":{"type":"boolean","description":"Set `true` to archive, `false` to restore."},"position":{"type":"integer","description":"Display sort position (zero-based)."}}}}}},"responses":{"200":{"description":"Updated time category.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TimeCategory"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Returned when a widget API key is used, or when the key has no `boundAgentUserId` (code: `agent_binding_required`)."},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/ValidationError"},"500":{"$ref":"#/components/responses/InternalError"}}}}},"tags":[{"name":"Tickets","description":"Create, list, retrieve, and update support tickets."},{"name":"Messages","description":"Read and add messages within a ticket thread."},{"name":"Projects","description":"Create, list, and update projects."},{"name":"Cards","description":"Manage project cards (kanban entries) — create, move, comment, complete, attach files."},{"name":"Tags","description":"Workspace-scoped labels shared by tickets and project cards. Discover the vocabulary here, then filter with `?tag=` or write with the `tags`/`addTags`/`removeTags` fields."},{"name":"Notes","description":"Project notes (Google-Keep-style) shared across a project's members."},{"name":"Companies","description":"Manage company records — list, create, update, and link contacts."},{"name":"Contacts","description":"Manage individual contact records."},{"name":"Knowledge Base","description":"Agent-only API for KB articles. Server-side keys only."},{"name":"Widget","description":"Configuration endpoint for the embeddable widget."},{"name":"MCP","description":"Model Context Protocol surface for AI agents (Claude Code, Codex CLI, Cortex, custom). JSON-RPC 2.0 over HTTP, with OAuth 2.1 (Clerk) or `idk_mcp_*` API key auth. Full tool / skill catalog and client setup snippets are in the in-app docs at Settings → Developer."},{"name":"Timesheet","description":"Per-agent time tracking — entries, timers, submissions, XLSX export."},{"name":"Time Categories","description":"Workspace-level categories for non-ticket/card time entries."}],"externalDocs":{"description":"Developer overview","url":"https://intellidesk.co/developers"}}