{"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. An **All desks key** (Developer settings → Help desks, managers only) is the exception: it has no desk, can only call the `/notifications` endpoints (`403` everywhere else), and each record names its desk by code. Each server-side key also has an **access level**: **Full** (everything), **Triage** (read and update tickets, internal notes, files and new tickets, but not public replies or knowledge-base writes) or **Read-only** (GET endpoints only). A call outside the key's level returns `403`."}},"schemas":{"NotificationRecord":{"type":"object","description":"One requested email (POST /notifications/batches, or POST /tickets/outbound with an externalRef).","properties":{"externalRef":{"type":"string","example":"AWB-779012345678-DUTIES"},"status":{"type":"string","enum":["queued","sent","failed","duplicate"],"description":"queued: waiting to send. sent: handed to the desk's mailbox. failed: see `error`. duplicate: the reference was already queued or sent, so nothing was sent."},"error":{"type":["string","null"],"example":"The mailbox couldn't send it: 550 mailbox unavailable"},"to":{"type":"string","format":"email"},"name":{"type":["string","null"]},"desk":{"type":["string","null"],"description":"Desk code.","example":"KHI"},"template":{"type":["string","null"],"example":"duties_payable"},"ticket":{"type":["object","null"],"properties":{"id":{"type":"string"},"publicId":{"type":["string","null"],"example":"HD-1042"}}},"duplicateOf":{"type":["object","null"],"description":"For a duplicate: the original record's status and ticket.","properties":{"status":{"type":"string"},"ticketPublicId":{"type":["string","null"]}}},"batchId":{"type":["string","null"]},"createdAt":{"type":"integer","description":"Unix ms."},"sentAt":{"type":["integer","null"],"description":"Unix ms."}}},"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","in_project","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."},"startedBy":{"type":"string","enum":["team","customer"],"description":"Who started the ticket. `team`: an agent emailed the customer first (POST /tickets/outbound, New email in the app, or a team member's email with the desk copied). `customer`: everything else."},"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},"linkedCard":{"type":"object","readOnly":true,"nullable":true,"description":"The project card this ticket was handed to by `POST /tickets/{id}/move-to-project`, or `null`. A ticket can only be on one board, so a non-null value means another move will be refused with 409.","properties":{"cardId":{"type":"string"},"cardPublicId":{"type":"string","example":"PM-204"},"title":{"type":"string"},"stage":{"type":"string","enum":["todo","in_progress","done"]},"status":{"type":"string","nullable":true,"description":"Sub-status slug. Fixed when the status is created; renaming it doesn't change it."},"statusLabel":{"type":"string","nullable":true,"description":"The sub-status name the board shows now."},"projectId":{"type":"string"},"projectName":{"type":"string","nullable":true},"projectArchived":{"type":"boolean"}}},"references":{"type":"array","readOnly":true,"description":"Reference numbers found on the ticket (Settings → Reference Numbers), on `GET /tickets/{id}` only. Present only in workspaces that use reference numbers.","items":{"type":"object","properties":{"type":{"type":"string","example":"fedex_tracking_awb"},"label":{"type":"string","example":"FedEx tracking (AWB)"},"value":{"type":"string","example":"878917947013","description":"Normalized: no spaces or dashes, upper case."},"link":{"type":"string","nullable":true,"example":"https://www.fedex.com/fedextrack/?trknbr=878917947013"}}}}},"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","in_project","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":"startedBy","in":"query","schema":{"type":"string","enum":["team","customer"]},"description":"`team`: only tickets the team started by emailing the customer first (proactive emails). `customer`: only the rest."},{"name":"reference","in":"query","schema":{"type":"string","maxLength":60},"description":"Tickets carrying this reference number (a tracking, account or order number set up under Settings → Reference Numbers), in any spacing: `8789 1794 7013` and `878917947013` match the same tickets. Returned in one page (no cursor); the other filters still apply. Matches nothing in a workspace without reference numbers.","example":"878917947013"},{"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/outbound":{"post":{"operationId":"sendNewEmail","summary":"Email a customer first (proactive email)","description":"Sends an email to a customer from the API key's desk mailbox and starts a ticket for them with it, like **New email** in the app. The email is the ticket's first message; the ticket is Pending (waiting on the customer) and their reply threads back into it. The subject goes out as `<subject> [HD-1234]`. A key bound to an agent sends as that agent, assigns the ticket to them and counts it as their proactive email in reports; otherwise it's sent in the desk's name and left unassigned. Needs a full-access key (it emails a customer) and a desk with a connected mailbox.\n\n**Templates:** pass `templateKey` and `variables` instead of `subject` and `body` to send one of your sending templates (Settings → Templates → Proactive sending); missing variables return `422` with a `missing` list, and the email ends with the bound agent's signature or the desk's name. **No double sends:** pass `externalRef` (or an `Idempotency-Key` header). Repeating it returns the original ticket with `200` and `duplicate: true`, and sends nothing. For many emails at once use POST /notifications/batches.","tags":["Tickets"],"parameters":[{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string","maxLength":200},"description":"Same as `externalRef` (the body field wins)."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["customerEmail"],"description":"Either `subject` + `body`, or `templateKey` (+ `variables`).","properties":{"templateKey":{"type":"string","description":"A sending template's key. Leave out subject and body.","example":"held_at_customs"},"variables":{"type":"object","additionalProperties":{"type":["string","number","boolean","null"]},"description":"The template's own values.","example":{"awb":"879012345678"}},"externalRef":{"type":"string","maxLength":200,"description":"Your unique reference; a repeat sends nothing and returns the original ticket.","example":"AWB-879012345678-CUSTOMS"},"customerEmail":{"type":"string","format":"email","description":"The customer. Matched to an existing contact, or added as a new one.","example":"ship@example.com"},"customerName":{"type":"string","description":"Used when the contact is new. Defaults to the part of the address before the @.","example":"Bold Eyes Exports"},"subject":{"type":"string","example":"Held at customs, AWB 8790 1234 5678"},"body":{"type":"string","description":"The email, as plain text or HTML.","example":"Your shipment is held at customs. Please send the commercial invoice."},"cc":{"type":"array","maxItems":20,"items":{"type":"string","format":"email"},"description":"People to copy in."},"priority":{"type":"string","enum":["low","normal","high","urgent"]},"ticketType":{"type":"string","enum":["question","incident","problem","task","change_request"]},"attachments":{"type":"array","maxItems":10,"description":"Files uploaded first via POST /upload.","items":{"type":"object","required":["storageId","filename","mimeType","size"],"properties":{"storageId":{"type":"string"},"filename":{"type":"string","example":"invoice-request.pdf"},"mimeType":{"type":"string","example":"application/pdf"},"size":{"type":"integer","example":48720}}}}}}}}},"responses":{"200":{"description":"This `externalRef` was already sent (or is on its way): the original ticket, and nothing new was sent.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":["string","null"]},"publicId":{"type":["string","null"]},"startedBy":{"type":"string","enum":["team"]},"externalRef":{"type":"string"},"duplicate":{"type":"boolean","enum":[true]},"status":{"type":"string","enum":["queued","sent","failed"],"description":"The original's status."}}}}}},"201":{"description":"Email queued and ticket started.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"Internal ticket ID.","example":"k97abc123def456"},"publicId":{"type":"string","example":"HD-1042"},"startedBy":{"type":"string","enum":["team"]},"externalRef":{"type":["string","null"],"description":"Only when a reference was given."},"duplicate":{"type":"boolean","description":"Only when a reference was given. Always false on 201."}},"required":["id","publicId","startedBy"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"422":{"$ref":"#/components/responses/ValidationError"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/notifications/batches":{"post":{"operationId":"sendNotificationBatch","summary":"Send a template to many customers","description":"Sends one of your sending templates (a template with a **key** and a **subject**, Settings → Templates → Proactive sending) to up to **500** recipients, one email and one ticket each, with each record's own values (`{{awb}}`, `{{exception_type}}`, …). Built-in placeholders such as `{{contact_first_name}}` and `{{ticket_id}}` are filled in by IntelliDesk.\n\n**Every record needs its own `externalRef`**, unique in your workspace. A reference that is already queued or sent is stored as `duplicate` and **nothing is sent**, so retrying a call or re-running a file never emails anyone twice. A reference that only ever failed can be sent again.\n\n**Which desk:** with a desk's API key, records send from that desk (`desk` may be left out, or name that desk's code only). With an **All desks key** (Developer settings → Help desks), every record must name a desk by its **code** (`KHI`, set on the desk's settings page).\n\nEvery row is decided straight away: `queued`, `duplicate`, or `failed` with the reason (bad address, unknown desk code, unknown template, missing variables, …); only the request's shape can reject the whole call. Queued rows are sent in the background, at most the desk's **bulk limit per minute** per mailbox (30 by default, changeable on the desk's settings page). A queued row becomes `sent` once it's handed to the desk's mailbox, or `failed` with the mail server's error if the mailbox refuses it.\n\nEmails end with the key's bound agent's signature when they have one, otherwise the desk's name. Needs a full-access key.","tags":["Notifications"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["records"],"properties":{"template":{"type":"string","description":"Template key used for every record that doesn't name its own.","example":"duties_payable"},"records":{"type":"array","minItems":1,"maxItems":500,"items":{"type":"object","required":["externalRef","to"],"properties":{"externalRef":{"type":"string","maxLength":200,"description":"Your unique reference for this email (text or a number).","example":"AWB-779012345678-DUTIES"},"to":{"type":"string","format":"email","example":"ship@example.com"},"name":{"type":"string","description":"Used when the contact is new, and for {{contact_first_name}}.","example":"Ayesha Khan"},"desk":{"type":"string","description":"Desk code. Required with an All desks key.","example":"KHI"},"template":{"type":"string","description":"Template key for this record.","example":"duties_payable"},"variables":{"type":"object","additionalProperties":{"type":["string","number","boolean","null"]},"description":"The template's own values. Every variable the template uses must have a non-blank value.","example":{"awb":"779012345678","exception_type":"Duties payable"}}}}}}}}}},"responses":{"202":{"description":"Batch stored; queued records are being sent.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"Batch ID.","example":"kd7abc123def456"},"status":{"type":"string","enum":["processing","done"]},"total":{"type":"integer","example":3},"counts":{"type":"object","properties":{"queued":{"type":"integer"},"sent":{"type":"integer"},"failed":{"type":"integer"},"duplicate":{"type":"integer"}},"example":{"queued":2,"sent":0,"failed":1,"duplicate":0}},"records":{"type":"array","description":"Each row's outcome, in request order.","items":{"type":"object","properties":{"index":{"type":"integer"},"externalRef":{"type":"string"},"status":{"type":"string","enum":["queued","sent","failed","duplicate"]},"error":{"type":["string","null"],"example":"Missing variable: exception_type"}}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"422":{"$ref":"#/components/responses/ValidationError"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/notifications/batches/{id}":{"get":{"operationId":"getNotificationBatch","summary":"Get a batch and its records","description":"The batch's status and counts, and a page of its records with each one's status (`queued`, `sent` with its ticket, `failed` with the reason, `duplicate`). Follow `nextCursor` for the next page. A desk key sees only its own desk's records.","tags":["Notifications"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Batch ID from POST /notifications/batches."},{"name":"limit","in":"query","schema":{"type":"integer","default":50,"maximum":100}},{"name":"cursor","in":"query","schema":{"type":"string"},"description":"`nextCursor` from the previous page."}],"responses":{"200":{"description":"The batch and a page of records.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"status":{"type":"string","enum":["processing","done"]},"source":{"type":"string","enum":["batch","upload","api"]},"template":{"type":["string","null"]},"total":{"type":"integer"},"counts":{"type":"object"},"createdAt":{"type":"integer","description":"Unix ms."},"finishedAt":{"type":["integer","null"]},"records":{"type":"array","items":{"$ref":"#/components/schemas/NotificationRecord"}},"nextCursor":{"type":["string","null"]}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/notifications/records/{externalRef}":{"get":{"operationId":"getNotificationRecord","summary":"Get a record by your reference","description":"Where one notification stands, by your own `externalRef` (URL-encode it). Returns the record that holds the reference: the sent or queued one, or the latest failed one. `attempts` counts every record stored under it, retries and duplicates included.","tags":["Notifications"],"parameters":[{"name":"externalRef","in":"path","required":true,"schema":{"type":"string"},"example":"AWB-779012345678-DUTIES"}],"responses":{"200":{"description":"The record.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/NotificationRecord"},{"type":"object","properties":{"attempts":{"type":"integer","example":1}}}]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/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","in_project","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}/move-to-project":{"post":{"operationId":"moveTicketToProject","summary":"Move a ticket onto a project board","description":"Hands a ticket to a project team: creates a card on the board linked to the ticket, copies the ticket's tags, seeds the card description with the requester and the original request, and sets the ticket status to `in_project` so it stops counting as waiting on the customer. A card created straight into `done` leaves the ticket's status alone.\n\nUnlike the same action in the dashboard, this does **not** email the customer — an automated move shouldn't mail someone as a side effect. Post a public reply if they should be told.\n\nReturns 409 if the ticket is already linked to a card; `GET /tickets/{id}` returns `linkedCard` so that case is checkable up front.","tags":["Tickets"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["projectId"],"properties":{"projectId":{"type":"string","description":"Target project id."},"title":{"type":"string","description":"Card title. Defaults to the ticket subject."},"priority":{"type":"string","enum":["low","normal","high","urgent"],"description":"Card priority. Defaults to the ticket's priority."},"stage":{"type":"string","enum":["todo","in_progress","done"],"description":"Initial stage. Default: todo."},"status":{"type":"string","description":"Status slug from the project workflow. Overrides stage when the slug belongs to a different one."},"memberUserIds":{"type":"array","items":{"type":"string"},"description":"Initial card members (Clerk user ids). Ids that aren't project members are ignored."}}}}}},"responses":{"201":{"description":"Card created and ticket moved.","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Widget keys cannot move tickets."},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"The ticket is already linked to a project card."},"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"}}}},"/help-desks":{"get":{"operationId":"listHelpDesks","summary":"Get the key's help desk","description":"The help desk this API key is bound to, with live ticket counters. Keys are desk-scoped, so the list holds at most one desk — it is empty when the desk has been deactivated.","tags":["Workspace"],"responses":{"200":{"description":"The key's help desk.","content":{"application/json":{"schema":{"type":"object","required":["helpDesks"],"properties":{"helpDesks":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"Internal help desk document ID."},"name":{"type":"string","example":"Support"},"slug":{"type":"string","example":"support"},"description":{"type":"string"},"emailAddress":{"type":"string","nullable":true,"description":"The desk's inbound email address, if one is set.","example":"support@example.com"},"openCount":{"type":"integer","description":"Tickets in `open` status."},"customerRespondedCount":{"type":"integer","description":"Tickets where the customer replied last."},"pendingCount":{"type":"integer","description":"Tickets in `pending` status."},"onHoldCount":{"type":"integer","description":"Tickets in `on_hold` status."},"inProjectCount":{"type":"integer","description":"Tickets in `in_project` status (handed to a project card)."},"unassignedActive":{"type":"integer","description":"Unclosed tickets with no assignee."}}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Widget and MCP keys cannot read help desks."},"500":{"$ref":"#/components/responses/InternalError"}}}},"/team-members":{"get":{"operationId":"listTeamMembers","summary":"List the help desk's team members","description":"Members of the key's help desk, sorted by name. `userId` is the value the `assignee` field on ticket writes accepts.","tags":["Workspace"],"responses":{"200":{"description":"Team members.","content":{"application/json":{"schema":{"type":"object","required":["members"],"properties":{"members":{"type":"array","items":{"type":"object","properties":{"userId":{"type":"string","example":"user_2abc123"},"name":{"type":"string","example":"Jane Agent"},"email":{"type":"string","nullable":true,"example":"jane@example.com"},"role":{"type":"string","enum":["manager","developer","agent","observer"]},"lastActiveAt":{"type":"integer","nullable":true,"description":"Unix ms of the member's last activity."}}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Widget and MCP keys cannot read team members."},"500":{"$ref":"#/components/responses/InternalError"}}}},"/canned-responses":{"get":{"operationId":"listCannedResponses","summary":"List canned responses","description":"Canned responses visible to the key's help desk: workspace-wide ones plus the desk's own, sorted by title. Personal responses are included only when the key is bound to the agent who wrote them.","tags":["Workspace"],"parameters":[{"name":"search","in":"query","schema":{"type":"string","maxLength":200},"description":"Case-insensitive substring match on the title."}],"responses":{"200":{"description":"Canned responses.","content":{"application/json":{"schema":{"type":"object","required":["cannedResponses"],"properties":{"cannedResponses":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"title":{"type":"string","example":"Refund policy"},"body":{"type":"string","description":"The response text."},"shortcut":{"type":"string","nullable":true,"example":"refund"},"helpDeskId":{"type":"string","nullable":true,"description":"The desk it belongs to, or `null` when workspace-wide."},"visibility":{"type":"string","enum":["shared","personal"]},"createdAt":{"type":"integer","description":"Unix ms."}}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Widget and MCP keys cannot read canned responses."},"422":{"$ref":"#/components/responses/ValidationError"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/incidents":{"get":{"operationId":"listIncidents","summary":"List status-page incidents","description":"Status-page incidents for the workspace, most recently started first, at most 50. Incidents are workspace-wide rather than desk-scoped, and include private (non-public) ones.","tags":["Workspace"],"parameters":[{"name":"status","in":"query","schema":{"type":"string","enum":["active","resolved","all"],"default":"active"},"description":"`active` returns every unresolved incident; `resolved` only resolved ones; `all` both."}],"responses":{"200":{"description":"Incidents.","content":{"application/json":{"schema":{"type":"object","required":["incidents"],"properties":{"incidents":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"title":{"type":"string","example":"Email delivery delayed"},"body":{"type":"string"},"severity":{"type":"string","enum":["maintenance","minor","major","outage"]},"state":{"type":"string","enum":["investigating","identified","monitoring","resolved"]},"isPublic":{"type":"boolean","description":"Shown on the public status page."},"affectedComponentIds":{"type":"array","items":{"type":"string"}},"startedAt":{"type":"integer","description":"Unix ms."},"resolvedAt":{"type":"integer","nullable":true,"description":"Unix ms."},"lastTouchedAt":{"type":"integer","description":"Unix ms of the last update."}}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Widget and MCP keys cannot read incidents."},"422":{"$ref":"#/components/responses/ValidationError"},"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 name), or `?tag=`. Each card carries its `tags`, its `status` slug and `statusLabel`, the name the board shows. A slug is fixed when the status is created, so a renamed status keeps its old slug. Wherever a sub-status is sent, the slug or the name (any case) is accepted; an exact slug wins over a name.","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":"Sub-status slug, or its name (any case). An exact slug wins over a name."},"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. `status` takes the sub-status slug or its name (any case); an exact slug wins over a name. `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":"Notifications","description":"Bulk proactive email: send a template to many customers at once, each with their own values and reference, and track every record (queued, sent, failed with the reason, duplicate). A reference is never sent twice."},{"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":"Workspace","description":"Read-only reference data: the key's help desk and its team, canned responses, and status-page incidents."},{"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"}}