{ "openapi": "3.1.0", "info": { "title": "Thicket", "version": "2026-08-21", "description": "Thicket's versioned REST API. The web app runs on the same routes, so everything the product does is here. Human-readable reference: https://www.thickethq.com/developers/api", "contact": { "name": "Thicket support", "email": "support@thickethq.com" }, "license": { "name": "MIT", "identifier": "MIT" } }, "servers": [ { "url": "https://www.thickethq.com" } ], "security": [ { "personalAccessToken": [] } ], "tags": [ { "name": "Authorization" }, { "name": "Organizations" }, { "name": "Projects" }, { "name": "Recordings" }, { "name": "Check-ins" }, { "name": "Clients" }, { "name": "Progress & health" }, { "name": "Chat" }, { "name": "Calendar" }, { "name": "Files" }, { "name": "People" }, { "name": "Templates" }, { "name": "Notifications" }, { "name": "Search & reports" }, { "name": "Trash & admin" }, { "name": "Billing" }, { "name": "Me" } ], "paths": { "/api/v1/{orgSlug}/account": { "get": { "operationId": "getAccount", "tags": [ "Organizations" ], "summary": "The account document", "description": "Identity, owners, plan limits, can-do flags, and settings for the caller's organization (any member). `logo` is omitted when unset.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" } ], "responses": { "200": { "description": "The account", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/OrgAccount" } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "404": { "$ref": "#/components/responses/NotFound" } } }, "patch": { "operationId": "renameAccount", "tags": [ "Organizations" ], "summary": "Rename the account", "description": "Owner only.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "name" ], "properties": { "name": { "type": "string", "minLength": 1, "maxLength": 200 } } } } } }, "responses": { "200": { "description": "Renamed", "content": { "application/json": { "schema": { "type": "object", "required": [ "ok" ], "properties": { "ok": { "type": "boolean" } } } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "403": { "$ref": "#/components/responses/Forbidden" }, "404": { "$ref": "#/components/responses/NotFound" }, "422": { "$ref": "#/components/responses/Validation" } } }, "delete": { "operationId": "cancelAccount", "tags": [ "Organizations" ], "summary": "Cancel the account", "description": "Owner only, effective immediately: the org turns invisible everywhere, any Stripe subscription is cancelled, and every owner gets a confirmation email. Data is retained for a 30-day restore window (POST /api/v1/orgs/{orgId}/restore); after that the purge task hard-deletes rows and storage.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" } ], "responses": { "200": { "description": "Cancelled", "content": { "application/json": { "schema": { "type": "object", "required": [ "ok" ], "properties": { "ok": { "type": "boolean" } } } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "403": { "$ref": "#/components/responses/Forbidden" }, "404": { "$ref": "#/components/responses/NotFound" } } } }, "/api/v1/{orgSlug}/account/export": { "get": { "operationId": "exportAccount", "tags": [ "Organizations" ], "summary": "Export the account as JSON", "description": "Owner only; the response is a JSON attachment. `?projects=id,id` limits content to those projects (people, companies, and groups always ride along). Private chats and personal notes are never included; use /account/export/chats for transcripts.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" }, { "name": "projects", "in": "query", "schema": { "type": "string" }, "description": "Comma-separated project ids to limit the content" } ], "responses": { "200": { "description": "The export document (Content-Disposition: attachment)", "content": { "application/json": { "schema": { "type": "object", "additionalProperties": true } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "403": { "$ref": "#/components/responses/Forbidden" }, "404": { "$ref": "#/components/responses/NotFound" }, "422": { "$ref": "#/components/responses/Validation" } } } }, "/api/v1/{orgSlug}/account/export/chats": { "get": { "operationId": "exportPrivateChats", "tags": [ "Organizations" ], "summary": "Export private-chat transcripts", "description": "Owner only; deliberately split from the account export because of the privacy weight it carries. Transcripts (participants plus author, timestamp, and content lines) for the chosen conversations, as a JSON attachment.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" }, { "name": "ids", "in": "query", "required": true, "schema": { "type": "string" }, "description": "Comma-separated DM recording ids, at least one" } ], "responses": { "200": { "description": "The transcripts document (Content-Disposition: attachment)", "content": { "application/json": { "schema": { "type": "object", "additionalProperties": true } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "403": { "$ref": "#/components/responses/Forbidden" }, "404": { "$ref": "#/components/responses/NotFound" }, "422": { "$ref": "#/components/responses/Validation" } } } }, "/api/v1/{orgSlug}/account/logo": { "put": { "operationId": "uploadAccountLogo", "tags": [ "Organizations" ], "summary": "Upload or replace the account logo", "description": "Admin and up. Multipart upload, `logo` field, any decodable image up to 5 MB; the server re-encodes it to a square webp.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" } ], "requestBody": { "required": true, "content": { "multipart/form-data": { "schema": { "type": "object", "required": [ "logo" ], "properties": { "logo": { "type": "string", "format": "binary", "description": "Any decodable image, 5 MB max" } } } } } }, "responses": { "204": { "description": "Stored" }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "403": { "$ref": "#/components/responses/Forbidden" }, "404": { "$ref": "#/components/responses/NotFound" }, "422": { "$ref": "#/components/responses/Validation" } } }, "delete": { "operationId": "deleteAccountLogo", "tags": [ "Organizations" ], "summary": "Remove the account logo", "description": "Admin and up.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" } ], "responses": { "204": { "description": "Removed" }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "403": { "$ref": "#/components/responses/Forbidden" }, "404": { "$ref": "#/components/responses/NotFound" } } } }, "/api/v1/{orgSlug}/account/storage": { "get": { "operationId": "getStorageUsage", "tags": [ "Organizations" ], "summary": "Storage usage vs the plan limit", "description": "Total usage, the plan's limit, and the largest files. Private-chat and unposted-draft uploads count toward usage but are never listed.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" } ], "responses": { "200": { "description": "Usage and the largest files", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/StorageUsage" } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "404": { "$ref": "#/components/responses/NotFound" } } } }, "/api/v1/{orgSlug}/activity": { "get": { "operationId": "listActivity", "tags": [ "Search & reports" ], "summary": "The account-wide activity timeline", "description": "Everything that happened across your accessible projects, newest first, ready to group by day client-side. Same visibility rules as the Latest-activity page: trashed content and non-client-visible recordings filtered, chat and DM lines never logged. Filters: `project_id`, `person_id` (the actor), `starred=1` (your starred projects), `since` (ISO datetime), `q` (matches title, actor name, or content text).", "parameters": [ { "$ref": "#/components/parameters/orgSlug" }, { "$ref": "#/components/parameters/page" }, { "$ref": "#/components/parameters/perPage" }, { "name": "project_id", "in": "query", "schema": { "type": "string", "format": "uuid" } }, { "name": "person_id", "in": "query", "schema": { "type": "string", "format": "uuid" }, "description": "Only this actor's events" }, { "name": "starred", "in": "query", "schema": { "type": "string", "enum": [ "1" ] }, "description": "Pass 1 to limit to your starred projects" }, { "name": "since", "in": "query", "schema": { "type": "string", "format": "date-time" }, "description": "Only events after this instant" }, { "name": "q", "in": "query", "schema": { "type": "string" }, "description": "Keyword filter: title, actor name, or content text" } ], "responses": { "200": { "description": "Activity events, newest first (default page size 50)", "content": { "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/ActivityEvent" } } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "422": { "$ref": "#/components/responses/Validation" } } } }, "/api/v1/{orgSlug}/billing": { "get": { "operationId": "getBilling", "tags": [ "Billing" ], "summary": "Current plan, status, and usage", "description": "Tier, subscription status, trial and period dates, plus usage against the plan's limits. In `limits`, -1 means unlimited; one `people` pool covers members and guests alike, clients included.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" } ], "responses": { "200": { "description": "The billing document", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/BillingInfo" } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "404": { "$ref": "#/components/responses/NotFound" } } } }, "/api/v1/{orgSlug}/billing/change-plan": { "post": { "operationId": "changePlan", "tags": [ "Billing" ], "summary": "Switch plans on the live subscription", "description": "Owner only. Swaps the price on the existing Stripe subscription with prorations, never a second subscription, and syncs the local row in-request so the change reads back immediately. 404 when there is no live subscription; 422 when the account is already on that plan. A downgrade below the org's active-project count is allowed: the org lands over-limit and the home chooser archives projects down, restorably.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "plan" ], "properties": { "plan": { "type": "string", "enum": [ "starter", "pro" ] }, "billing_interval": { "type": "string", "enum": [ "monthly", "yearly" ], "default": "monthly" } } } } } }, "responses": { "200": { "description": "The synced plan", "content": { "application/json": { "schema": { "type": "object", "required": [ "tier", "billing_interval", "status" ], "properties": { "tier": { "type": "string", "enum": [ "free", "starter", "pro", "comped" ] }, "billing_interval": { "type": [ "string", "null" ], "enum": [ "monthly", "yearly", null ] }, "status": { "type": "string", "enum": [ "active", "trialing", "past_due", "cancelled", "pending", "comped", "lapsed" ] } } } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "403": { "$ref": "#/components/responses/Forbidden" }, "404": { "$ref": "#/components/responses/NotFound" }, "422": { "$ref": "#/components/responses/Validation" } } } }, "/api/v1/{orgSlug}/billing/create-checkout": { "post": { "operationId": "createCheckout", "tags": [ "Billing" ], "summary": "Create a Stripe Checkout session", "description": "Owner only. Verifies the price (from GET /api/billing/prices) and returns a Checkout URL to redirect the buyer to. The signup trial is the only trial an org ever gets: a session created mid-trial carries a trial_end aligned to the existing end date, never restarted, and every other org is charged at checkout. Under 48 hours of trial left checks out as an immediate charge.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "price_id" ], "properties": { "price_id": { "type": "string", "minLength": 1 }, "billing_interval": { "type": "string", "enum": [ "monthly", "yearly" ], "default": "monthly" } } } } } }, "responses": { "200": { "description": "The Checkout session", "content": { "application/json": { "schema": { "type": "object", "required": [ "url" ], "properties": { "url": { "type": [ "string", "null" ], "format": "uri", "description": "Redirect the buyer here" } } } } } }, "400": { "description": "Unknown, inactive, or non-Thicket price (code bad_request)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "403": { "$ref": "#/components/responses/Forbidden" }, "404": { "$ref": "#/components/responses/NotFound" }, "422": { "$ref": "#/components/responses/Validation" } } } }, "/api/v1/{orgSlug}/billing/create-portal": { "post": { "operationId": "createBillingPortal", "tags": [ "Billing" ], "summary": "Create a Stripe customer portal session", "description": "Owner only: payment method, invoices, and cancellation. The optional `{\"flow\": \"subscription_cancel\"}` body deep-links the portal's cancel-subscription confirmation (404 without a subscription). Plan switching is not portal work; it rides /billing/change-plan.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" } ], "requestBody": { "required": false, "content": { "application/json": { "schema": { "type": "object", "properties": { "flow": { "type": "string", "enum": [ "subscription_cancel" ], "description": "Deep-link the portal's cancel-subscription confirmation" } } } } } }, "responses": { "200": { "description": "The portal session", "content": { "application/json": { "schema": { "type": "object", "required": [ "url" ], "properties": { "url": { "type": "string", "format": "uri", "description": "Redirect the owner here" } } } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "403": { "$ref": "#/components/responses/Forbidden" }, "404": { "$ref": "#/components/responses/NotFound" }, "422": { "$ref": "#/components/responses/Validation" } } } }, "/api/v1/{orgSlug}/bulletins": { "get": { "operationId": "listBulletins", "tags": [ "Notifications" ], "summary": "The announcements archive", "description": "Published platform announcements, newest first: global content read through an org-scoped URL, personalized per viewer ({{first_name}} interpolated, excerpts capped at 200 chars).", "parameters": [ { "$ref": "#/components/parameters/orgSlug" } ], "responses": { "200": { "description": "Published announcements, newest first", "content": { "application/json": { "schema": { "type": "object", "required": [ "bulletins" ], "properties": { "bulletins": { "type": "array", "items": { "$ref": "#/components/schemas/BulletinSummary" } } } } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "404": { "$ref": "#/components/responses/NotFound" } } } }, "/api/v1/{orgSlug}/bulletins/{bulletinId}": { "get": { "operationId": "getBulletin", "tags": [ "Notifications" ], "summary": "One announcement", "description": "The way to open an `action: \"announced\"` notification row (those carry a bulletin_id instead of a recording_id). Published bulletins read for anyone in the org; a draft reads only for someone holding their own tray row for it (a test send), everyone else gets 404. Reading marks the caller's notification row read, same as opening the page.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" }, { "name": "bulletinId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "The announcement, personalized for the caller", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Bulletin" } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "404": { "$ref": "#/components/responses/NotFound" } } } }, "/api/v1/{orgSlug}/calendar": { "get": { "operationId": "listCalendarItems", "tags": [ "Calendar" ], "summary": "The global calendar window", "description": "Events (and optionally due tasks) across every accessible project's calendar plus the account calendar. Recurring events expand at read time: each occurrence arrives as its own row carrying `occurrence_key`, the anchor-zone day key the occurrences DELETE endpoint speaks. `from`/`to` accept ISO 8601 with an explicit offset, or a zoneless wall clock interpreted in the caller's resolved zone (their /me timezone preference, then the tz cookie, then UTC).", "parameters": [ { "$ref": "#/components/parameters/orgSlug" }, { "name": "from", "in": "query", "required": true, "schema": { "type": "string", "format": "date-time" }, "description": "Window start; offset or zoneless wall clock" }, { "name": "to", "in": "query", "required": true, "schema": { "type": "string", "format": "date-time" }, "description": "Window end" }, { "name": "just", "in": "query", "schema": { "type": "string", "enum": [ "me", "everyone" ], "default": "everyone" }, "description": "`me`: only items you created or are invited to" }, { "name": "tasks", "in": "query", "schema": { "type": "boolean", "default": false }, "description": "Include due to-dos and cards (per-request view state, not persisted)" }, { "name": "projects", "in": "query", "schema": { "type": "string" }, "description": "Comma-separated project ids to limit to" }, { "name": "starred", "in": "query", "schema": { "type": "boolean", "default": false }, "description": "Limit to your starred projects" } ], "responses": { "200": { "description": "Items in the window", "content": { "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/CalendarOccurrence" } } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "422": { "$ref": "#/components/responses/Validation" } } }, "post": { "operationId": "createCalendarEvent", "tags": [ "Calendar" ], "summary": "Create an event", "description": "Invitees (`participant_ids`) are notified. `project_id` null or omitted targets the account calendar: org-level events with no project, non-clients only. Datetimes accept an explicit offset or a zoneless wall clock resolved in the caller's zone. All-day events are floating dates, encoded T00:00:00Z to T23:59:59Z by convention.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "title", "starts_at" ], "properties": { "project_id": { "type": [ "string", "null" ], "format": "uuid", "description": "null or omitted: the account calendar" }, "title": { "type": "string", "minLength": 1, "maxLength": 10000 }, "content": { "type": "string", "maxLength": 10000, "description": "Plain-text notes, converted to HTML" }, "content_html": { "type": [ "string", "null" ], "maxLength": 100000, "description": "Rich notes (sanitized); wins over `content` when both are sent" }, "starts_at": { "type": "string", "format": "date-time" }, "ends_at": { "type": [ "string", "null" ], "format": "date-time" }, "all_day": { "type": "boolean", "default": false }, "link": { "type": [ "string", "null" ], "format": "uri", "description": "A join link (video call etc.); reminders carry it" }, "circled": { "type": "boolean", "default": false }, "recurrence": { "oneOf": [ { "$ref": "#/components/schemas/CalendarRecurrence" }, { "type": "null" } ] }, "participant_ids": { "type": "array", "items": { "type": "string", "format": "uuid" }, "maxItems": 100, "description": "Membership ids to invite; they are notified" } } } } } }, "responses": { "201": { "description": "The created event", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Recording" } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "404": { "$ref": "#/components/responses/NotFound" }, "422": { "$ref": "#/components/responses/Validation" } } } }, "/api/v1/{orgSlug}/calendar/{eventId}": { "patch": { "operationId": "updateCalendarEvent", "tags": [ "Calendar" ], "summary": "Edit an event", "description": "Edits apply to the whole series for recurring events; invitees are notified of the change. Same fields as create, all optional. `recurrence: null` stops the event recurring; to drop a single occurrence use the occurrences DELETE endpoint instead.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" }, { "name": "eventId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "title": { "type": "string", "minLength": 1, "maxLength": 10000 }, "content": { "type": [ "string", "null" ], "maxLength": 10000, "description": "Plain-text notes, converted to HTML" }, "content_html": { "type": [ "string", "null" ], "maxLength": 100000, "description": "Rich notes (sanitized); wins over `content` when both are sent" }, "starts_at": { "type": "string", "format": "date-time" }, "ends_at": { "type": [ "string", "null" ], "format": "date-time" }, "all_day": { "type": "boolean" }, "link": { "type": [ "string", "null" ], "format": "uri" }, "circled": { "type": "boolean" }, "recurrence": { "oneOf": [ { "$ref": "#/components/schemas/CalendarRecurrence" }, { "type": "null" } ] }, "participant_ids": { "type": "array", "items": { "type": "string", "format": "uuid" }, "maxItems": 100, "description": "Replaces the invitee set" } } } } } }, "responses": { "200": { "description": "The updated event", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Recording" } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "404": { "$ref": "#/components/responses/NotFound" }, "422": { "$ref": "#/components/responses/Validation" } } } }, "/api/v1/{orgSlug}/calendar/{eventId}/ics": { "get": { "operationId": "downloadEventIcs", "tags": [ "Calendar" ], "summary": "One event as an .ics file", "description": "\"Add to my calendar\": the event as a credential-authenticated iCalendar download (Content-Disposition: attachment). Recurring events export a year of occurrences. For an ongoing subscription mint a feed instead.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" }, { "name": "eventId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "The iCalendar file", "content": { "text/calendar": { "schema": { "type": "string", "description": "An RFC 5545 VCALENDAR document" } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "404": { "$ref": "#/components/responses/NotFound" } } } }, "/api/v1/{orgSlug}/calendar/{eventId}/occurrences/{date}": { "delete": { "operationId": "deleteEventOccurrence", "tags": [ "Calendar" ], "summary": "Delete an occurrence of a recurring event", "description": "Basecamp's trash prompt for recurring events only. Default: drop just this occurrence (an exdate, `result: excluded`). `?mode=future`: end the series the day before (`result: truncated`); from the first occurrence that trashes the whole series (`result: trashed`). `date` is the occurrence's `occurrence_key`, the anchor-zone day key from the calendar listing; never derive it from `start` client-side.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" }, { "name": "eventId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } }, { "name": "date", "in": "path", "required": true, "schema": { "type": "string", "format": "date" }, "description": "The occurrence's anchor-zone day key (YYYY-MM-DD)" }, { "name": "mode", "in": "query", "schema": { "type": "string", "enum": [ "occurrence", "future" ], "default": "occurrence" }, "description": "`future`: also delete all later occurrences" } ], "responses": { "200": { "description": "What happened to the series", "content": { "application/json": { "schema": { "type": "object", "required": [ "result" ], "properties": { "result": { "type": "string", "enum": [ "excluded", "truncated", "trashed" ] } } } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "404": { "$ref": "#/components/responses/NotFound" } } } }, "/api/v1/{orgSlug}/calendar/feeds": { "get": { "operationId": "listCalendarFeeds", "tags": [ "Calendar" ], "summary": "List your iCal feeds", "description": "Your active (unrevoked) feeds. Each `url` is a capability URL: the token in the path is the only credential the public /feeds/{token}.ics endpoint checks, so treat it like a password. Revoked feeds return 404 there.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" } ], "responses": { "200": { "description": "Your feeds", "content": { "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/CalendarFeed" } } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" } } }, "post": { "operationId": "createCalendarFeed", "tags": [ "Calendar" ], "summary": "Mint an iCal feed", "description": "A token-authenticated read-only ICS feed honoring the filters, for subscribing an external calendar app. The returned `url` is the capability: anyone holding it can read the feed until you revoke it. Clients cannot mint feeds and receive 404.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "project_id": { "type": "string", "format": "uuid", "description": "Limit the feed to one project" }, "include_tasks": { "type": "boolean", "description": "Include due to-dos and cards" }, "just": { "type": "string", "enum": [ "me", "everyone" ] } } } } } }, "responses": { "201": { "description": "The feed's capability URL", "content": { "application/json": { "schema": { "type": "object", "required": [ "url" ], "properties": { "url": { "type": "string", "format": "uri", "description": "The public /feeds/{token}.ics URL; the token is the credential" } } } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "404": { "$ref": "#/components/responses/NotFound" }, "422": { "$ref": "#/components/responses/Validation" } } } }, "/api/v1/{orgSlug}/calendar/feeds/{feedId}": { "delete": { "operationId": "revokeCalendarFeed", "tags": [ "Calendar" ], "summary": "Revoke a feed", "description": "Effective immediately: the feed's public URL starts returning 404. Only your own feeds are revocable.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" }, { "name": "feedId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "Revoked", "content": { "application/json": { "schema": { "type": "object", "required": [ "ok" ], "properties": { "ok": { "type": "boolean", "const": true } } } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "404": { "$ref": "#/components/responses/NotFound" } } } }, "/api/v1/{orgSlug}/calendar/parse": { "get": { "operationId": "parseCalendarInput", "tags": [ "Calendar" ], "summary": "Parse natural-language event text", "description": "Prefill for the event form: \"Team call Friday at 10am\" becomes a title plus a start (and end, when a range was given). A suggestion the user confirms, never a silent commit. Relative dates and clock times mean the viewer's zone. When nothing date-like is found, the datetime fields come back null.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" }, { "name": "q", "in": "query", "required": true, "schema": { "type": "string", "maxLength": 10000 }, "description": "The text to parse" } ], "responses": { "200": { "description": "The parse suggestion", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CalendarParseResult" } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "422": { "$ref": "#/components/responses/Validation" } } } }, "/api/v1/{orgSlug}/categories": { "get": { "operationId": "listCategories", "tags": [ "Recordings" ], "summary": "List message-board categories", "description": "Defaults are seeded per organization.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" } ], "responses": { "200": { "description": "The categories", "content": { "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/MessageCategory" } } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "404": { "$ref": "#/components/responses/NotFound" } } }, "post": { "operationId": "createCategory", "tags": [ "Recordings" ], "summary": "Create a message-board category", "description": "Admin and up.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "name" ], "properties": { "name": { "type": "string", "minLength": 1, "maxLength": 100 }, "icon": { "type": "string", "maxLength": 8, "description": "An emoji" } } } } } }, "responses": { "201": { "description": "The new category", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/MessageCategory" } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "403": { "$ref": "#/components/responses/Forbidden" }, "404": { "$ref": "#/components/responses/NotFound" }, "422": { "$ref": "#/components/responses/Validation" } } } }, "/api/v1/{orgSlug}/categories/{categoryId}": { "patch": { "operationId": "updateCategory", "tags": [ "Recordings" ], "summary": "Update a category", "description": "Admin and up.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" }, { "name": "categoryId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "name": { "type": "string", "minLength": 1, "maxLength": 100 }, "icon": { "type": [ "string", "null" ], "maxLength": 8 } } } } } }, "responses": { "200": { "description": "The updated category", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/MessageCategory" } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "403": { "$ref": "#/components/responses/Forbidden" }, "404": { "$ref": "#/components/responses/NotFound" }, "422": { "$ref": "#/components/responses/Validation" } } }, "delete": { "operationId": "deleteCategory", "tags": [ "Recordings" ], "summary": "Delete a category", "description": "Admin and up; deleting clears the category from posts.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" }, { "name": "categoryId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "Deleted", "content": { "application/json": { "schema": { "type": "object", "required": [ "ok" ], "properties": { "ok": { "type": "boolean" } } } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "403": { "$ref": "#/components/responses/Forbidden" }, "404": { "$ref": "#/components/responses/NotFound" } } } }, "/api/v1/{orgSlug}/chats": { "get": { "operationId": "listChats", "tags": [ "Chat" ], "summary": "List your direct chats", "description": "Your conversations, latest activity first, with participants and a preview of the last line. Direct chats have no project, never appear in activity, and are visible only to their participants. `?status=archived` lists archived ones: readable (and searchable) forever, but no new lines post into them.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" }, { "name": "status", "in": "query", "schema": { "type": "string", "enum": [ "active", "archived" ], "default": "active" } } ], "responses": { "200": { "description": "Your conversations", "content": { "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/DmSummary" } } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" } } }, "post": { "operationId": "findOrCreateChat", "tags": [ "Chat" ], "summary": "Find or start a conversation", "description": "Finds the existing conversation with exactly these people or starts one; either way the `dm` recording comes back with 201. Your own id alone starts a chat with just yourself. You can message admins, owners, and anyone you share a project with. Lines are chat children: POST /recordings/{chat_id}/children with type chat_message; archive via PUT /recordings/{id}/status/archived (for everyone, cannot be undone).", "parameters": [ { "$ref": "#/components/parameters/orgSlug" } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "membership_ids" ], "properties": { "membership_ids": { "type": "array", "items": { "type": "string", "format": "uuid" }, "minItems": 1, "maxItems": 20 } } } } } }, "responses": { "201": { "description": "The conversation (found or created)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Recording" } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "404": { "$ref": "#/components/responses/NotFound" }, "422": { "$ref": "#/components/responses/Validation" } } } }, "/api/v1/{orgSlug}/cheers/{cheerId}": { "delete": { "operationId": "removeCheer", "tags": [ "Recordings" ], "summary": "Remove a cheer", "description": "Only whoever left the cheer, or an org admin or owner (403 otherwise; 404 when already gone). Works for recording cheers and event cheers alike.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" }, { "name": "cheerId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "Removed", "content": { "application/json": { "schema": { "type": "object", "required": [ "ok" ], "properties": { "ok": { "type": "boolean" } } } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "403": { "$ref": "#/components/responses/Forbidden" }, "404": { "$ref": "#/components/responses/NotFound" } } } }, "/api/v1/{orgSlug}/companies": { "get": { "operationId": "listCompanies", "tags": [ "People" ], "summary": "List companies", "parameters": [ { "$ref": "#/components/parameters/orgSlug" } ], "responses": { "200": { "description": "The organization's companies", "content": { "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/Company" } } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "404": { "$ref": "#/components/responses/NotFound" } } }, "post": { "operationId": "createCompany", "tags": [ "People" ], "summary": "Create a company", "parameters": [ { "$ref": "#/components/parameters/orgSlug" } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "name" ], "properties": { "name": { "type": "string", "minLength": 1, "maxLength": 200 }, "is_client": { "type": "boolean" } } } } } }, "responses": { "201": { "description": "The new company", "content": { "application/json": { "schema": { "type": "object", "required": [ "id", "name", "is_client" ], "properties": { "id": { "type": "string", "format": "uuid" }, "name": { "type": "string" }, "is_client": { "type": "boolean" } } } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "403": { "$ref": "#/components/responses/Forbidden" }, "404": { "$ref": "#/components/responses/NotFound" }, "422": { "$ref": "#/components/responses/Validation" } } } }, "/api/v1/{orgSlug}/companies/{companyId}": { "patch": { "operationId": "updateCompany", "tags": [ "People" ], "summary": "Update a company", "description": "Rename, toggle client status, or toggle `dms_disabled` (the per-company private-chat restriction: members drop from chat pickers, can't start DMs, and existing conversations freeze read-only both ways).", "parameters": [ { "$ref": "#/components/parameters/orgSlug" }, { "name": "companyId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "name": { "type": "string", "minLength": 1, "maxLength": 200 }, "is_client": { "type": "boolean" }, "dms_disabled": { "type": "boolean" } } } } } }, "responses": { "200": { "description": "The updated company", "content": { "application/json": { "schema": { "type": "object", "required": [ "id", "name", "is_client", "dms_disabled" ], "properties": { "id": { "type": "string", "format": "uuid" }, "name": { "type": "string" }, "is_client": { "type": "boolean" }, "dms_disabled": { "type": "boolean" } } } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "403": { "$ref": "#/components/responses/Forbidden" }, "404": { "$ref": "#/components/responses/NotFound" }, "422": { "$ref": "#/components/responses/Validation" } } }, "delete": { "operationId": "deleteCompany", "tags": [ "People" ], "summary": "Delete a company", "description": "People keep their membership; they just lose the company association.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" }, { "name": "companyId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "Deleted", "content": { "application/json": { "schema": { "type": "object", "required": [ "ok" ], "properties": { "ok": { "type": "boolean" } } } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "403": { "$ref": "#/components/responses/Forbidden" }, "404": { "$ref": "#/components/responses/NotFound" } } } }, "/api/v1/{orgSlug}/embeds": { "post": { "operationId": "createEmbed", "tags": [ "Files" ], "summary": "Turn a URL into sanctioned embed markup", "description": "Server-generated embed markup for the allowlist: YouTube, Vimeo, Loom, Spotify, X, Flickr, Imgur, SoundCloud, and direct `.gif` URLs. Non-allowlisted URLs return `{html: null}`: keep a plain link. Client-supplied iframe HTML is never accepted anywhere; the returned markup also round-trips the rich-text sanitizer.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "url" ], "properties": { "url": { "type": "string", "format": "uri", "maxLength": 2000 } } } } } }, "responses": { "200": { "description": "The markup, or null for a non-allowlisted URL", "content": { "application/json": { "schema": { "type": "object", "required": [ "html" ], "properties": { "html": { "type": [ "string", "null" ], "description": "Sanitized figure/iframe markup to insert" } } } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "404": { "$ref": "#/components/responses/NotFound" }, "422": { "$ref": "#/components/responses/Validation" } } } }, "/api/v1/{orgSlug}/events/{eventId}/cheers": { "get": { "operationId": "listEventCheers", "tags": [ "Recordings" ], "summary": "List cheers on a change-log event", "description": "Cheers on one row of a recording's change log (Basecamp boosts events too). The per-recording events listing already inlines the same shape.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" }, { "name": "eventId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "The cheers", "content": { "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/EventCheer" } } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "404": { "$ref": "#/components/responses/NotFound" } } }, "post": { "operationId": "createEventCheer", "tags": [ "Recordings" ], "summary": "Cheer a change-log event", "description": "Up to 16 characters of text or emoji. Pings the event's actor through their `notify_cheers` channel, bundled into one notification per recording within 3 hours. An exact duplicate returns 200 `{already_cheered: true}` instead of 201.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" }, { "name": "eventId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "content" ], "properties": { "content": { "type": "string", "minLength": 1, "maxLength": 16 } } } } } }, "responses": { "200": { "description": "Already cheered with exactly this content", "content": { "application/json": { "schema": { "type": "object", "required": [ "already_cheered" ], "properties": { "already_cheered": { "type": "boolean" } } } } } }, "201": { "description": "The added cheer", "content": { "application/json": { "schema": { "type": "object", "required": [ "id", "content" ], "properties": { "id": { "type": "string", "format": "uuid" }, "content": { "type": "string" } } } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "404": { "$ref": "#/components/responses/NotFound" }, "422": { "$ref": "#/components/responses/Validation" } } } }, "/api/v1/{orgSlug}/folders": { "get": { "operationId": "listProjectFolders", "tags": [ "Files" ], "summary": "List project folders", "description": "Account-wide home-screen groupings of projects. Move a project in or out via PATCH /projects/{id} with `folder_id` (or null).", "parameters": [ { "$ref": "#/components/parameters/orgSlug" } ], "responses": { "200": { "description": "The folders", "content": { "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/ProjectFolder" } } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "404": { "$ref": "#/components/responses/NotFound" } } }, "post": { "operationId": "createProjectFolder", "tags": [ "Files" ], "summary": "Create a project folder", "description": "Any non-client member can manage folders; clients get 403.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "name" ], "properties": { "name": { "type": "string", "minLength": 1, "maxLength": 100 }, "color": { "type": [ "string", "null" ], "maxLength": 30 } } } } } }, "responses": { "201": { "description": "The created folder", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ProjectFolder" } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "403": { "$ref": "#/components/responses/Forbidden" }, "404": { "$ref": "#/components/responses/NotFound" }, "422": { "$ref": "#/components/responses/Validation" } } } }, "/api/v1/{orgSlug}/folders/{folderId}": { "patch": { "operationId": "updateProjectFolder", "tags": [ "Files" ], "summary": "Rename, recolor, or reorder a folder", "description": "Any non-client member; clients get 403.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" }, { "name": "folderId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "name": { "type": "string", "minLength": 1, "maxLength": 100 }, "color": { "type": [ "string", "null" ], "maxLength": 30 }, "position": { "type": "number" } } } } } }, "responses": { "200": { "description": "The updated folder", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ProjectFolder" } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "403": { "$ref": "#/components/responses/Forbidden" }, "404": { "$ref": "#/components/responses/NotFound" }, "422": { "$ref": "#/components/responses/Validation" } } }, "delete": { "operationId": "deleteProjectFolder", "tags": [ "Files" ], "summary": "Delete a project folder", "description": "The folder's projects drop back to the top level. Any non-client member; clients get 403.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" }, { "name": "folderId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "Deleted", "content": { "application/json": { "schema": { "type": "object", "required": [ "ok" ], "properties": { "ok": { "type": "boolean" } } } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "403": { "$ref": "#/components/responses/Forbidden" }, "404": { "$ref": "#/components/responses/NotFound" } } } }, "/api/v1/{orgSlug}/groups": { "get": { "operationId": "listGroups", "tags": [ "People" ], "summary": "List groups", "description": "Groups bundle non-client people for @-mentions and pickers.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" } ], "responses": { "200": { "description": "The organization's groups with their members", "content": { "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/PeopleGroup" } } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "404": { "$ref": "#/components/responses/NotFound" } } }, "post": { "operationId": "createGroup", "tags": [ "People" ], "summary": "Create a group", "parameters": [ { "$ref": "#/components/parameters/orgSlug" } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "name" ], "properties": { "name": { "type": "string", "minLength": 1, "maxLength": 200 } } } } } }, "responses": { "201": { "description": "The new group", "content": { "application/json": { "schema": { "type": "object", "required": [ "id", "name" ], "properties": { "id": { "type": "string", "format": "uuid" }, "name": { "type": "string" } } } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "403": { "$ref": "#/components/responses/Forbidden" }, "404": { "$ref": "#/components/responses/NotFound" }, "422": { "$ref": "#/components/responses/Validation" } } } }, "/api/v1/{orgSlug}/groups/{groupId}": { "patch": { "operationId": "updateGroup", "tags": [ "People" ], "summary": "Replace a group's members", "parameters": [ { "$ref": "#/components/parameters/orgSlug" }, { "name": "groupId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "membership_ids" ], "properties": { "membership_ids": { "type": "array", "items": { "type": "string", "format": "uuid" }, "description": "Replaces the member set" } } } } } }, "responses": { "200": { "description": "Updated", "content": { "application/json": { "schema": { "type": "object", "required": [ "ok" ], "properties": { "ok": { "type": "boolean" } } } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "403": { "$ref": "#/components/responses/Forbidden" }, "404": { "$ref": "#/components/responses/NotFound" }, "422": { "$ref": "#/components/responses/Validation" } } }, "delete": { "operationId": "deleteGroup", "tags": [ "People" ], "summary": "Delete a group", "parameters": [ { "$ref": "#/components/parameters/orgSlug" }, { "name": "groupId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "Deleted", "content": { "application/json": { "schema": { "type": "object", "required": [ "ok" ], "properties": { "ok": { "type": "boolean" } } } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "403": { "$ref": "#/components/responses/Forbidden" }, "404": { "$ref": "#/components/responses/NotFound" } } } }, "/api/v1/{orgSlug}/merge-people": { "get": { "operationId": "listMerges", "tags": [ "Organizations" ], "summary": "List recent people merges", "parameters": [ { "$ref": "#/components/parameters/orgSlug" } ], "responses": { "200": { "description": "Recent merges", "content": { "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/PersonMerge" } } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "403": { "$ref": "#/components/responses/Forbidden" }, "404": { "$ref": "#/components/responses/NotFound" } } }, "post": { "operationId": "mergePeople", "tags": [ "Organizations" ], "summary": "Merge two people", "description": "Irreversible dedupe: the winner inherits the loser's assignments, subscriptions, project access, and authorship.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "winner_id", "loser_id" ], "properties": { "winner_id": { "type": "string", "format": "uuid" }, "loser_id": { "type": "string", "format": "uuid" } } } } } }, "responses": { "200": { "description": "Merged", "content": { "application/json": { "schema": { "type": "object", "required": [ "ok" ], "properties": { "ok": { "type": "boolean" } } } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "403": { "$ref": "#/components/responses/Forbidden" }, "404": { "$ref": "#/components/responses/NotFound" }, "422": { "$ref": "#/components/responses/Validation" } } } }, "/api/v1/{orgSlug}/my/assignments": { "get": { "operationId": "getMyAssignments", "tags": [ "Search & reports" ], "summary": "Your open assignments", "description": "Open work assigned to you across projects, grouped into `priorities` (your Up Next list, in your order; items carry `priority_id`) and `non_priorities`. Card steps are normalized under their parent card as `children`: the card is pulled in even when only a step is assigned, and a prioritized step surfaces its card. Top-level items carry `assigned_to_me`, false only for a pulled-in parent card, which is not your assignment and cannot go Up Next.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" } ], "responses": { "200": { "description": "Your open assignments, grouped", "content": { "application/json": { "schema": { "type": "object", "required": [ "priorities", "non_priorities" ], "properties": { "priorities": { "type": "array", "items": { "$ref": "#/components/schemas/GroupedAssignment" } }, "non_priorities": { "type": "array", "items": { "$ref": "#/components/schemas/GroupedAssignment" } } } } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" } } } }, "/api/v1/{orgSlug}/my/assignments/completed": { "get": { "operationId": "listCompletedAssignments", "tags": [ "Search & reports" ], "summary": "Your completed assignments", "description": "Newest first; assignments on archived or trashed recordings are excluded.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" } ], "responses": { "200": { "description": "Completed assignments, newest first", "content": { "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/AssignmentRow" } } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" } } } }, "/api/v1/{orgSlug}/my/assignments/due": { "get": { "operationId": "listDueAssignments", "tags": [ "Search & reports" ], "summary": "Your dated assignments by due window", "description": "Dated open assignments in one due window. `overdue` is the default; `due_later_this_week` runs through this Sunday and `due_next_week` covers next Monday to Sunday, computed in your time zone.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" }, { "name": "scope", "in": "query", "schema": { "type": "string", "enum": [ "overdue", "due_today", "due_tomorrow", "due_later_this_week", "due_next_week", "due_later" ], "default": "overdue" } } ], "responses": { "200": { "description": "Dated open assignments in the window", "content": { "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/AssignmentRow" } } } } }, "400": { "description": "Invalid scope: the message lists the valid options", "content": { "application/json": { "schema": { "type": "object", "required": [ "error" ], "properties": { "error": { "type": "string" } } } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" } } } }, "/api/v1/{orgSlug}/my/assignments/priorities": { "get": { "operationId": "listUpNext", "tags": [ "Search & reports" ], "summary": "Your Up Next list", "description": "Your curated priority list, in your order; every row carries `priority_id`.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" } ], "responses": { "200": { "description": "Up Next, in your order", "content": { "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/UpNextItem" } } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" } } }, "post": { "operationId": "addUpNext", "tags": [ "Search & reports" ], "summary": "Add an assignment to Up Next", "description": "Adds one of your own open assignments to the bottom of Up Next (idempotent). Anything that is not your assignment is a 404.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "recording_id" ], "properties": { "recording_id": { "type": "string", "format": "uuid" } } } } } }, "responses": { "201": { "description": "Added (or already there)", "content": { "application/json": { "schema": { "type": "object", "required": [ "ok" ], "properties": { "ok": { "type": "boolean" } } } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "404": { "$ref": "#/components/responses/NotFound" }, "422": { "$ref": "#/components/responses/Validation" } } } }, "/api/v1/{orgSlug}/my/assignments/priorities/{recordingId}": { "patch": { "operationId": "moveUpNext", "tags": [ "Search & reports" ], "summary": "Reorder within Up Next", "description": "Swaps the item with its neighbor in the direction of `move`. Items not in your Up Next list are a 404.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" }, { "name": "recordingId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "move" ], "properties": { "move": { "type": "string", "enum": [ "up", "down" ] } } } } } }, "responses": { "200": { "description": "Reordered", "content": { "application/json": { "schema": { "type": "object", "required": [ "ok" ], "properties": { "ok": { "type": "boolean" } } } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "404": { "$ref": "#/components/responses/NotFound" }, "422": { "$ref": "#/components/responses/Validation" } } }, "delete": { "operationId": "removeUpNext", "tags": [ "Search & reports" ], "summary": "Drop an item out of Up Next", "description": "The item returns to `non_priorities` on the assignments list.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" }, { "name": "recordingId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "Removed", "content": { "application/json": { "schema": { "type": "object", "required": [ "ok" ], "properties": { "ok": { "type": "boolean" } } } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "404": { "$ref": "#/components/responses/NotFound" } } } }, "/api/v1/{orgSlug}/my/bookmarks": { "get": { "operationId": "listBookmarks", "tags": [ "Me" ], "summary": "Your bookmarks", "description": "Personal saved links to recordings, newest first.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" } ], "responses": { "200": { "description": "Your bookmarks, newest first", "content": { "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/BookmarkRow" } } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" } } }, "post": { "operationId": "addBookmark", "tags": [ "Me" ], "summary": "Bookmark a recording", "description": "Idempotent; the recording must be one you can see.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "recording_id" ], "properties": { "recording_id": { "type": "string", "format": "uuid" } } } } } }, "responses": { "201": { "description": "Bookmarked (or already was)", "content": { "application/json": { "schema": { "type": "object", "required": [ "ok" ], "properties": { "ok": { "type": "boolean" } } } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "404": { "$ref": "#/components/responses/NotFound" }, "422": { "$ref": "#/components/responses/Validation" } } } }, "/api/v1/{orgSlug}/my/bookmarks/{recordingId}": { "delete": { "operationId": "removeBookmark", "tags": [ "Me" ], "summary": "Remove a bookmark", "description": "Idempotent: removing a bookmark that is not there still succeeds.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" }, { "name": "recordingId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "Removed", "content": { "application/json": { "schema": { "type": "object", "required": [ "ok" ], "properties": { "ok": { "type": "boolean" } } } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" } } } }, "/api/v1/{orgSlug}/my/cheers": { "get": { "operationId": "listMyCheers", "tags": [ "Me" ], "summary": "Cheers you have received and given", "description": "`{received, given}`, each newest first. Cheer notifications are their own channel, gated solely by the `notify_cheers` preference and never by the notify scope.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" } ], "responses": { "200": { "description": "Received and given cheers", "content": { "application/json": { "schema": { "type": "object", "required": [ "received", "given" ], "properties": { "received": { "type": "array", "items": { "$ref": "#/components/schemas/MyCheerRow" } }, "given": { "type": "array", "items": { "$ref": "#/components/schemas/MyCheerRow" } } } } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" } } } }, "/api/v1/{orgSlug}/my/do-today": { "get": { "operationId": "getDoToday", "tags": [ "Me" ], "summary": "Today's events and tasks", "description": "`{events, tasks}` for today, side by side: your events plus your due assignments. The \"today\" window is computed in your resolved time zone, and each item carries `kind` (`event` or `task`).", "parameters": [ { "$ref": "#/components/parameters/orgSlug" } ], "responses": { "200": { "description": "Today's items", "content": { "application/json": { "schema": { "type": "object", "required": [ "events", "tasks" ], "properties": { "events": { "type": "array", "items": { "$ref": "#/components/schemas/DoTodayItem" } }, "tasks": { "type": "array", "items": { "$ref": "#/components/schemas/DoTodayItem" } } } } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" } } } }, "/api/v1/{orgSlug}/my/drafts": { "get": { "operationId": "listMyDrafts", "tags": [ "Me" ], "summary": "Your unpublished drafts", "description": "Drafts across the org. Only ever returns the caller's own: drafts are shareable by direct link to non-client project members, but they stay unlisted everywhere, and every draft write is author-only.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" } ], "responses": { "200": { "description": "Your drafts", "content": { "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/Recording" } } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" } } } }, "/api/v1/{orgSlug}/my/events": { "get": { "operationId": "listMyEvents", "tags": [ "Me" ], "summary": "Your events, next 7 days", "description": "Your upcoming events across all projects, recurring occurrences expanded. All-day events use the floating `T00:00:00Z` convention.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" } ], "responses": { "200": { "description": "Events in the next 7 days, in start order", "content": { "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/MyEventRow" } } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" } } } }, "/api/v1/{orgSlug}/my/events/soon": { "get": { "operationId": "getEventStartingSoon", "tags": [ "Me" ], "summary": "Your next event starting soon", "description": "The next timed event starting within about 20 minutes (the countdown banner); `{event: null}` when nothing is imminent. `href` is the web app path to the event, carrying `?occurrence=` for a recurring occurrence.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" } ], "responses": { "200": { "description": "The imminent event, or null", "content": { "application/json": { "schema": { "type": "object", "required": [ "event" ], "properties": { "event": { "type": [ "object", "null" ], "required": [ "id", "title", "starts_at", "href" ], "properties": { "id": { "type": "string", "format": "uuid" }, "title": { "type": [ "string", "null" ] }, "starts_at": { "type": "string", "format": "date-time" }, "link": { "type": [ "string", "null" ], "description": "The event's join link, when set" }, "href": { "type": "string", "description": "Web app path to the event" } } } } } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" } } } }, "/api/v1/{orgSlug}/my/notes": { "get": { "operationId": "listMyNotes", "tags": [ "Me" ], "summary": "Your private notes", "description": "My Notes: private rich-text notes with no project, visible only to their creator. Rows are `personal_note` recordings.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" } ], "responses": { "200": { "description": "Your notes", "content": { "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/Recording" } } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" } } }, "post": { "operationId": "createMyNote", "tags": [ "Me" ], "summary": "Create a private note", "description": "`content_html` (rich, sanitized) wins over `content` (plain text, converted to HTML).", "parameters": [ { "$ref": "#/components/parameters/orgSlug" } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "title": { "type": [ "string", "null" ], "maxLength": 10000 }, "content": { "type": [ "string", "null" ], "maxLength": 50000, "description": "Plain text; converted to HTML" }, "content_html": { "type": [ "string", "null" ], "maxLength": 100000, "description": "Rich HTML, sanitized; wins over content" } } } } } }, "responses": { "201": { "description": "The created note", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Recording" } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "422": { "$ref": "#/components/responses/Validation" } } } }, "/api/v1/{orgSlug}/my/notes/{noteId}": { "patch": { "operationId": "updateMyNote", "tags": [ "Me" ], "summary": "Edit a note", "description": "Send only the fields to change; `content_html` wins over `content`. Creator-only: anyone else's note is a 404.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" }, { "name": "noteId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "title": { "type": [ "string", "null" ], "maxLength": 10000 }, "content": { "type": [ "string", "null" ], "maxLength": 50000, "description": "Plain text; converted to HTML" }, "content_html": { "type": [ "string", "null" ], "maxLength": 100000, "description": "Rich HTML, sanitized; wins over content" } } } } } }, "responses": { "200": { "description": "The updated note", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Recording" } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "404": { "$ref": "#/components/responses/NotFound" }, "422": { "$ref": "#/components/responses/Validation" } } }, "delete": { "operationId": "deleteMyNote", "tags": [ "Me" ], "summary": "Delete a note", "description": "Permanent: notes skip the trash. Creator-only.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" }, { "name": "noteId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "Deleted", "content": { "application/json": { "schema": { "type": "object", "required": [ "ok" ], "properties": { "ok": { "type": "boolean" } } } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "404": { "$ref": "#/components/responses/NotFound" } } } }, "/api/v1/{orgSlug}/my/notifications": { "get": { "operationId": "listNotifications", "tags": [ "Notifications" ], "summary": "Your notification tray", "description": "`{unread_count, notifications}`, newest first. Event and due-date reminders sweep lazily on every read (the count-only arm included) with DB-level dedupe, so reminder rows land exactly once even under concurrent reads. Rows carry `bulletin_id` for platform announcements (action `announced`, actor-less) and `bundle_count` for chat rows: lines folded into one entry while it stayed unread. Project-chat rows use action `chatted` for the push-worthy idle-edge delivery and `chat_activity` for the quiet bundled tray row, both pointing at the room recording; visiting the room marks them read.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" }, { "name": "unread", "in": "query", "schema": { "type": "boolean" }, "description": "true: unread rows only" }, { "name": "count_only", "in": "query", "schema": { "type": "boolean" }, "description": "true: return {unread_count} alone (the mobile tab badge's poll; the reminder sweeps still run)" } ], "responses": { "200": { "description": "The tray (notifications omitted with count_only=true)", "content": { "application/json": { "schema": { "type": "object", "required": [ "unread_count" ], "properties": { "unread_count": { "type": "integer" }, "notifications": { "type": "array", "items": { "$ref": "#/components/schemas/NotificationRow" } } } } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" } } }, "put": { "operationId": "markAllNotificationsRead", "tags": [ "Notifications" ], "summary": "Mark all notifications read", "description": "Marks every notification in this org read.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" } ], "requestBody": { "required": false, "content": { "application/json": { "schema": { "type": "object", "properties": { "read": { "type": "boolean", "enum": [ true ] } }, "description": "Optional; {read: true} is the only accepted body" } } } }, "responses": { "200": { "description": "All read", "content": { "application/json": { "schema": { "type": "object", "required": [ "ok" ], "properties": { "ok": { "type": "boolean" } } } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" } } } }, "/api/v1/{orgSlug}/my/notifications/{notificationId}": { "patch": { "operationId": "markNotificationRead", "tags": [ "Notifications" ], "summary": "Mark one notification read or unread", "description": "`{read: false}` returns it to the tray's \"New for you\" section. Unknown ids are a quiet no-op.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" }, { "name": "notificationId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "read" ], "properties": { "read": { "type": "boolean" } } } } } }, "responses": { "200": { "description": "Updated", "content": { "application/json": { "schema": { "type": "object", "required": [ "ok" ], "properties": { "ok": { "type": "boolean" } } } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "422": { "$ref": "#/components/responses/Validation" } } } }, "/api/v1/{orgSlug}/my/snoozes": { "get": { "operationId": "listSnoozes", "tags": [ "Notifications" ], "summary": "Your snoozes (Remind me)", "description": "`{pending_count, due, pending}`: `due` rows' resurface time has passed (the sidebar's \"Back for you\" section), `pending` rows are still waiting (the \"N waiting\" list). Trashed content is excluded.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" } ], "responses": { "200": { "description": "Due and pending snoozes", "content": { "application/json": { "schema": { "type": "object", "required": [ "pending_count", "due", "pending" ], "properties": { "pending_count": { "type": "integer" }, "due": { "type": "array", "items": { "$ref": "#/components/schemas/SnoozeRow" } }, "pending": { "type": "array", "items": { "$ref": "#/components/schemas/SnoozeRow" } } } } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" } } } }, "/api/v1/{orgSlug}/my/snoozes/{recordingId}": { "post": { "operationId": "snoozeRecording", "tags": [ "Notifications" ], "summary": "Snooze a recording", "description": "Snoozes the recording until `resurface_at`, when it lands in the sidebar's \"Back for you\" section. Upserts your existing snooze; the recording must be one you can see.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" }, { "name": "recordingId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "resurface_at" ], "properties": { "resurface_at": { "type": "string", "format": "date-time", "description": "ISO 8601, local or with offset" }, "note": { "type": [ "string", "null" ], "maxLength": 500, "description": "A note to future you, shown when it resurfaces" } } } } } }, "responses": { "201": { "description": "Snoozed", "content": { "application/json": { "schema": { "type": "object", "required": [ "ok" ], "properties": { "ok": { "type": "boolean" } } } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "404": { "$ref": "#/components/responses/NotFound" }, "422": { "$ref": "#/components/responses/Validation" } } }, "delete": { "operationId": "dismissSnooze", "tags": [ "Notifications" ], "summary": "Dismiss a snooze", "description": "Idempotent: dismissing a snooze that is not there still succeeds.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" }, { "name": "recordingId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "Dismissed", "content": { "application/json": { "schema": { "type": "object", "required": [ "ok" ], "properties": { "ok": { "type": "boolean" } } } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" } } } }, "/api/v1/{orgSlug}/my/ui-state": { "get": { "operationId": "getUiState", "tags": [ "Me" ], "summary": "Your cross-device UI memory", "description": "A free-form JSON object of personal UI state, e.g. `{collapsedColumns: {: [columnIds]}}` for the saved-just-for-you card-table column collapse, keyed by project id.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" } ], "responses": { "200": { "description": "Your UI state ({} until you save some)", "content": { "application/json": { "schema": { "type": "object", "additionalProperties": true } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" } } }, "put": { "operationId": "updateUiState", "tags": [ "Me" ], "summary": "Merge into your UI memory", "description": "A shallow top-level merge: each key you send replaces that key wholesale, so send `collapsedColumns` complete. Returns the merged state.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "additionalProperties": true } } } }, "responses": { "200": { "description": "The merged state", "content": { "application/json": { "schema": { "type": "object", "additionalProperties": true } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "422": { "$ref": "#/components/responses/Validation" } } } }, "/api/v1/{orgSlug}/people": { "get": { "operationId": "listPeople", "tags": [ "People" ], "summary": "List the organization's people", "description": "Active members with role, title, company, and out-of-office dates. Soft-removed people are excluded.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" } ], "responses": { "200": { "description": "The organization's people", "content": { "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/Person" } } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "404": { "$ref": "#/components/responses/NotFound" } } }, "post": { "operationId": "invitePerson", "tags": [ "People" ], "summary": "Invite someone", "description": "Admins and up; owner invites require an owner. `note` rides the invitation email as plain text; `project_ids` are granted on accept. The email carries a decline link that stamps the invite declined and notifies the inviter. Plan seat limits apply.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "email", "role" ], "properties": { "email": { "type": "string", "format": "email" }, "role": { "$ref": "#/components/schemas/OrgRole" }, "company_id": { "type": "string", "format": "uuid" }, "name": { "type": "string", "maxLength": 200 }, "title": { "type": "string", "maxLength": 200 }, "note": { "type": "string", "maxLength": 2000, "description": "Plain text; included in the invitation email" }, "project_ids": { "type": "array", "items": { "type": "string", "format": "uuid" }, "maxItems": 100, "description": "Projects granted when the invite is accepted" } } } } } }, "responses": { "201": { "description": "Invitation sent", "content": { "application/json": { "schema": { "type": "object", "required": [ "invited" ], "properties": { "invited": { "type": "string", "format": "email" } } } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "402": { "$ref": "#/components/responses/PlanLimit" }, "403": { "$ref": "#/components/responses/Forbidden" }, "404": { "$ref": "#/components/responses/NotFound" }, "422": { "$ref": "#/components/responses/Validation" } } } }, "/api/v1/{orgSlug}/people/{membershipId}": { "get": { "operationId": "getPerson", "tags": [ "People" ], "summary": "One person's org profile", "parameters": [ { "$ref": "#/components/parameters/orgSlug" }, { "name": "membershipId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "The person", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Person" } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "404": { "$ref": "#/components/responses/NotFound" } } }, "patch": { "operationId": "updatePerson", "tags": [ "People" ], "summary": "Change a person's role, title, or company", "description": "Admins and up. Last-owner protections apply. `role: \"client\"` also strips group memberships and access to projects that don't work with clients.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" }, { "name": "membershipId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "role": { "$ref": "#/components/schemas/OrgRole" }, "company_id": { "type": [ "string", "null" ], "format": "uuid" }, "title": { "type": [ "string", "null" ], "maxLength": 200 } } } } } }, "responses": { "200": { "description": "Updated", "content": { "application/json": { "schema": { "type": "object", "required": [ "ok" ], "properties": { "ok": { "type": "boolean" } } } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "403": { "$ref": "#/components/responses/Forbidden" }, "404": { "$ref": "#/components/responses/NotFound" }, "422": { "$ref": "#/components/responses/Validation" } } }, "delete": { "operationId": "removePerson", "tags": [ "People" ], "summary": "Remove someone from the org (or leave)", "description": "Soft removal: contributions keep their name, and re-inviting the email reactivates the membership. Removing yourself is leaving; leaving accepts an optional `{note}` body (2000 chars max) emailed to the owners with the departure notice. Admin removes send no body.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" }, { "name": "membershipId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "requestBody": { "required": false, "content": { "application/json": { "schema": { "type": "object", "properties": { "note": { "type": "string", "maxLength": 2000, "description": "Leaving only; emailed to the owners" } } } } } }, "responses": { "200": { "description": "Removed", "content": { "application/json": { "schema": { "type": "object", "required": [ "ok" ], "properties": { "ok": { "type": "boolean" } } } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "403": { "$ref": "#/components/responses/Forbidden" }, "404": { "$ref": "#/components/responses/NotFound" } } } }, "/api/v1/{orgSlug}/people/{membershipId}/events": { "get": { "operationId": "listPersonEvents", "tags": [ "People" ], "summary": "One person's activity trail", "description": "The Someone's-activity report: the person plus their events, newest first. Team only; clients receive 403.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" }, { "name": "membershipId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } }, { "$ref": "#/components/parameters/page" }, { "$ref": "#/components/parameters/perPage" } ], "responses": { "200": { "description": "The person and their events", "content": { "application/json": { "schema": { "type": "object", "required": [ "person", "events" ], "properties": { "person": { "type": "object", "required": [ "id", "name" ], "properties": { "id": { "type": "string", "format": "uuid", "description": "Membership id" }, "name": { "type": "string" }, "title": { "type": [ "string", "null" ] }, "out_of_office": { "type": [ "object", "null" ], "properties": { "start": { "type": [ "string", "null" ], "format": "date" }, "end": { "type": [ "string", "null" ], "format": "date" } } } } }, "events": { "type": "array", "items": { "$ref": "#/components/schemas/PersonEvent" } } } } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "403": { "$ref": "#/components/responses/Forbidden" }, "404": { "$ref": "#/components/responses/NotFound" } } } }, "/api/v1/{orgSlug}/people/{membershipId}/out_of_office": { "get": { "operationId": "getOutOfOffice", "tags": [ "People" ], "summary": "Someone's out-of-office state", "description": "Dates are null while out-of-office is off.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" }, { "name": "membershipId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "The out-of-office representation", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/OutOfOffice" } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "404": { "$ref": "#/components/responses/NotFound" } } }, "post": { "operationId": "setOutOfOffice", "tags": [ "People" ], "summary": "Enable or replace out-of-office", "description": "Sets the date range, replacing any existing one. Writes are self-only unless the caller is an owner or admin.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" }, { "name": "membershipId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "out_of_office" ], "properties": { "out_of_office": { "type": "object", "required": [ "start_date", "end_date" ], "properties": { "start_date": { "type": "string", "format": "date" }, "end_date": { "type": "string", "format": "date", "description": "On or after start_date" } } } } } } } }, "responses": { "200": { "description": "The fresh representation", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/OutOfOffice" } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "403": { "$ref": "#/components/responses/Forbidden" }, "404": { "$ref": "#/components/responses/NotFound" }, "422": { "$ref": "#/components/responses/Validation" } } }, "delete": { "operationId": "disableOutOfOffice", "tags": [ "People" ], "summary": "Disable out-of-office", "description": "Idempotent. Same self-or-admin rule as enabling.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" }, { "name": "membershipId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "204": { "description": "Disabled" }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "403": { "$ref": "#/components/responses/Forbidden" }, "404": { "$ref": "#/components/responses/NotFound" } } } }, "/api/v1/{orgSlug}/people/{membershipId}/projects": { "get": { "operationId": "getPersonProjects", "tags": [ "People" ], "summary": "A person's project access", "description": "The admin Change-access surface (admin and up): every project the person can reach. `has_row` marks an explicit grant; non-clients are on all-access projects implicitly, and client targets see only clients-enabled projects. Removed people answer 404.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" }, { "name": "membershipId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "The person's reachable projects", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PersonProjectAccess" } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "403": { "$ref": "#/components/responses/Forbidden" }, "404": { "$ref": "#/components/responses/NotFound" } } }, "put": { "operationId": "setPersonProjects", "tags": [ "People" ], "summary": "Replace a person's explicit project access", "description": "Replaces the explicit grants (admin and up): the membership mirror of the invitation /projects route.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" }, { "name": "membershipId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "project_ids" ], "properties": { "project_ids": { "type": "array", "items": { "type": "string", "format": "uuid" }, "maxItems": 500 } } } } } }, "responses": { "200": { "description": "The fresh access representation", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PersonProjectAccess" } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "403": { "$ref": "#/components/responses/Forbidden" }, "404": { "$ref": "#/components/responses/NotFound" }, "422": { "$ref": "#/components/responses/Validation" } } } }, "/api/v1/{orgSlug}/people/invitations": { "get": { "operationId": "listInvitations", "tags": [ "People" ], "summary": "List pending invitations", "description": "Pending invitations, oldest first (admin and up). Declined invites stay listed (a resend revives them); accepted ones became memberships and drop out. `email_status` is the delivery ledger's word on the latest invite email; `join_url` is the same link the email button carries, for sharing over another channel when email fails.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" } ], "responses": { "200": { "description": "Pending invitations, oldest first", "content": { "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/Invitation" } } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "403": { "$ref": "#/components/responses/Forbidden" }, "404": { "$ref": "#/components/responses/NotFound" } } } }, "/api/v1/{orgSlug}/people/invitations/{invitationId}": { "delete": { "operationId": "revokeInvitation", "tags": [ "People" ], "summary": "Revoke a pending invitation", "description": "Hard delete (admin and up): the emailed link dies. Accepted or missing invitations answer 404.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" }, { "name": "invitationId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "204": { "description": "Revoked" }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "403": { "$ref": "#/components/responses/Forbidden" }, "404": { "$ref": "#/components/responses/NotFound" } } } }, "/api/v1/{orgSlug}/people/invitations/{invitationId}/projects": { "get": { "operationId": "getInvitationProjects", "tags": [ "People" ], "summary": "A pending invitee's project access", "description": "What the invitee will be able to reach, and which projects the invitation already grants (admin and up). Client invites see only clients-enabled projects.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" }, { "name": "invitationId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "The invitation's reachable projects with per-row grants", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/InvitationProjectAccess" } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "403": { "$ref": "#/components/responses/Forbidden" }, "404": { "$ref": "#/components/responses/NotFound" } } }, "put": { "operationId": "setInvitationProjects", "tags": [ "People" ], "summary": "Replace a pending invitee's project grants", "description": "Replaces the grants-on-accept (admin and up). Newly added projects send the invitee an added-you-to-that-project email whose join link lands them straight in that project after accepting.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" }, { "name": "invitationId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "project_ids" ], "properties": { "project_ids": { "type": "array", "items": { "type": "string", "format": "uuid" }, "maxItems": 500 } } } } } }, "responses": { "200": { "description": "The fresh access representation", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/InvitationProjectAccess" } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "403": { "$ref": "#/components/responses/Forbidden" }, "404": { "$ref": "#/components/responses/NotFound" }, "422": { "$ref": "#/components/responses/Validation" } } } }, "/api/v1/{orgSlug}/people/invitations/{invitationId}/resend": { "post": { "operationId": "resendInvitation", "tags": [ "People" ], "summary": "Resend an invitation", "description": "Nudge a pending invitee: fresh email, same still-valid token (admin and up). Also revives a declined invitation so it can be accepted again.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" }, { "name": "invitationId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "Resent", "content": { "application/json": { "schema": { "type": "object", "required": [ "ok" ], "properties": { "ok": { "type": "boolean" } } } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "403": { "$ref": "#/components/responses/Forbidden" }, "404": { "$ref": "#/components/responses/NotFound" } } } }, "/api/v1/{orgSlug}/people/invite-link": { "get": { "operationId": "getInviteLink", "tags": [ "People" ], "summary": "Get the shareable invite link", "description": "The org's auto-join link (admin and up): anyone holding the URL joins as a team member. Member seat limits are checked at join time, and the joiner's address is not auto-verified (a URL proves no mailbox).", "parameters": [ { "$ref": "#/components/parameters/orgSlug" } ], "responses": { "200": { "description": "The link's state; url is null while disabled", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/InviteLink" } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "403": { "$ref": "#/components/responses/Forbidden" }, "404": { "$ref": "#/components/responses/NotFound" } } }, "post": { "operationId": "enableInviteLink", "tags": [ "People" ], "summary": "Mint or re-enable the invite link", "description": "An empty body means make sure the link is on. `{\"rotate\": true}` swaps in a fresh token so the old URL stops working (admin and up).", "parameters": [ { "$ref": "#/components/parameters/orgSlug" } ], "requestBody": { "required": false, "content": { "application/json": { "schema": { "type": "object", "properties": { "rotate": { "type": "boolean", "description": "Swap the token; the old URL dies" } } } } } }, "responses": { "200": { "description": "The enabled link", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/InviteLink" } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "403": { "$ref": "#/components/responses/Forbidden" }, "404": { "$ref": "#/components/responses/NotFound" } } }, "delete": { "operationId": "disableInviteLink", "tags": [ "People" ], "summary": "Turn the invite link off", "description": "The off state sticks until a POST turns it back on (admin and up).", "parameters": [ { "$ref": "#/components/parameters/orgSlug" } ], "responses": { "200": { "description": "The disabled link (enabled false, url null)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/InviteLink" } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "403": { "$ref": "#/components/responses/Forbidden" }, "404": { "$ref": "#/components/responses/NotFound" } } } }, "/api/v1/{orgSlug}/projects": { "get": { "operationId": "listProjects", "tags": [ "Projects" ], "summary": "List projects", "description": "Active projects you can access, starred first then by name. Rows carry `starred`, `folder_id`, and `is_sample` (true only on the seeded Getting Started project, which never counts against the plan's project limit). `?status` lists archived or trashed instead. `?include=members` is the home listing: active rows also carry `members` (first 5 by name; all-access projects union the org's non-client roster) and `member_count`. `?scope=all` is the owner-only trusted list of every project with a `joined` flag; admins and members receive 403. Access model: nobody, owners and admins included, has implicit access to a non-all-access project's content; membership or `all_access` is required.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" }, { "name": "status", "in": "query", "schema": { "type": "string", "enum": [ "active", "archived", "trashed" ] }, "description": "Defaults to active" }, { "name": "include", "in": "query", "schema": { "type": "string", "enum": [ "members" ] }, "description": "members: the home listing's avatar-stack fields" }, { "name": "scope", "in": "query", "schema": { "type": "string", "enum": [ "all" ] }, "description": "all: owner-only list of every project with a joined flag" } ], "responses": { "200": { "description": "Projects, starred first then by name", "content": { "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/Project" } } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "403": { "$ref": "#/components/responses/Forbidden" }, "404": { "$ref": "#/components/responses/NotFound" } } }, "post": { "operationId": "createProject", "tags": [ "Projects" ], "summary": "Create a project", "description": "Auto-starred for the creator. `all_access` defaults to true. The plan's project cap applies; the seeded sample project never counts toward it.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "name" ], "properties": { "name": { "type": "string", "minLength": 1, "maxLength": 150 }, "description": { "type": "string", "maxLength": 2000 }, "all_access": { "type": "boolean", "default": true } } } } } }, "responses": { "201": { "description": "The created project", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Project" } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "402": { "$ref": "#/components/responses/PlanLimit" }, "403": { "$ref": "#/components/responses/Forbidden" }, "404": { "$ref": "#/components/responses/NotFound" }, "422": { "$ref": "#/components/responses/Validation" } } } }, "/api/v1/{orgSlug}/projects/{projectId}": { "get": { "operationId": "getProject", "tags": [ "Projects" ], "summary": "Get a project", "description": "The project plus its `tools`, each with its `container_id` (content is created under that container recording). `dock` is a deprecated alias of `tools` during the vocabulary transition.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" }, { "name": "projectId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "The project with its tools", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ProjectDetail" } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "404": { "$ref": "#/components/responses/NotFound" } } }, "patch": { "operationId": "updateProject", "tags": [ "Projects" ], "summary": "Update a project", "description": "`color` is a preset card color; anything outside the enum is a 400. `starts_on` and `ends_on` travel together: set both or null both, and `ends_on` must not precede `starts_on` (violations are a 400). `show_activity` toggles the project's activity rollups. `clients_enabled: false` reverts every recording in the project to team-only and drops client access (Work with clients); `client_company_name` is the label shown on the project. Move the project in or out of a home-screen folder with `folder_id` (or null).", "parameters": [ { "$ref": "#/components/parameters/orgSlug" }, { "name": "projectId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "name": { "type": "string", "minLength": 1, "maxLength": 150 }, "description": { "type": [ "string", "null" ], "maxLength": 2000 }, "all_access": { "type": "boolean" }, "folder_id": { "type": [ "string", "null" ], "format": "uuid" }, "color": { "type": [ "string", "null" ], "enum": [ "yellow", "orange", "coral", "pink", "purple", "blue", "green", "tan", "gray", null ] }, "starts_on": { "type": [ "string", "null" ], "format": "date" }, "ends_on": { "type": [ "string", "null" ], "format": "date" }, "show_activity": { "type": "boolean" }, "clients_enabled": { "type": "boolean" }, "client_company_name": { "type": [ "string", "null" ], "maxLength": 200 } } } } } }, "responses": { "200": { "description": "The updated project", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Project" } } } }, "400": { "description": "Unknown color, or a starts_on/ends_on pair rule violated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "403": { "$ref": "#/components/responses/Forbidden" }, "404": { "$ref": "#/components/responses/NotFound" }, "422": { "$ref": "#/components/responses/Validation" } } } }, "/api/v1/{orgSlug}/projects/{projectId}/events": { "get": { "operationId": "listProjectEvents", "tags": [ "Projects" ], "summary": "The project timeline", "description": "Basecamp's `/projects/{id}/timeline.json`. Rows carry a plain-text `excerpt`. Re-parenting moves and file downloads never appear here; they are change-log material on `GET /recordings/{id}/events`, matching Basecamp's timeline kinds. Clients see only activity about client-visible recordings.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" }, { "name": "projectId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } }, { "name": "page", "in": "query", "schema": { "type": "integer", "minimum": 1 }, "description": "1-based page number" }, { "name": "per_page", "in": "query", "schema": { "type": "integer", "minimum": 1, "maximum": 200 }, "description": "Page size, default 50, capped at 200" }, { "name": "limit", "in": "query", "schema": { "type": "integer", "minimum": 1 }, "description": "Plain alternative to per_page" }, { "name": "since", "in": "query", "schema": { "type": "string", "format": "date-time" }, "description": "Only events at or after this instant" }, { "name": "q", "in": "query", "schema": { "type": "string" }, "description": "Keyword filter" } ], "responses": { "200": { "description": "Timeline events, newest first", "content": { "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/ProjectEvent" } } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "404": { "$ref": "#/components/responses/NotFound" } } } }, "/api/v1/{orgSlug}/projects/{projectId}/health": { "get": { "operationId": "getProjectHealth", "tags": [ "Progress & health" ], "summary": "The project health gauge", "description": "`enabled`, the current `status`/`position` (0 to 100), `updated_at`, and the update history newest first. Clients see only client-visible updates and derive their current state from those.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" }, { "name": "projectId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "The gauge and its history", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/HealthGauge" } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "404": { "$ref": "#/components/responses/NotFound" } } }, "put": { "operationId": "setProjectHealthEnabled", "tags": [ "Progress & health" ], "summary": "Turn the gauge on or off", "description": "Admins only: Basecamp's gauge toggle.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" }, { "name": "projectId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "enabled" ], "properties": { "enabled": { "type": "boolean" } } } } } }, "responses": { "200": { "description": "Toggled", "content": { "application/json": { "schema": { "type": "object", "required": [ "ok" ], "properties": { "ok": { "type": "boolean" } } } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "403": { "$ref": "#/components/responses/Forbidden" }, "404": { "$ref": "#/components/responses/NotFound" }, "422": { "$ref": "#/components/responses/Validation" } } } }, "/api/v1/{orgSlug}/projects/{projectId}/health/updates": { "post": { "operationId": "createHealthUpdate", "tags": [ "Progress & health" ], "summary": "Post a health update", "description": "Move the needle: `position` (0 to 100) is required, `status` defaults to on_track. Creates a commentable, cheerable `health_update` recording. `notify` is the update's exact subscriber set; omitted, it falls back to the project notify audience (Thicket convention, vs bc3's omit meaning nobody). `visible_to_clients` defaults to false, hidden from clients. Only the note is editable afterwards (`PATCH /recordings/{id}`, author only); trashing the update rolls the needle back to the previous one.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" }, { "name": "projectId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "position" ], "properties": { "position": { "type": "number", "minimum": 0, "maximum": 100 }, "status": { "type": "string", "enum": [ "on_track", "some_risk", "concerned" ], "default": "on_track" }, "note": { "type": "string", "maxLength": 50000, "description": "Rich HTML; sanitized" }, "notify": { "description": "The exact subscriber set: everyone, none, or specific membership ids", "oneOf": [ { "type": "string", "enum": [ "everyone", "none" ] }, { "type": "array", "items": { "type": "string", "format": "uuid" } } ] }, "visible_to_clients": { "type": "boolean", "default": false } } } } } }, "responses": { "201": { "description": "The created health_update recording's id", "content": { "application/json": { "schema": { "type": "object", "required": [ "id" ], "properties": { "id": { "type": "string", "format": "uuid" } } } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "403": { "$ref": "#/components/responses/Forbidden" }, "404": { "$ref": "#/components/responses/NotFound" }, "422": { "$ref": "#/components/responses/Validation" } } } }, "/api/v1/{orgSlug}/projects/{projectId}/join": { "post": { "operationId": "joinProject", "tags": [ "Projects" ], "summary": "Add yourself to a project", "description": "Owner-only: ordinary membership plus an activity event, on any project. Admins and members receive 403. This and `?scope=all` listing are the only cracks in the invite-only wall: owners may see and self-add, never silently read.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" }, { "name": "projectId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "The joined project", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Project" } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "403": { "$ref": "#/components/responses/Forbidden" }, "404": { "$ref": "#/components/responses/NotFound" } } } }, "/api/v1/{orgSlug}/projects/{projectId}/links": { "get": { "operationId": "listProjectLinks", "tags": [ "Projects" ], "summary": "List project links", "description": "External links shown with the project's tools, in position order. Clients see only `visible_to_clients` rows.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" }, { "name": "projectId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "Links in position order", "content": { "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/ProjectLink" } } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "404": { "$ref": "#/components/responses/NotFound" } } }, "post": { "operationId": "createProjectLink", "tags": [ "Projects" ], "summary": "Add a project link", "description": "Clients receive 403.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" }, { "name": "projectId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "url", "title" ], "properties": { "url": { "type": "string", "format": "uri", "maxLength": 2000 }, "title": { "type": "string", "minLength": 1, "maxLength": 26 }, "description": { "type": [ "string", "null" ], "maxLength": 2000 }, "visible_to_clients": { "type": "boolean", "default": false } } } } } }, "responses": { "201": { "description": "The created link", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ProjectLink" } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "403": { "$ref": "#/components/responses/Forbidden" }, "404": { "$ref": "#/components/responses/NotFound" }, "422": { "$ref": "#/components/responses/Validation" } } } }, "/api/v1/{orgSlug}/projects/{projectId}/links/{linkId}": { "patch": { "operationId": "updateProjectLink", "tags": [ "Projects" ], "summary": "Update a project link", "description": "Any field, including `position` (finite number, fractional ok: the drag-reorder contract). Provide at least one field or receive 422. Clients receive 403.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" }, { "name": "projectId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } }, { "name": "linkId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "url": { "type": "string", "format": "uri", "maxLength": 2000 }, "title": { "type": "string", "minLength": 1, "maxLength": 26 }, "description": { "type": [ "string", "null" ], "maxLength": 2000 }, "visible_to_clients": { "type": "boolean" }, "position": { "type": "number" } } } } } }, "responses": { "200": { "description": "The updated link", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ProjectLink" } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "403": { "$ref": "#/components/responses/Forbidden" }, "404": { "$ref": "#/components/responses/NotFound" }, "422": { "$ref": "#/components/responses/Validation" } } }, "delete": { "operationId": "deleteProjectLink", "tags": [ "Projects" ], "summary": "Delete a project link", "description": "Hard delete: links are tool-level furniture with no trash lifecycle. Clients receive 403.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" }, { "name": "projectId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } }, { "name": "linkId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "204": { "description": "Deleted" }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "403": { "$ref": "#/components/responses/Forbidden" }, "404": { "$ref": "#/components/responses/NotFound" } } } }, "/api/v1/{orgSlug}/projects/{projectId}/logo": { "put": { "operationId": "uploadProjectLogo", "tags": [ "Projects" ], "summary": "Upload or replace the project logo", "description": "Multipart `logo` field: any decodable image, 10 MB or smaller, re-encoded to a square webp with metadata stripped. Bad, missing, or oversize files are a 422. Project payloads then carry the same-origin serve path in `logo` (immutable-cacheable, no session needed: the per-upload UUID is the capability). Gated like every project-settings write: clients receive 403, and the Restrict-who-can-edit-project-details governance toggle honors the creator rule.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" }, { "name": "projectId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "requestBody": { "required": true, "content": { "multipart/form-data": { "schema": { "type": "object", "required": [ "logo" ], "properties": { "logo": { "type": "string", "format": "binary", "description": "The image file, 10 MB or smaller" } } } } } }, "responses": { "204": { "description": "Stored" }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "403": { "$ref": "#/components/responses/Forbidden" }, "404": { "$ref": "#/components/responses/NotFound" }, "422": { "$ref": "#/components/responses/Validation" } } }, "delete": { "operationId": "deleteProjectLogo", "tags": [ "Projects" ], "summary": "Remove the project logo", "description": "Same gates as the upload.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" }, { "name": "projectId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "204": { "description": "Removed" }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "403": { "$ref": "#/components/responses/Forbidden" }, "404": { "$ref": "#/components/responses/NotFound" } } } }, "/api/v1/{orgSlug}/projects/{projectId}/notification-pref": { "get": { "operationId": "getProjectNotificationPref", "tags": [ "Projects" ], "summary": "Get the project bell", "description": "Your per-project notification preference. Resolution order: thread subscription, then project, then account default.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" }, { "name": "projectId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "The preference; null means the account default applies", "content": { "application/json": { "schema": { "type": "object", "required": [ "scope" ], "properties": { "scope": { "type": [ "string", "null" ], "enum": [ "everything", "mentions", null ] } } } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "404": { "$ref": "#/components/responses/NotFound" } } }, "put": { "operationId": "setProjectNotificationPref", "tags": [ "Projects" ], "summary": "Set the project bell", "description": "Notify about everything vs only when mentioned; null reverts to the account default.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" }, { "name": "projectId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "scope" ], "properties": { "scope": { "type": [ "string", "null" ], "enum": [ "everything", "mentions", null ] } } } } } }, "responses": { "200": { "description": "The stored preference", "content": { "application/json": { "schema": { "type": "object", "required": [ "scope" ], "properties": { "scope": { "type": [ "string", "null" ], "enum": [ "everything", "mentions", null ] } } } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "404": { "$ref": "#/components/responses/NotFound" }, "422": { "$ref": "#/components/responses/Validation" } } } }, "/api/v1/{orgSlug}/projects/{projectId}/people": { "get": { "operationId": "listProjectPeople", "tags": [ "Projects" ], "summary": "People on the project", "parameters": [ { "$ref": "#/components/parameters/orgSlug" }, { "name": "projectId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "The project's people", "content": { "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/ProjectPerson" } } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "404": { "$ref": "#/components/responses/NotFound" } } }, "put": { "operationId": "setProjectPeople", "tags": [ "Projects" ], "summary": "Change who has access", "description": "Either replace the explicit access list with `membership_ids`, or apply Basecamp-style deltas: `grant` and `revoke` take membership ids, and `create` invites someone new straight into this project (the grant rides the invitation). Provide at least one of the four. Clients can only be granted while the project works with clients; otherwise 403.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" }, { "name": "projectId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "membership_ids": { "type": "array", "items": { "type": "string", "format": "uuid" }, "description": "Replaces the project's explicit access list" }, "grant": { "type": "array", "maxItems": 200, "items": { "type": "string", "format": "uuid" } }, "revoke": { "type": "array", "maxItems": 200, "items": { "type": "string", "format": "uuid" } }, "create": { "type": "array", "maxItems": 50, "items": { "type": "object", "required": [ "email_address" ], "properties": { "name": { "type": "string", "maxLength": 200 }, "email_address": { "type": "string", "format": "email" }, "title": { "type": "string", "maxLength": 200 } } } } } } } } }, "responses": { "200": { "description": "The resulting roster plus the emails invited", "content": { "application/json": { "schema": { "type": "object", "required": [ "granted", "invited" ], "properties": { "granted": { "type": "array", "items": { "type": "object", "required": [ "membership_id", "name" ], "properties": { "membership_id": { "type": "string", "format": "uuid" }, "name": { "type": "string" }, "email": { "type": [ "string", "null" ] }, "role": { "type": "string" } } } }, "invited": { "type": "array", "items": { "type": "string", "format": "email" } } } } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "403": { "$ref": "#/components/responses/Forbidden" }, "404": { "$ref": "#/components/responses/NotFound" }, "422": { "$ref": "#/components/responses/Validation" } } } }, "/api/v1/{orgSlug}/projects/{projectId}/star": { "put": { "operationId": "starProject", "tags": [ "Projects" ], "summary": "Star a project", "description": "Personal: starred projects pin to the top of home.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" }, { "name": "projectId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "Starred", "content": { "application/json": { "schema": { "type": "object", "required": [ "starred" ], "properties": { "starred": { "type": "boolean" } } } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "404": { "$ref": "#/components/responses/NotFound" } } }, "delete": { "operationId": "unstarProject", "tags": [ "Projects" ], "summary": "Unstar a project", "parameters": [ { "$ref": "#/components/parameters/orgSlug" }, { "name": "projectId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "Unstarred", "content": { "application/json": { "schema": { "type": "object", "required": [ "starred" ], "properties": { "starred": { "type": "boolean" } } } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "404": { "$ref": "#/components/responses/NotFound" } } } }, "/api/v1/{orgSlug}/projects/{projectId}/status/{status}": { "put": { "operationId": "setProjectStatus", "tags": [ "Projects" ], "summary": "Change the project lifecycle", "description": "The one lifecycle sub-path: activate, archive, or trash the project. Restoring from the trash is the same call with active.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" }, { "name": "projectId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } }, { "name": "status", "in": "path", "required": true, "schema": { "type": "string", "enum": [ "active", "archived", "trashed" ] } } ], "responses": { "200": { "description": "The project in its new status", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Project" } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "404": { "$ref": "#/components/responses/NotFound" }, "422": { "$ref": "#/components/responses/Validation" } } } }, "/api/v1/{orgSlug}/projects/{projectId}/tools": { "get": { "operationId": "listProjectTools", "tags": [ "Projects" ], "summary": "The project's tools", "description": "Each enabled tool has exactly one singleton container recording (its `container_id`); content is created under it.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" }, { "name": "projectId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "The tool list in position order", "content": { "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/ToolEntry" } } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "404": { "$ref": "#/components/responses/NotFound" } } } }, "/api/v1/{orgSlug}/projects/{projectId}/tools/{tool}": { "put": { "operationId": "updateProjectTool", "tags": [ "Projects" ], "summary": "Configure one tool", "description": "Toggle the tool, rename it on this project (`title: null` restores the default name; a per-project rename wins over the org-wide override), and/or reposition it in the tool list (finite number, fractional ok: the drag-reorder contract). Provide at least one field or receive 422. Clients receive 403. Returns the whole tool list. Legacy tool names (todoset, vault, schedule, card_table, questionnaire, clientside) are still accepted on the path segment during the vocabulary transition.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" }, { "name": "projectId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } }, { "name": "tool", "in": "path", "required": true, "schema": { "type": "string", "enum": [ "message_board", "todos", "folder", "calendar", "board", "chat", "check_ins", "clients" ] } } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "enabled": { "type": "boolean" }, "title": { "type": [ "string", "null" ], "maxLength": 60, "description": "null restores the default name" }, "position": { "type": "number" } } } } } }, "responses": { "200": { "description": "The whole tool list after the change", "content": { "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/ToolEntry" } } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "403": { "$ref": "#/components/responses/Forbidden" }, "404": { "$ref": "#/components/responses/NotFound" }, "422": { "$ref": "#/components/responses/Validation" } } } }, "/api/v1/{orgSlug}/projects/{projectId}/trash": { "get": { "operationId": "listProjectTrash", "tags": [ "Projects" ], "summary": "The project trash", "description": "The project trash page. Members see the items they created or trashed, org admins see the project's whole trash, clients only client-visible items. Restore rides the generic `PUT /recordings/{id}/status/active`.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" }, { "name": "projectId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "Trashed items visible to you", "content": { "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/ProjectTrashItem" } } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "404": { "$ref": "#/components/responses/NotFound" } } }, "delete": { "operationId": "emptyProjectTrash", "tags": [ "Projects" ], "summary": "Empty the project trash", "description": "Permanently destroys the same set the GET shows you: your own items for members, the whole project trash for admins. Irreversible.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" }, { "name": "projectId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "How many recordings were destroyed", "content": { "application/json": { "schema": { "type": "object", "required": [ "deleted" ], "properties": { "deleted": { "type": "integer" } } } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "404": { "$ref": "#/components/responses/NotFound" } } } }, "/api/v1/{orgSlug}/public-items": { "get": { "operationId": "listPublicItems", "tags": [ "Recordings" ], "summary": "List every active public link", "description": "Owner-only: the Admin inventory of everything shared at /public/{token}. Non-owners get 403.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" } ], "responses": { "200": { "description": "The active links", "content": { "application/json": { "schema": { "type": "array", "items": { "type": "object", "required": [ "recording_id", "url", "type", "created_at" ], "properties": { "recording_id": { "type": "string", "format": "uuid" }, "url": { "type": "string", "format": "uri", "description": "The public capability URL" }, "type": { "$ref": "#/components/schemas/RecordingType" }, "title": { "type": [ "string", "null" ] }, "project_id": { "type": [ "string", "null" ], "format": "uuid" }, "project_name": { "type": [ "string", "null" ] }, "created_by_name": { "type": [ "string", "null" ] }, "created_at": { "type": "string", "format": "date-time" } } } } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "403": { "$ref": "#/components/responses/Forbidden" }, "404": { "$ref": "#/components/responses/NotFound" } } }, "delete": { "operationId": "revokeAllPublicItems", "tags": [ "Recordings" ], "summary": "Unpublish everything", "description": "Owner-only: revoke every active public link at once. Non-owners get 403.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" } ], "responses": { "200": { "description": "How many links were revoked", "content": { "application/json": { "schema": { "type": "object", "required": [ "revoked" ], "properties": { "revoked": { "type": "integer" } } } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "403": { "$ref": "#/components/responses/Forbidden" }, "404": { "$ref": "#/components/responses/NotFound" } } } }, "/api/v1/{orgSlug}/public-items/{recordingId}": { "delete": { "operationId": "revokePublicItem", "tags": [ "Recordings" ], "summary": "Unpublish one item", "description": "Owner-only: revoke one recording's public link from the Admin list. Non-owners get 403.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" }, { "name": "recordingId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "Revoked", "content": { "application/json": { "schema": { "type": "object", "required": [ "ok" ], "properties": { "ok": { "type": "boolean" } } } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "403": { "$ref": "#/components/responses/Forbidden" }, "404": { "$ref": "#/components/responses/NotFound" } } } }, "/api/v1/{orgSlug}/reassign": { "post": { "operationId": "reassignTodos", "tags": [ "Trash & admin" ], "summary": "Bulk-change a person's open to-do assignments", "description": "Owner only, account-wide or in one project. Three modes: `reassign` moves them to someone else (the default), `remove` takes them off everything (no target), `add` adds someone alongside while the source keeps theirs. Completed history is untouched; on reassign and add the target inherits the source's subscriptions on the affected items; one summary event is logged per affected project; the acting owner gets an email confirmation.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "from_id" ], "properties": { "mode": { "type": "string", "enum": [ "reassign", "remove", "add" ], "default": "reassign" }, "from_id": { "type": "string", "format": "uuid" }, "to_id": { "type": [ "string", "null" ], "format": "uuid", "description": "Required unless mode is remove" }, "project_id": { "type": [ "string", "null" ], "format": "uuid", "description": "Omit for account-wide" } } } } } }, "responses": { "200": { "description": "What changed", "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/ReassignImpact" }, { "type": "object", "required": [ "email_queued" ], "properties": { "email_queued": { "type": "boolean", "description": "Whether the confirmation email was queued" } } } ] } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "403": { "$ref": "#/components/responses/Forbidden" }, "404": { "$ref": "#/components/responses/NotFound" }, "422": { "$ref": "#/components/responses/Validation" } } } }, "/api/v1/{orgSlug}/reassign/preview": { "get": { "operationId": "previewReassign", "tags": [ "Trash & admin" ], "summary": "Preview a bulk assignment change", "description": "Owner only: how many open to-dos the change would touch, per project. Open to-dos only; `to` is unused for mode=remove.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" }, { "name": "from", "in": "query", "required": true, "schema": { "type": "string", "format": "uuid" }, "description": "The source membership id" }, { "name": "to", "in": "query", "schema": { "type": "string", "format": "uuid" }, "description": "The target membership id; required unless mode=remove" }, { "name": "project", "in": "query", "schema": { "type": "string", "format": "uuid" }, "description": "Limit to one project" }, { "name": "mode", "in": "query", "schema": { "type": "string", "enum": [ "reassign", "remove", "add" ], "default": "reassign" } } ], "responses": { "200": { "description": "The would-be impact", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ReassignImpact" } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "403": { "$ref": "#/components/responses/Forbidden" }, "404": { "$ref": "#/components/responses/NotFound" }, "422": { "$ref": "#/components/responses/Validation" } } } }, "/api/v1/{orgSlug}/recordings": { "get": { "operationId": "listRecordings", "tags": [ "Recordings" ], "summary": "Query recordings across projects", "description": "The generic cross-project query: every row of the given `type` you can access, any project. Filter with `project_id` and `status`.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" }, { "name": "type", "in": "query", "required": true, "schema": { "type": "string", "enum": [ "message_board", "message", "comment", "todos", "todolist", "todo", "folder", "document", "upload", "calendar", "calendar_event", "board", "column", "card", "step", "chat", "chat_message" ] }, "description": "The recording type to list" }, { "name": "project_id", "in": "query", "schema": { "type": "string", "format": "uuid" } }, { "name": "status", "in": "query", "schema": { "type": "string", "enum": [ "active", "archived", "trashed" ] } } ], "responses": { "200": { "description": "Matching recordings", "content": { "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/Recording" } } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "404": { "$ref": "#/components/responses/NotFound" }, "422": { "$ref": "#/components/responses/Validation" } } } }, "/api/v1/{orgSlug}/recordings/{recordingId}": { "get": { "operationId": "getRecording", "tags": [ "Recordings" ], "summary": "Fetch one recording", "description": "Any recording, flat route. Includes the author byline (`creator_name`, `creator_image`; names survive the person leaving the org) and `content_attachments`, structured metadata for every file embedded in the rich text (always present, [] when none). Drafts return to any non-client project member by link but every write stays author-only; clients always 404 on drafts.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" }, { "name": "recordingId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "The recording with byline and attachment metadata", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/RecordingDetail" } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "404": { "$ref": "#/components/responses/NotFound" } } }, "patch": { "operationId": "updateRecording", "tags": [ "Recordings" ], "summary": "Edit a recording", "description": "All fields stay editable after creation. To-dos use `content`/`content_html` for notes and `starts_on`..`due_on` for a date range. `recurrence` is to-dos only (422 otherwise): validated and merged into `meta.recurrence` without clobbering sibling keys, `null` stops the series; editing the primary re-projects every FUTURE occurrence while already-spawned instances keep their own fields. `scheduled_at` re-schedules a draft (`null` removes the schedule). `base_name` renames an upload, keeping the extension. `schedule` and `membership_ids` are check-in questions only (new askees get subscribed). Comment and chat-line edits are author-only and stamp `meta.editedAt`; the governance 15-minute edit window returns 403 once closed.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" }, { "name": "recordingId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "title": { "type": [ "string", "null" ], "maxLength": 10000 }, "content": { "type": [ "string", "null" ], "maxLength": 50000, "description": "Plain text; converted to safe HTML server-side" }, "content_html": { "type": [ "string", "null" ], "maxLength": 100000, "description": "Rich HTML; sanitized server-side. Wins over content if both sent" }, "due_on": { "type": [ "string", "null" ], "format": "date" }, "starts_on": { "type": [ "string", "null" ], "format": "date", "description": "To-do date range start (date, not timestamp; pairs with due_on)" }, "starts_at": { "type": [ "string", "null" ], "format": "date-time" }, "ends_at": { "type": [ "string", "null" ], "format": "date-time" }, "all_day": { "type": [ "boolean", "null" ] }, "category_id": { "type": [ "string", "null" ], "format": "uuid" }, "scheduled_at": { "type": [ "string", "null" ], "format": "date-time", "description": "Drafts only: publish automatically at this time; null removes the schedule" }, "meta": { "type": [ "object", "null" ], "additionalProperties": true }, "base_name": { "type": "string", "minLength": 1, "maxLength": 280, "description": "Uploads only: rename the file, the extension is kept" }, "schedule": { "$ref": "#/components/schemas/QuestionSchedule" }, "membership_ids": { "type": "array", "items": { "type": "string", "format": "uuid" }, "minItems": 1, "description": "Questions only: replace who's asked (new askees get subscribed)" }, "recurrence": { "oneOf": [ { "$ref": "#/components/schemas/RecordingRecurrence" }, { "type": "null" } ], "description": "To-dos only: set/replace the repeat schedule (merged into meta.recurrence); null stops the series" } } } } } }, "responses": { "200": { "description": "The updated recording", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Recording" } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "402": { "$ref": "#/components/responses/PlanLimit" }, "403": { "$ref": "#/components/responses/Forbidden" }, "404": { "$ref": "#/components/responses/NotFound" }, "422": { "$ref": "#/components/responses/Validation" } } }, "delete": { "operationId": "deleteRecordingLine", "tags": [ "Recordings" ], "summary": "Hard-delete a chat or DM line", "description": "Immediate, permanent deletion, chat and DM lines only (author or admin+); these never enter the trash. Everything else, comments included, goes through PUT .../status/trashed instead. Under the governance 15-minute window, authors lose delete once it closes (403); admins keep it.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" }, { "name": "recordingId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "Deleted", "content": { "application/json": { "schema": { "type": "object", "required": [ "ok" ], "properties": { "ok": { "type": "boolean", "const": true } } } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "403": { "$ref": "#/components/responses/Forbidden" }, "404": { "$ref": "#/components/responses/NotFound" } } } }, "/api/v1/{orgSlug}/recordings/{recordingId}/answers": { "get": { "operationId": "listAnswers", "tags": [ "Check-ins" ], "summary": "List a question's answers", "description": "Answers to a check-in question, newest day first. Each answer is stamped with its day (`starts_on`); rows carry `creator_name` and `comment_count`.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" }, { "name": "recordingId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" }, "description": "The check-in question's id" }, { "name": "date", "in": "query", "schema": { "type": "string", "format": "date" }, "description": "Only answers for this day" }, { "name": "creator_id", "in": "query", "schema": { "type": "string", "format": "uuid" }, "description": "Only answers by this membership" } ], "responses": { "200": { "description": "The answers, newest day first", "content": { "application/json": { "schema": { "type": "array", "items": { "allOf": [ { "$ref": "#/components/schemas/Recording" }, { "type": "object", "properties": { "creator_name": { "type": [ "string", "null" ] }, "comment_count": { "type": "integer" } } } ] } } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "404": { "$ref": "#/components/responses/NotFound" }, "422": { "$ref": "#/components/responses/Validation" } } }, "post": { "operationId": "createAnswer", "tags": [ "Check-ins" ], "summary": "Answer a check-in question", "description": "Answers the given day, default today; any day is pickable. Send `content` (plain text) or `content_html` (rich, sanitized); one is required. Answers are commentable and cheerable, stamped with their day on `starts_on`, and notify the question's subscribers, never the poster. Editing afterwards is author-only (PATCH `content` or `starts_on` on the answer; anyone else gets 404).", "parameters": [ { "$ref": "#/components/parameters/orgSlug" }, { "name": "recordingId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" }, "description": "The check-in question's id" } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "description": "One of `content` or `content_html` is required", "properties": { "content": { "type": "string", "maxLength": 50000, "description": "Plain text, converted to HTML" }, "content_html": { "type": "string", "maxLength": 100000, "description": "Rich HTML, sanitized server-side" }, "occurs_on": { "type": "string", "format": "date", "description": "The day being answered; default today" } } } } } }, "responses": { "201": { "description": "The created answer recording", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Recording" } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "404": { "$ref": "#/components/responses/NotFound" }, "422": { "$ref": "#/components/responses/Validation" } } } }, "/api/v1/{orgSlug}/recordings/{recordingId}/answers/by": { "get": { "operationId": "listAnswerers", "tags": [ "Check-ins" ], "summary": "List who has answered", "description": "Everyone who has answered the question, newest answer first (bc3's list-answerers convention).", "parameters": [ { "$ref": "#/components/parameters/orgSlug" }, { "name": "recordingId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" }, "description": "The check-in question's id" } ], "responses": { "200": { "description": "The answerers", "content": { "application/json": { "schema": { "type": "array", "items": { "type": "object", "required": [ "membership_id", "name", "answer_count", "latest_on" ], "properties": { "membership_id": { "type": "string", "format": "uuid" }, "name": { "type": "string" }, "answer_count": { "type": "integer" }, "latest_on": { "type": "string", "format": "date", "description": "The day of their most recent answer" } } } } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "404": { "$ref": "#/components/responses/NotFound" } } } }, "/api/v1/{orgSlug}/recordings/{recordingId}/approval": { "put": { "operationId": "decideApproval", "tags": [ "Clients" ], "summary": "Decide an approval", "description": "The assigned approver decides, one shot: anyone else gets 403, and an already-decided approval also returns 403. The decision lands on the recording's `approval_status` and subscribers are notified.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" }, { "name": "recordingId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" }, "description": "The client_approval recording's id" } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "decision" ], "properties": { "decision": { "type": "string", "enum": [ "approved", "rejected" ] } } } } } }, "responses": { "200": { "description": "The decided approval recording", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Recording" } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "403": { "$ref": "#/components/responses/Forbidden" }, "404": { "$ref": "#/components/responses/NotFound" }, "422": { "$ref": "#/components/responses/Validation" } } } }, "/api/v1/{orgSlug}/recordings/{recordingId}/approvals": { "post": { "operationId": "createApproval", "tags": [ "Clients" ], "summary": "Ask a client to approve something", "description": "Creates a `client_approval` under the project's clients container; `approver_id` must be a client membership on the project. Approvals are requested by the team, not clients (403). List them via GET /recordings/{clientsId}/children?type=client_approval, whose rows carry `assignees` (the first is the approver).", "parameters": [ { "$ref": "#/components/parameters/orgSlug" }, { "name": "recordingId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" }, "description": "The project's clients container id" } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "title", "approver_id" ], "properties": { "title": { "type": "string", "minLength": 1, "maxLength": 10000 }, "content_html": { "type": "string", "maxLength": 100000, "description": "Rich HTML, sanitized server-side" }, "due_on": { "type": "string", "format": "date" }, "approver_id": { "type": "string", "format": "uuid", "description": "The client membership asked to approve" } } } } } }, "responses": { "201": { "description": "The created approval recording", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Recording" } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "403": { "$ref": "#/components/responses/Forbidden" }, "404": { "$ref": "#/components/responses/NotFound" }, "422": { "$ref": "#/components/responses/Validation" } } } }, "/api/v1/{orgSlug}/recordings/{recordingId}/assignees": { "get": { "operationId": "listAssignees", "tags": [ "Recordings" ], "summary": "List assignees", "parameters": [ { "$ref": "#/components/parameters/orgSlug" }, { "name": "recordingId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "Who's assigned", "content": { "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/RecordingPersonRow" } } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "404": { "$ref": "#/components/responses/NotFound" } } }, "put": { "operationId": "setAssignees", "tags": [ "Recordings" ], "summary": "Replace the assignee set", "description": "Assignment is always many-capable: PUT replaces the WHOLE set (always an array; [] clears). New assignees are notified and subscribed, and so is whoever assigned them: assigning follows the work.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" }, { "name": "recordingId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "membership_ids" ], "properties": { "membership_ids": { "type": "array", "items": { "type": "string", "format": "uuid" } } } } } } }, "responses": { "200": { "description": "Replaced", "content": { "application/json": { "schema": { "type": "object", "required": [ "ok" ], "properties": { "ok": { "type": "boolean", "const": true } } } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "404": { "$ref": "#/components/responses/NotFound" }, "422": { "$ref": "#/components/responses/Validation" } } } }, "/api/v1/{orgSlug}/recordings/{recordingId}/cheers": { "get": { "operationId": "listCheers", "tags": [ "Recordings" ], "summary": "List a recording's cheers", "parameters": [ { "$ref": "#/components/parameters/orgSlug" }, { "name": "recordingId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "The cheers", "content": { "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/RecordingCheerRow" } } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "404": { "$ref": "#/components/responses/NotFound" } } }, "post": { "operationId": "addCheer", "tags": [ "Recordings" ], "summary": "Leave a cheer", "description": "A short reaction: up to 16 characters of emoji or text. Rejected while comments are closed, on drafts, and on uncheerable types (404). Cheering notifies the recording's author only, on their own channel gated solely by their notify_cheers preference (never the notify scope), bundled into one notification per recording within 3 hours. Remove one via DELETE /cheers/{cheer_id}.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" }, { "name": "recordingId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "content" ], "properties": { "content": { "type": "string", "minLength": 1, "maxLength": 16 } } } } } }, "responses": { "201": { "description": "The cheer", "content": { "application/json": { "schema": { "type": "object", "required": [ "id", "content" ], "properties": { "id": { "type": "string", "format": "uuid" }, "content": { "type": "string" } } } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "404": { "$ref": "#/components/responses/NotFound" }, "409": { "description": "You already left that exact cheer (code duplicate)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "422": { "$ref": "#/components/responses/Validation" } } } }, "/api/v1/{orgSlug}/recordings/{recordingId}/children": { "get": { "operationId": "listChildren", "tags": [ "Recordings" ], "summary": "List a container's children", "description": "Default order is position/created; `sort`+`direction` follow the Basecamp messages-listing convention, and `before`+`limit` page backwards by creation time (chat history uses this cursor). Rows carry `creator_name`, `creator_image`, `comment_count`, `last_comment_at` so list surfaces render bylines and comment badges without N+1 lookups; `chat_message` rows additionally carry their `cheers` and `client_approval` rows their `assignees`. `status=drafted` only ever returns the caller's own drafts.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" }, { "name": "recordingId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } }, { "name": "type", "in": "query", "schema": { "type": "string", "enum": [ "message", "comment", "todolist", "todo", "document", "upload", "linked_file", "folder", "calendar_event", "column", "card", "step", "chat_message", "question", "answer", "client_approval", "client_correspondence" ] } }, { "name": "status", "in": "query", "schema": { "type": "string", "enum": [ "active", "archived", "trashed", "drafted" ] } }, { "name": "completed", "in": "query", "schema": { "type": "string", "enum": [ "true", "false" ] } }, { "name": "sort", "in": "query", "schema": { "type": "string", "enum": [ "created_at", "updated_at" ] } }, { "name": "direction", "in": "query", "schema": { "type": "string", "enum": [ "asc", "desc" ] } }, { "name": "before", "in": "query", "schema": { "type": "string", "format": "date-time" }, "description": "Only children created before this instant (chat history paging)" }, { "name": "limit", "in": "query", "schema": { "type": "integer", "minimum": 1, "maximum": 500 } } ], "responses": { "200": { "description": "The children, enriched list rows", "content": { "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/ChildListRow" } } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "404": { "$ref": "#/components/responses/NotFound" }, "422": { "$ref": "#/components/responses/Validation" } } }, "post": { "operationId": "createChild", "tags": [ "Recordings" ], "summary": "Create content under a container", "description": "Creates any content type under its container; parent rules are validated server-side (message under message_board, todolist under todos or todolist as a group, todo under todolist/todos/todo as a subtask, document/upload/linked_file/folder under folder, calendar_event under calendar, column under board, card under column, step under card, chat_message under chat or dm, question under check_ins, answer under question). `url` is required for linked_file (stored on meta.url). `notify` applies to messages, documents, and linked files: \"everyone\" (default), \"none\", or membership ids; the resolved set is notified when the item posts and becomes its exact subscriber set (\"none\" subscribes nobody, not even the author), honored when a draft or scheduled post publishes; picked people outside the project audience are dropped. `completion_subscriber_ids` are subscribed at creation and notified on completion. `recurrence` needs a dated to-do (422 otherwise) and makes it a recurring series' primary. `status: \"drafted\"` keeps it private to you until published; add `scheduled_at` to publish automatically. Clients can't add loose to-dos (403).", "parameters": [ { "$ref": "#/components/parameters/orgSlug" }, { "name": "recordingId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "type" ], "properties": { "type": { "type": "string", "enum": [ "message", "todolist", "todo", "document", "folder", "linked_file", "calendar_event", "column", "card", "step", "chat_message", "question", "answer", "client_correspondence" ] }, "title": { "type": "string", "maxLength": 10000 }, "content": { "type": "string", "maxLength": 50000, "description": "Plain text; converted to safe HTML server-side" }, "content_html": { "type": "string", "maxLength": 100000, "description": "Rich HTML; sanitized server-side. Wins over content if both sent" }, "due_on": { "type": "string", "format": "date" }, "starts_on": { "type": "string", "format": "date", "description": "To-do date range start (pairs with due_on)" }, "starts_at": { "type": "string", "format": "date-time" }, "ends_at": { "type": "string", "format": "date-time" }, "all_day": { "type": "boolean" }, "position": { "type": "number", "description": "Explicit sort position (fractional ok); omitted appends to the end" }, "assignee_ids": { "type": "array", "items": { "type": "string", "format": "uuid" } }, "completion_subscriber_ids": { "type": "array", "items": { "type": "string", "format": "uuid" }, "description": "When done, notify: subscribed at creation, notified on completion" }, "visible_to_clients": { "type": "boolean" }, "status": { "type": "string", "enum": [ "active", "drafted" ], "description": "drafted keeps it private to you until published" }, "scheduled_at": { "type": "string", "format": "date-time", "description": "With status drafted: publish automatically at this time" }, "category_id": { "type": "string", "format": "uuid" }, "meta": { "type": "object", "additionalProperties": true, "description": "Type-specific settings" }, "url": { "type": "string", "format": "uri", "maxLength": 2000, "description": "Linked files only: where the file lives; required for the type" }, "notify": { "oneOf": [ { "type": "string", "enum": [ "everyone", "none" ] }, { "type": "array", "items": { "type": "string", "format": "uuid" } } ], "description": "Messages, documents, linked files: who's notified when this posts; they become the thread's exact subscriber set" }, "recurrence": { "oneOf": [ { "$ref": "#/components/schemas/RecordingRecurrence" }, { "type": "null" } ], "description": "Dated to-dos only: the repeat schedule, stored as meta.recurrence" } } } } } }, "responses": { "201": { "description": "The created recording", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Recording" } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "402": { "$ref": "#/components/responses/PlanLimit" }, "403": { "$ref": "#/components/responses/Forbidden" }, "404": { "$ref": "#/components/responses/NotFound" }, "422": { "$ref": "#/components/responses/Validation" } } } }, "/api/v1/{orgSlug}/recordings/{recordingId}/color": { "put": { "operationId": "setRecordingColor", "tags": [ "Recordings" ], "summary": "Set the color", "description": "Color band on a to-do group or card column, or icon tint on a Docs & Files item (folder, doc, file, linked file); `white` or null clears. The special Triage/Not Now/Done columns and the root folder refuse (404).", "parameters": [ { "$ref": "#/components/parameters/orgSlug" }, { "name": "recordingId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "color" ], "properties": { "color": { "type": [ "string", "null" ], "enum": [ "white", "red", "orange", "yellow", "green", "blue", "aqua", "purple", "gray", "pink", "brown", null ] } } } } } }, "responses": { "200": { "description": "The recolored recording", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Recording" } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "404": { "$ref": "#/components/responses/NotFound" }, "422": { "$ref": "#/components/responses/Validation" } } } }, "/api/v1/{orgSlug}/recordings/{recordingId}/comments": { "get": { "operationId": "listComments", "tags": [ "Recordings" ], "summary": "List a thread's comments", "description": "Comments work on any commentable recording, one endpoint for all types (card steps take none). Rows carry `creator_name`/`creator_image` so threads render bylines directly.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" }, { "name": "recordingId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "The comments with author bylines", "content": { "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/CommentRow" } } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "404": { "$ref": "#/components/responses/NotFound" } } }, "post": { "operationId": "createComment", "tags": [ "Recordings" ], "summary": "Comment on a recording", "description": "Needs `content` or `content_html`. Closed threads, archived/trashed parents, and uncommentable types (card steps) refuse with 404. Edits afterwards are author-only via PATCH /recordings/{comment_id} (stamps meta.editedAt); comments trash via PUT .../status/trashed, never DELETE.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" }, { "name": "recordingId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "content": { "type": "string", "maxLength": 50000, "description": "Plain text; converted to safe HTML server-side" }, "content_html": { "type": "string", "maxLength": 100000, "description": "Rich HTML; sanitized server-side. Wins over content if both sent" } } } } } }, "responses": { "201": { "description": "The created comment", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Recording" } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "404": { "$ref": "#/components/responses/NotFound" }, "422": { "$ref": "#/components/responses/Validation" } } } }, "/api/v1/{orgSlug}/recordings/{recordingId}/comments-closed": { "put": { "operationId": "setCommentsClosed", "tags": [ "Recordings" ], "summary": "Close or reopen comments", "description": "Author or admin+ only (others 404). Closed threads reject comments AND cheers.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" }, { "name": "recordingId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "closed" ], "properties": { "closed": { "type": "boolean" } } } } } }, "responses": { "200": { "description": "The updated recording", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Recording" } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "404": { "$ref": "#/components/responses/NotFound" }, "422": { "$ref": "#/components/responses/Validation" } } } }, "/api/v1/{orgSlug}/recordings/{recordingId}/completion": { "put": { "operationId": "completeRecording", "tags": [ "Recordings" ], "summary": "Mark done", "description": "Completion (todos, cards, steps) is orthogonal to status; the payload carries provenance (`completed_at`, `completed_by_id`). Completing a to-do notifies its subscribers, excluding whoever checked it off; completing a subtask never touches the parent and pings the parent to-do's subscribers plus the subtask's own. Cards: completion IS the Done column, so PUT moves the card to its board's Done column (top slot) and stamps it. Completing a recurring primary immediately spawns the next occurrence.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" }, { "name": "recordingId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "The completed recording", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Recording" } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "404": { "$ref": "#/components/responses/NotFound" } } }, "delete": { "operationId": "reopenRecording", "tags": [ "Recordings" ], "summary": "Reopen", "description": "Clears the completion stamp. Cards move back to the column they were completed from (else Triage).", "parameters": [ { "$ref": "#/components/parameters/orgSlug" }, { "name": "recordingId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "The reopened recording", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Recording" } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "404": { "$ref": "#/components/responses/NotFound" } } } }, "/api/v1/{orgSlug}/recordings/{recordingId}/copy": { "post": { "operationId": "copyRecording", "tags": [ "Recordings" ], "summary": "Deep-copy under a new parent", "description": "Deep copy under the new parent, any project. Comments stay behind unless `include_comments: true` (included comments keep their original author and timestamps). Parent rules and subtask depth apply to the target; copying a draft yields another (unscheduled) draft.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" }, { "name": "recordingId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "parent_id" ], "properties": { "parent_id": { "type": "string", "format": "uuid" }, "include_comments": { "type": "boolean" } } } } } }, "responses": { "201": { "description": "The copy", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Recording" } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "404": { "$ref": "#/components/responses/NotFound" }, "422": { "$ref": "#/components/responses/Validation" } } } }, "/api/v1/{orgSlug}/recordings/{recordingId}/events": { "get": { "operationId": "listRecordingEvents", "tags": [ "Recordings" ], "summary": "Read the change log", "description": "The per-recording \"History of changes\": every lifecycle event, oldest first (created, completed, moved with fromParentTitle/toParentTitle captured at move time, version_added, visibility/hold/comment toggles). Commented rows stay out: the thread is that history. Each row includes its cheers.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" }, { "name": "recordingId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "The change log, oldest first", "content": { "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/RecordingEvent" } } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "404": { "$ref": "#/components/responses/NotFound" } } } }, "/api/v1/{orgSlug}/recordings/{recordingId}/hold": { "put": { "operationId": "setRecordingHold", "tags": [ "Recordings" ], "summary": "Toggle On Hold", "description": "On Hold is per column. On a card: parks it in its column's ON HOLD section WITHOUT changing its column (the column's section must be enabled; cards in Triage/Not Now/Done refuse; moving the card to another column releases it). On a column: enables/disables the section itself (disabling releases every held card; Triage/Not Now/Done refuse).", "parameters": [ { "$ref": "#/components/parameters/orgSlug" }, { "name": "recordingId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "on_hold" ], "properties": { "on_hold": { "type": "boolean" } } } } } }, "responses": { "200": { "description": "The updated card or column", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Recording" } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "404": { "$ref": "#/components/responses/NotFound" }, "422": { "$ref": "#/components/responses/Validation" } } } }, "/api/v1/{orgSlug}/recordings/{recordingId}/opt-out": { "put": { "operationId": "optOutQuestion", "tags": [ "Recordings" ], "summary": "Opt out of a check-in question", "description": "Per-person \"stop asking me\" on a check-in question; affects only the caller.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" }, { "name": "recordingId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "opted_out" ], "properties": { "opted_out": { "type": "boolean" } } } } } }, "responses": { "200": { "description": "The new opt-out state", "content": { "application/json": { "schema": { "type": "object", "required": [ "opted_out" ], "properties": { "opted_out": { "type": "boolean" } } } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "404": { "$ref": "#/components/responses/NotFound" }, "422": { "$ref": "#/components/responses/Validation" } } } }, "/api/v1/{orgSlug}/recordings/{recordingId}/pause": { "put": { "operationId": "pauseQuestion", "tags": [ "Recordings" ], "summary": "Pause or resume a check-in question", "description": "Stop/resume asking a check-in question; the ask sweep skips paused questions. The recording must be a question (404 otherwise).", "parameters": [ { "$ref": "#/components/parameters/orgSlug" }, { "name": "recordingId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "paused" ], "properties": { "paused": { "type": "boolean" } } } } } }, "responses": { "200": { "description": "The new pause state", "content": { "application/json": { "schema": { "type": "object", "required": [ "paused" ], "properties": { "paused": { "type": "boolean" } } } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "404": { "$ref": "#/components/responses/NotFound" }, "422": { "$ref": "#/components/responses/Validation" } } } }, "/api/v1/{orgSlug}/recordings/{recordingId}/pin": { "put": { "operationId": "pinRecording", "tags": [ "Recordings" ], "summary": "Pin a message", "description": "Pins a message to the top of its board; messages only, and at most 10 pinned per board (the 11th refuses).", "parameters": [ { "$ref": "#/components/parameters/orgSlug" }, { "name": "recordingId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "The pinned message", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Recording" } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "404": { "$ref": "#/components/responses/NotFound" } } }, "delete": { "operationId": "unpinRecording", "tags": [ "Recordings" ], "summary": "Unpin a message", "description": "Clears the pin.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" }, { "name": "recordingId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "The unpinned message", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Recording" } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "404": { "$ref": "#/components/responses/NotFound" } } } }, "/api/v1/{orgSlug}/recordings/{recordingId}/position": { "put": { "operationId": "moveRecording", "tags": [ "Recordings" ], "summary": "Move and/or reorder", "description": "Kanban moves, drag reorders, and moving a to-do into/out of/between lists and groups (a group is a todolist, so `parent_id` is the destination list or group; the todos container = loose). `position` is fractional: drops send the midpoint between neighbors. Cross-project parents move the whole subtree; a parent inside the recording's own subtree (folder/group cycles) returns 404. Position-only calls (pure reorders) don't log a moved activity event; re-parents do. Moving a card across the Done boundary completes it (in) or clears the stamp (out).", "parameters": [ { "$ref": "#/components/parameters/orgSlug" }, { "name": "recordingId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "parent_id": { "type": "string", "format": "uuid", "description": "The destination container; omit to reorder in place" }, "position": { "type": "number", "description": "Fractional sort position (drag surfaces send the midpoint between neighbors)" } } } } } }, "responses": { "200": { "description": "The moved recording", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Recording" } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "404": { "$ref": "#/components/responses/NotFound" }, "422": { "$ref": "#/components/responses/Validation" } } } }, "/api/v1/{orgSlug}/recordings/{recordingId}/progress": { "get": { "operationId": "getProgressChart", "tags": [ "Progress & health" ], "summary": "Get the progress chart", "description": "The todos container's progress chart. Charts track to-do lists, not items: each tracked list is a dot positioned 0 to 1 along the arc (left = working it out, right = getting it done) with a stable `color`. Returns the `enabled` flag, the dots (`lists`), saved `updates` newest first (each with its snapshot `positions`), and the legacy move `history`.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" }, { "name": "recordingId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" }, "description": "The project's todos container id" } ], "responses": { "200": { "description": "The chart", "content": { "application/json": { "schema": { "type": "object", "required": [ "enabled", "updated_at", "lists", "updates", "history" ], "properties": { "enabled": { "type": "boolean", "description": "True while at least one list is tracked" }, "updated_at": { "type": [ "string", "null" ], "format": "date-time", "description": "When the newest saved update landed" }, "lists": { "type": "array", "items": { "$ref": "#/components/schemas/ProgressDot" } }, "updates": { "type": "array", "description": "Saved progress updates, newest first", "items": { "type": "object", "properties": { "id": { "type": "string", "format": "uuid", "description": "The progress_update recording's id" }, "note": { "type": [ "string", "null" ], "description": "Sanitized HTML" }, "creator_name": { "type": [ "string", "null" ] }, "created_at": { "type": "string", "format": "date-time" }, "visible_to_clients": { "type": "boolean" }, "positions": { "type": "array", "items": { "$ref": "#/components/schemas/ProgressSnapshotDot" }, "description": "The whole chart as it stood when the update was saved" } } } }, "history": { "type": "array", "description": "Legacy per-move log, newest first", "items": { "type": "object", "properties": { "detail": { "type": [ "object", "null" ], "additionalProperties": true }, "actor_name": { "type": [ "string", "null" ] }, "created_at": { "type": "string", "format": "date-time" } } } } } } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "404": { "$ref": "#/components/responses/NotFound" } } }, "put": { "operationId": "updateProgress", "tags": [ "Progress & health" ], "summary": "Move a dot or change tracking settings", "description": "Two shapes depending on the target. On a todolist: `{position, note?}` moves that list's dot (0 to 1; null stops tracking; the color assigned at first track is kept across re-tracks) and returns `{ok: true}`. On the todos container: bulk settings `{tracked: [ids], untracked: [ids]}` (Basecamp's hill-settings shape) start or stop tracking whole lists and return the refreshed chart.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" }, { "name": "recordingId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" }, "description": "A todolist id (move one dot) or the todos container id (bulk settings)" } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "oneOf": [ { "type": "object", "title": "Move one list's dot (todolist target)", "required": [ "position" ], "properties": { "position": { "type": [ "number", "null" ], "minimum": 0, "maximum": 1, "description": "The dot's new position; null stops tracking" }, "note": { "type": "string", "maxLength": 2000 } } }, { "type": "object", "title": "Bulk tracking settings (todos target)", "properties": { "tracked": { "type": "array", "items": { "type": "string", "format": "uuid" }, "description": "Todolist ids to start tracking (at position 0)" }, "untracked": { "type": "array", "items": { "type": "string", "format": "uuid" }, "description": "Todolist ids to stop tracking" } } } ] } } } }, "responses": { "200": { "description": "For a todolist target `{ok: true}`; for the todos target the refreshed chart", "content": { "application/json": { "schema": { "oneOf": [ { "type": "object", "title": "Dot moved", "required": [ "ok" ], "properties": { "ok": { "type": "boolean" } } }, { "type": "object", "title": "Refreshed chart", "required": [ "enabled", "lists" ], "properties": { "enabled": { "type": "boolean" }, "lists": { "type": "array", "items": { "$ref": "#/components/schemas/ProgressDot" } } } } ] } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "404": { "$ref": "#/components/responses/NotFound" }, "422": { "$ref": "#/components/responses/Validation" } } } }, "/api/v1/{orgSlug}/recordings/{recordingId}/progress/updates": { "post": { "operationId": "createProgressUpdate", "tags": [ "Progress & health" ], "summary": "Save a progress update", "description": "Basecamp's Save-this-update: apply the moved dots, then snapshot the whole chart into a `progress_update` recording (commentable and cheerable). `notify` is the exact subscriber set, M5 semantics: everyone, none, or a membership id list. Trashing an update (PUT /recordings/{id}/status/trashed) rolls its moves back; dot positions derive from the chain of active updates.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" }, { "name": "recordingId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" }, "description": "The project's todos container id" } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "positions": { "type": "object", "additionalProperties": { "type": "number", "minimum": 0, "maximum": 1 }, "description": "Map of todolist id to its new dot position; default empty (snapshot only)" }, "note": { "type": "string", "maxLength": 50000, "description": "Rich HTML, sanitized server-side" }, "notify": { "oneOf": [ { "type": "string", "enum": [ "everyone", "none" ] }, { "type": "array", "items": { "type": "string", "format": "uuid" } } ], "description": "Who is notified and becomes the update's exact subscriber set" }, "visible_to_clients": { "type": "boolean" } } } } } }, "responses": { "201": { "description": "The saved update", "content": { "application/json": { "schema": { "type": "object", "required": [ "id", "created_at" ], "properties": { "id": { "type": "string", "format": "uuid" }, "note": { "type": [ "string", "null" ] }, "created_at": { "type": "string", "format": "date-time" }, "positions": { "type": "array", "items": { "$ref": "#/components/schemas/ProgressSnapshotDot" } } } } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "404": { "$ref": "#/components/responses/NotFound" }, "422": { "$ref": "#/components/responses/Validation" } } } }, "/api/v1/{orgSlug}/recordings/{recordingId}/public-link": { "get": { "operationId": "getPublicLink", "tags": [ "Recordings" ], "summary": "Get a recording's public link", "description": "Whether the recording is shared read-only at /public/{token}, and where.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" }, { "name": "recordingId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "The link state", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PublicLinkState" } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "404": { "$ref": "#/components/responses/NotFound" } } }, "put": { "operationId": "publishPublicLink", "tags": [ "Recordings" ], "summary": "Publish a public link", "description": "Shares one message, document, or file read-only at /public/{token}: a 256-bit token, revocable, noindex, sanitized content only, no comments, no subtree. Idempotent while a link is active. Clients can't publish, and when the org restricts public links only owners and admins can (403).", "parameters": [ { "$ref": "#/components/parameters/orgSlug" }, { "name": "recordingId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "Published; `url` is the capability URL", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PublicLinkState" } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "403": { "$ref": "#/components/responses/Forbidden" }, "404": { "$ref": "#/components/responses/NotFound" } } }, "delete": { "operationId": "revokePublicLink", "tags": [ "Recordings" ], "summary": "Unpublish a public link", "description": "Revokes the link; the public URL dies immediately.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" }, { "name": "recordingId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "Unpublished", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PublicLinkState" } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "404": { "$ref": "#/components/responses/NotFound" } } } }, "/api/v1/{orgSlug}/recordings/{recordingId}/publish": { "post": { "operationId": "publishRecording", "tags": [ "Recordings" ], "summary": "Publish a draft now", "description": "Publishes one of YOUR drafts immediately; anyone but the draft's creator gets 404. Publishing fires the create-time notifications, honoring the draft's `notify` choice.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" }, { "name": "recordingId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "The published recording", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Recording" } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "404": { "$ref": "#/components/responses/NotFound" } } } }, "/api/v1/{orgSlug}/recordings/{recordingId}/questions": { "get": { "operationId": "listQuestions", "tags": [ "Check-ins" ], "summary": "List a project's check-in questions", "description": "Questions under the project's check_ins container. Rows carry `schedule`, `askees`, `answer_count`, and `recent_answer_count` (last 24 hours).", "parameters": [ { "$ref": "#/components/parameters/orgSlug" }, { "name": "recordingId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" }, "description": "The project's check_ins container id" } ], "responses": { "200": { "description": "The questions", "content": { "application/json": { "schema": { "type": "array", "items": { "allOf": [ { "$ref": "#/components/schemas/Recording" }, { "type": "object", "properties": { "schedule": { "$ref": "#/components/schemas/CheckInSchedule" }, "askees": { "type": "array", "items": { "$ref": "#/components/schemas/CheckInAskee" } }, "answer_count": { "type": "integer" }, "recent_answer_count": { "type": "integer", "description": "Answers in the last 24 hours" } } } ] } } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "404": { "$ref": "#/components/responses/NotFound" } } }, "post": { "operationId": "createQuestion", "tags": [ "Check-ins" ], "summary": "Create a check-in question", "description": "Askees (`membership_ids`) are auto-subscribed to answers and notified per occurrence. Schedule semantics: daily takes `days` (0=Sunday to 6=Saturday, default Monday to Friday, weekends allowed); weekly and every_other_week take `day_of_week` (the alternation anchors at creation); monthly means the first `day_of_week` of the month, or a fixed `day_of_month`. Clients can't create check-ins (403). Question edits go through the generic PATCH /recordings/{id}: `title`, plus question-only `schedule` and `membership_ids`.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" }, { "name": "recordingId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" }, "description": "The project's check_ins container id" } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "title", "schedule", "membership_ids" ], "properties": { "title": { "type": "string", "minLength": 1, "maxLength": 10000 }, "schedule": { "$ref": "#/components/schemas/CheckInSchedule" }, "membership_ids": { "type": "array", "items": { "type": "string", "format": "uuid" }, "minItems": 1, "description": "Who gets asked" }, "visible_to_clients": { "type": "boolean" } } } } } }, "responses": { "201": { "description": "The created question recording", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Recording" } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "403": { "$ref": "#/components/responses/Forbidden" }, "404": { "$ref": "#/components/responses/NotFound" }, "422": { "$ref": "#/components/responses/Validation" } } } }, "/api/v1/{orgSlug}/recordings/{recordingId}/references": { "get": { "operationId": "listRecordingReferences", "tags": [ "Recordings" ], "summary": "List what links here", "description": "Recordings whose content links to this one, enriched for the References tab (project_name, creator_name, referenced_at, excerpt) and filtered to what the caller can access: project access, clients see client-visible only, DM sources show to participants only.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" }, { "name": "recordingId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "The referencing recordings", "content": { "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/RecordingReference" } } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "404": { "$ref": "#/components/responses/NotFound" } } } }, "/api/v1/{orgSlug}/recordings/{recordingId}/status/{status}": { "put": { "operationId": "setRecordingStatus", "tags": [ "Recordings" ], "summary": "Archive, trash, or restore", "description": "Works on ANY type; trash is recoverable. Tool containers refuse (except a folder with a parent, which archives and trashes with its contents cascading); drafts must publish before archiving; restoring a trashed draft returns it to drafted, never published. The governance restrict-content-actions toggle can refuse non-authors (403).", "parameters": [ { "$ref": "#/components/parameters/orgSlug" }, { "name": "recordingId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } }, { "name": "status", "in": "path", "required": true, "schema": { "type": "string", "enum": [ "active", "archived", "trashed" ] } } ], "responses": { "200": { "description": "The recording in its new state", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Recording" } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "403": { "$ref": "#/components/responses/Forbidden" }, "404": { "$ref": "#/components/responses/NotFound" }, "422": { "$ref": "#/components/responses/Validation" } } } }, "/api/v1/{orgSlug}/recordings/{recordingId}/subscribers": { "get": { "operationId": "listSubscribers", "tags": [ "Recordings" ], "summary": "List who gets notified", "parameters": [ { "$ref": "#/components/parameters/orgSlug" }, { "name": "recordingId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "The subscriber set", "content": { "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/RecordingPersonRow" } } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "404": { "$ref": "#/components/responses/NotFound" } } }, "put": { "operationId": "updateSubscribers", "tags": [ "Recordings" ], "summary": "Add or remove subscribers", "description": "Deltas, not replacement: `add` and `remove` are membership id lists. `notify: \"now\"` pings added people immediately; `\"next\"` stays quiet until the next comment. Returns the updated subscriber set.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" }, { "name": "recordingId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "add": { "type": "array", "items": { "type": "string", "format": "uuid" } }, "remove": { "type": "array", "items": { "type": "string", "format": "uuid" } }, "notify": { "type": "string", "enum": [ "now", "next" ] } } } } } }, "responses": { "200": { "description": "The updated subscriber set", "content": { "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/RecordingPersonRow" } } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "404": { "$ref": "#/components/responses/NotFound" }, "422": { "$ref": "#/components/responses/Validation" } } } }, "/api/v1/{orgSlug}/recordings/{recordingId}/subscription": { "get": { "operationId": "getSubscription", "tags": [ "Recordings" ], "summary": "Check your subscription", "description": "Your subscription on the recording plus the full subscriber list. On a card column this is \"watch this column\" (subscribers hear about new cards and column comments); on a chat room it is the chat bell (\"Notify me when people chat\").", "parameters": [ { "$ref": "#/components/parameters/orgSlug" }, { "name": "recordingId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "Your subscription state", "content": { "application/json": { "schema": { "type": "object", "required": [ "subscribed", "subscribers" ], "properties": { "subscribed": { "type": "boolean" }, "subscribers": { "type": "array", "items": { "type": "object", "required": [ "membership_id", "name" ], "properties": { "membership_id": { "type": "string", "format": "uuid" }, "name": { "type": "string" } } } } } } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "404": { "$ref": "#/components/responses/NotFound" } } }, "put": { "operationId": "subscribeRecording", "tags": [ "Recordings" ], "summary": "Subscribe yourself", "parameters": [ { "$ref": "#/components/parameters/orgSlug" }, { "name": "recordingId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "Subscribed", "content": { "application/json": { "schema": { "type": "object", "required": [ "subscribed" ], "properties": { "subscribed": { "type": "boolean", "const": true } } } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "404": { "$ref": "#/components/responses/NotFound" } } }, "delete": { "operationId": "unsubscribeRecording", "tags": [ "Recordings" ], "summary": "Unsubscribe yourself", "parameters": [ { "$ref": "#/components/parameters/orgSlug" }, { "name": "recordingId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "Unsubscribed", "content": { "application/json": { "schema": { "type": "object", "required": [ "subscribed" ], "properties": { "subscribed": { "type": "boolean", "const": false } } } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "404": { "$ref": "#/components/responses/NotFound" } } } }, "/api/v1/{orgSlug}/recordings/{recordingId}/uploads": { "get": { "operationId": "listChatUploads", "tags": [ "Files" ], "summary": "List files posted into a chat room or dm", "description": "Every file posted into the conversation, newest first (Basecamp's GET /chats/{id}/uploads.json). Chat rooms and dms only: any other recording type returns 404. Folder files are listed via GET /recordings/{folderId}/children?type=upload.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" }, { "name": "recordingId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" }, "description": "A chat or dm recording id" } ], "responses": { "200": { "description": "The files, newest first", "content": { "application/json": { "schema": { "type": "array", "items": { "type": "object", "required": [ "id", "filename", "byte_size", "content_type", "posted_at" ], "properties": { "id": { "type": "string", "format": "uuid", "description": "The upload recording's id" }, "title": { "type": [ "string", "null" ] }, "filename": { "type": "string" }, "byte_size": { "type": "integer" }, "content_type": { "type": "string" }, "posted_at": { "type": "string", "format": "date-time" }, "posted_by": { "type": [ "string", "null" ] } } } } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "404": { "$ref": "#/components/responses/NotFound" } } }, "post": { "operationId": "createUpload", "tags": [ "Files" ], "summary": "Upload a file", "description": "Multipart upload into a project folder (or a dm, for files posted into a conversation). `file` is required and capped at 50 MB. `notify` follows the M5 semantics: \"everyone\", \"none\", or a JSON array of membership ids; omitted means silent, so rich-text attachments never fan out. Returns 402 when the plan's storage cap is reached. Rename later via PATCH /recordings/{id} with `base_name` (the extension is kept).", "parameters": [ { "$ref": "#/components/parameters/orgSlug" }, { "name": "recordingId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" }, "description": "The destination folder (or dm) recording id" } ], "requestBody": { "required": true, "content": { "multipart/form-data": { "schema": { "type": "object", "required": [ "file" ], "properties": { "file": { "type": "string", "format": "binary", "description": "The file bytes, up to 50 MB, non-empty" }, "description_html": { "type": "string", "description": "Rich notes, sanitized server-side" }, "notify": { "type": "string", "description": "\"everyone\", \"none\", or a JSON array of membership ids; omitted means nobody is notified" } } } } } }, "responses": { "201": { "description": "The created upload recording", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Recording" } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "402": { "$ref": "#/components/responses/PlanLimit" }, "404": { "$ref": "#/components/responses/NotFound" }, "422": { "$ref": "#/components/responses/Validation" } } } }, "/api/v1/{orgSlug}/recordings/{recordingId}/visibility": { "put": { "operationId": "setRecordingVisibility", "tags": [ "Recordings" ], "summary": "Toggle client visibility", "description": "Toggles the client-visibility flag for the recording AND its subtree. Loose to-dos are never visible to clients, so toggling one returns 403; comments always follow their thread.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" }, { "name": "recordingId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "visible_to_clients" ], "properties": { "visible_to_clients": { "type": "boolean" } } } } } }, "responses": { "200": { "description": "The updated recording", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Recording" } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "403": { "$ref": "#/components/responses/Forbidden" }, "404": { "$ref": "#/components/responses/NotFound" }, "422": { "$ref": "#/components/responses/Validation" } } } }, "/api/v1/{orgSlug}/recordings/{recordingId}/watch": { "get": { "operationId": "getWatch", "tags": [ "Recordings" ], "summary": "Check your watch", "description": "Personal \"notify me when new to-dos are added to this list\": watch a todolist (covers its groups) or the todos container (loose to-dos). New to-dos fan out a todo_added notification to watchers.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" }, { "name": "recordingId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "Your watch state", "content": { "application/json": { "schema": { "type": "object", "required": [ "watching" ], "properties": { "watching": { "type": "boolean" } } } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "404": { "$ref": "#/components/responses/NotFound" } } }, "put": { "operationId": "watchRecording", "tags": [ "Recordings" ], "summary": "Watch for new to-dos", "parameters": [ { "$ref": "#/components/parameters/orgSlug" }, { "name": "recordingId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "Watching", "content": { "application/json": { "schema": { "type": "object", "required": [ "watching" ], "properties": { "watching": { "type": "boolean", "const": true } } } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "404": { "$ref": "#/components/responses/NotFound" } } }, "delete": { "operationId": "unwatchRecording", "tags": [ "Recordings" ], "summary": "Stop watching", "parameters": [ { "$ref": "#/components/parameters/orgSlug" }, { "name": "recordingId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "No longer watching", "content": { "application/json": { "schema": { "type": "object", "required": [ "watching" ], "properties": { "watching": { "type": "boolean", "const": false } } } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "404": { "$ref": "#/components/responses/NotFound" } } } }, "/api/v1/{orgSlug}/recordings/batch": { "post": { "operationId": "batchRecordings", "tags": [ "Recordings" ], "summary": "Apply one bulk verb to a selection", "description": "One round trip applies `action` to every id, in order; every id passes the same per-recording guards as the single endpoints, and missing per-action fields return 422. Actions: `assign` REPLACES each recording's assignee set (`assignee_ids: []` removes all assignments); `due` sets a day, a `starts_on`..`due_on` range, or both null for no due date; `shift_dates` moves `starts_on` AND `due_on` forward by `amount` x `unit`, skipping undated recordings (`count` = rows actually shifted); `move` re-parents (arrival order = `ids` order); `copy` deep-copies (returns the copies' `ids`); `status` archives, trashes, or restores; `color` tints Docs & Files items (`white` clears); `group` creates a group in the selection's list, moves the selection in, and returns `group_id`.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "ids", "action" ], "properties": { "ids": { "type": "array", "items": { "type": "string", "format": "uuid" }, "minItems": 1, "maxItems": 100 }, "action": { "type": "string", "enum": [ "assign", "due", "shift_dates", "move", "copy", "status", "color", "group" ] }, "assignee_ids": { "type": "array", "items": { "type": "string", "format": "uuid" }, "description": "assign: the replacement assignee set ([] removes all assignments)" }, "due_on": { "type": [ "string", "null" ], "format": "date", "description": "due: a day, or the end of a starts_on..due_on range; null clears" }, "starts_on": { "type": [ "string", "null" ], "format": "date", "description": "due: range start; must not be after due_on" }, "amount": { "type": "integer", "minimum": 1, "maximum": 30, "description": "shift_dates: how far forward" }, "unit": { "type": "string", "enum": [ "days", "weekdays", "weeks", "months" ], "description": "shift_dates: the step unit" }, "parent_id": { "type": "string", "format": "uuid", "description": "move / copy: the destination container" }, "include_comments": { "type": "boolean", "description": "copy: bring each thread along (original authors and timestamps)" }, "status": { "type": "string", "enum": [ "active", "archived", "trashed" ], "description": "status: the target lifecycle state" }, "color": { "type": "string", "enum": [ "white", "red", "orange", "yellow", "green", "blue", "aqua", "purple", "gray", "pink", "brown" ], "description": "color: Docs & Files tint (white clears); also the optional group color" }, "title": { "type": "string", "maxLength": 10000, "description": "group: the new group's name" } } } } } }, "responses": { "200": { "description": "What the bulk verb touched", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/RecordingBatchResult" } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "402": { "$ref": "#/components/responses/PlanLimit" }, "403": { "$ref": "#/components/responses/Forbidden" }, "404": { "$ref": "#/components/responses/NotFound" }, "422": { "$ref": "#/components/responses/Validation" } } } }, "/api/v1/{orgSlug}/reports/calendar/upcoming": { "get": { "operationId": "getUpcomingCalendarReport", "tags": [ "Calendar" ], "summary": "Upcoming dates report", "description": "Everything dated inside the window: one-time events, expanded recurring occurrences, and dated open assignables (to-dos, cards, and card steps, which the calendar's own task arm lacks). Both window parameters are required; a missing, malformed, or inverted window is a 400. Team only: clients receive 403.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" }, { "name": "window_starts_on", "in": "query", "required": true, "schema": { "type": "string", "format": "date" } }, { "name": "window_ends_on", "in": "query", "required": true, "schema": { "type": "string", "format": "date" } } ], "responses": { "200": { "description": "The window's report", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UpcomingReport" } } } }, "400": { "description": "Missing, malformed, or inverted window (code bad_request)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "403": { "$ref": "#/components/responses/Forbidden" } } } }, "/api/v1/{orgSlug}/reports/health": { "get": { "operationId": "getHealthReport", "tags": [ "Search & reports" ], "summary": "Org-wide project health gauges", "description": "Every accessible active project with its health gauge on and a needle set, most recently updated first (Basecamp's Mission Control). Team only: clients receive 403.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" } ], "responses": { "200": { "description": "The gauges", "content": { "application/json": { "schema": { "type": "object", "required": [ "gauges" ], "properties": { "gauges": { "type": "array", "items": { "$ref": "#/components/schemas/HealthReportGauge" } } } } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "403": { "$ref": "#/components/responses/Forbidden" } } } }, "/api/v1/{orgSlug}/reports/todos/assigned": { "get": { "operationId": "listAssignablePeople", "tags": [ "Search & reports" ], "summary": "Everyone who can have work assigned", "description": "The people directory the assigned-work report starts from; feed a `membership_id` into /reports/todos/assigned/{membershipId} for that person's open work. Team only: clients receive 403.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" } ], "responses": { "200": { "description": "The assignable people", "content": { "application/json": { "schema": { "type": "object", "required": [ "people" ], "properties": { "people": { "type": "array", "items": { "$ref": "#/components/schemas/ReportPerson" } } } } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "403": { "$ref": "#/components/responses/Forbidden" } } } }, "/api/v1/{orgSlug}/reports/todos/assigned/{membershipId}": { "get": { "operationId": "getPersonAssignments", "tags": [ "Search & reports" ], "summary": "One person's open assignments", "description": "That person's open, pending assignments across the projects the caller can see. `group_by=bucket` (default) sorts by project, `group_by=date` by due date; the response echoes the grouping in `grouped_by`. Team only: clients receive 403.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" }, { "name": "membershipId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } }, { "name": "group_by", "in": "query", "schema": { "type": "string", "enum": [ "bucket", "date" ], "default": "bucket" }, "description": "`bucket`: by project; `date`: by due date" } ], "responses": { "200": { "description": "The person and their open work", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AssignedReport" } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "403": { "$ref": "#/components/responses/Forbidden" }, "404": { "$ref": "#/components/responses/NotFound" } } } }, "/api/v1/{orgSlug}/reports/todos/overdue": { "get": { "operationId": "getOverdueTodosReport", "tags": [ "Search & reports" ], "summary": "All overdue to-dos, by lateness", "description": "Every overdue open to-do across accessible projects, grouped into the four Basecamp lateness buckets. Each row carries `days_late` and its assignee chips. Team only: clients receive 403.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" } ], "responses": { "200": { "description": "The four lateness buckets", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/OverdueReport" } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "403": { "$ref": "#/components/responses/Forbidden" } } } }, "/api/v1/{orgSlug}/roadmap/markers": { "get": { "operationId": "listRoadmapMarkers", "tags": [ "Projects" ], "summary": "List roadmap markers", "description": "Org-wide roadmap markers in date order (bc3 lineup_markers conventions). Team-readable; clients receive 403.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" } ], "responses": { "200": { "description": "Markers in date order", "content": { "application/json": { "schema": { "type": "object", "required": [ "markers" ], "properties": { "markers": { "type": "array", "items": { "$ref": "#/components/schemas/RoadmapMarker" } } } } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "403": { "$ref": "#/components/responses/Forbidden" }, "404": { "$ref": "#/components/responses/NotFound" } } }, "post": { "operationId": "createRoadmapMarker", "tags": [ "Projects" ], "summary": "Create a roadmap marker", "description": "Owners and admins only; others receive 403. Both fields are required; a missing or invalid name or date is a 400.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "name", "date" ], "properties": { "name": { "type": "string" }, "date": { "type": "string", "format": "date", "description": "YYYY-MM-DD" } } } } } }, "responses": { "201": { "description": "The created marker", "content": { "application/json": { "schema": { "type": "object", "required": [ "marker" ], "properties": { "marker": { "$ref": "#/components/schemas/RoadmapMarker" } } } } } }, "400": { "description": "Missing or invalid name or date", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "403": { "$ref": "#/components/responses/Forbidden" }, "404": { "$ref": "#/components/responses/NotFound" } } } }, "/api/v1/{orgSlug}/roadmap/markers/{markerId}": { "put": { "operationId": "updateRoadmapMarker", "tags": [ "Projects" ], "summary": "Rename or move a marker", "description": "Owners and admins only.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" }, { "name": "markerId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "name": { "type": "string" }, "date": { "type": "string", "format": "date", "description": "YYYY-MM-DD" } } } } } }, "responses": { "200": { "description": "The updated marker", "content": { "application/json": { "schema": { "type": "object", "required": [ "marker" ], "properties": { "marker": { "$ref": "#/components/schemas/RoadmapMarker" } } } } } }, "400": { "description": "Invalid name or date", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "403": { "$ref": "#/components/responses/Forbidden" }, "404": { "$ref": "#/components/responses/NotFound" } } }, "delete": { "operationId": "deleteRoadmapMarker", "tags": [ "Projects" ], "summary": "Delete a marker", "description": "Permanent: markers have no trash lifecycle. Owners and admins only.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" }, { "name": "markerId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "204": { "description": "Deleted" }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "403": { "$ref": "#/components/responses/Forbidden" }, "404": { "$ref": "#/components/responses/NotFound" } } } }, "/api/v1/{orgSlug}/search": { "get": { "operationId": "search", "tags": [ "Search & reports" ], "summary": "Full-text search", "description": "Postgres FTS with word-prefix matching (as-you-type queries find whole words), relevance-ranked with a recency boost. Results honor your visibility: accessible projects only, client rules applied. Rows are enriched with project, creator, and parent context plus `excerpt`/`title_excerpt`, tag-stripped text with matches wrapped in ``. Fetch valid `type`/`file_type` values from /search/metadata rather than hardcoding.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" }, { "name": "q", "in": "query", "required": true, "schema": { "type": "string", "minLength": 1 } }, { "name": "project_id", "in": "query", "schema": { "type": "string", "format": "uuid" } }, { "name": "type", "in": "query", "schema": { "type": "string" }, "description": "One recording type; valid keys come from /search/metadata" }, { "name": "creator_id", "in": "query", "schema": { "type": "string", "format": "uuid" } }, { "name": "after", "in": "query", "schema": { "type": "string", "format": "date" }, "description": "Created on or after this date" }, { "name": "before", "in": "query", "schema": { "type": "string", "format": "date" }, "description": "Created on or before this date" }, { "name": "archived", "in": "query", "schema": { "type": "string", "enum": [ "true", "false", "1", "0" ] }, "description": "Include archived projects and recordings" }, { "name": "exclude_chat", "in": "query", "schema": { "type": "string", "enum": [ "true", "false", "1", "0" ] }, "description": "Drop chat and DM lines" }, { "name": "file_type", "in": "query", "schema": { "type": "string", "enum": [ "image", "audio", "video", "pdf" ] }, "description": "Narrow upload results by kind" }, { "name": "since", "in": "query", "schema": { "type": "string", "enum": [ "last_year", "forever" ] }, "description": "`last_year`: a 12-month window" }, { "name": "sort", "in": "query", "schema": { "type": "string", "enum": [ "relevance", "recent" ], "default": "relevance" } }, { "$ref": "#/components/parameters/page" }, { "$ref": "#/components/parameters/perPage" } ], "responses": { "200": { "description": "Matching recordings (default page size 50)", "content": { "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/SearchResult" } } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "422": { "$ref": "#/components/responses/Validation" } } } }, "/api/v1/{orgSlug}/search/metadata": { "get": { "operationId": "getSearchMetadata", "tags": [ "Search & reports" ], "summary": "Valid search filter options", "description": "The `type` and `file_type` filter vocabularies for /search, as key/label pairs (a null key means no filter). Fetch these rather than hardcoding: the vocabulary grows with the product.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" } ], "responses": { "200": { "description": "Filter options", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SearchMetadata" } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" } } } }, "/api/v1/{orgSlug}/settings": { "get": { "operationId": "getOrgSettings", "tags": [ "Organizations" ], "summary": "Read the org settings", "parameters": [ { "$ref": "#/components/parameters/orgSlug" } ], "responses": { "200": { "description": "The settings document", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/OrgSettings" } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "404": { "$ref": "#/components/responses/NotFound" } } }, "patch": { "operationId": "updateOrgSettings", "tags": [ "Organizations" ], "summary": "Update the org settings", "description": "Tool names, history retention, voice notes, and the five governance flags: admin and up. `require_two_factor` is owner-only. Members receive 403. Governance flags bite at the guarded writes, not here: restricted project or people edits, moves, archives, and trashes answer 403 for non-creator members; public-link creation answers 403 for non-admins; comment and chat edits answer 403 for everyone (and deletes for non-admins) after 15 minutes.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "require_two_factor": { "type": "boolean", "description": "Owner-only. When on, non-client members without TOTP are funneled into 2FA setup at org entry; clients are exempt" }, "history_retention_days": { "type": [ "integer", "null" ], "minimum": 1, "maximum": 3650, "description": "null keeps chat and DM history forever; a number hard-deletes older lines, permanently" }, "tool_names": { "type": "object", "description": "Per-tool display-name overrides applied account-wide", "additionalProperties": false, "properties": { "message_board": { "type": "string", "maxLength": 40 }, "todos": { "type": "string", "maxLength": 40 }, "folder": { "type": "string", "maxLength": 40 }, "calendar": { "type": "string", "maxLength": 40 }, "board": { "type": "string", "maxLength": 40 }, "chat": { "type": "string", "maxLength": 40 }, "check_ins": { "type": "string", "maxLength": 40 }, "clients": { "type": "string", "maxLength": 40 } } }, "voice_notes_enabled": { "type": "boolean" }, "restrict_project_edits": { "type": "boolean" }, "restrict_people_edits": { "type": "boolean" }, "restrict_content_actions": { "type": "boolean" }, "restrict_public_links": { "type": "boolean" }, "limit_comment_editing": { "type": "boolean" } } } } } }, "responses": { "200": { "description": "The fresh settings document", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/OrgSettings" } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "403": { "$ref": "#/components/responses/Forbidden" }, "404": { "$ref": "#/components/responses/NotFound" }, "422": { "$ref": "#/components/responses/Validation" } } } }, "/api/v1/{orgSlug}/support": { "post": { "operationId": "createSupportTicket", "tags": [ "Organizations" ], "summary": "Open a support ticket", "description": "The in-app support dialog's endpoint: pick a topic and write a message. The caller's email, name, and workspace context are attached server-side and forwarded to Thicket support, which acknowledges by email. When forwarding fails the error points at support@thickethq.com instead.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "topic", "message" ], "properties": { "topic": { "type": "string", "enum": [ "broken", "login", "signup", "request", "billing", "email", "confused" ] }, "message": { "type": "string", "minLength": 1, "maxLength": 10000 } } } } } }, "responses": { "200": { "description": "Ticket created", "content": { "application/json": { "schema": { "type": "object", "required": [ "ticket" ], "properties": { "ticket": { "type": [ "string", "null" ], "description": "The ticket reference" } } } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "404": { "$ref": "#/components/responses/NotFound" }, "422": { "$ref": "#/components/responses/Validation" }, "502": { "description": "The ticket could not be forwarded (code support_failed); email support@thickethq.com instead", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } }, "/api/v1/{orgSlug}/templates": { "get": { "operationId": "listTemplates", "tags": [ "Templates" ], "summary": "List project templates", "description": "Active templates in name order; `?status` lists archived or trashed instead. Templates are not projects to the rest of the API: they never appear in `/projects` listings, do not count against the plan's project limit, and stay out of activity feeds.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" }, { "name": "status", "in": "query", "schema": { "type": "string", "enum": [ "active", "archived", "trashed" ] }, "description": "Defaults to active" } ], "responses": { "200": { "description": "Templates in name order", "content": { "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/Project" } } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "404": { "$ref": "#/components/responses/NotFound" } } }, "post": { "operationId": "createTemplate", "tags": [ "Templates" ], "summary": "Create a template", "description": "With `project_id`: snapshot that project's structure (tools, lists, groups, to-dos, pinned messages, folders and docs, columns, questions); completed to-dos reset to incomplete and cards return to Triage. Without it: an empty template to build up in the template editor, and `name` is required.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "project_id": { "type": "string", "format": "uuid", "description": "The project to snapshot; omit for an empty template" }, "name": { "type": "string", "minLength": 1, "maxLength": 200, "description": "Required when project_id is omitted" }, "description": { "type": [ "string", "null" ], "maxLength": 2000 }, "include_assignments": { "type": "boolean" }, "include_comments": { "type": "boolean" }, "keep_client_visibility": { "type": "boolean" } } } } } }, "responses": { "201": { "description": "The created template", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Project" } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "403": { "$ref": "#/components/responses/Forbidden" }, "404": { "$ref": "#/components/responses/NotFound" }, "422": { "$ref": "#/components/responses/Validation" } } } }, "/api/v1/{orgSlug}/templates/{templateId}": { "get": { "operationId": "getTemplate", "tags": [ "Templates" ], "summary": "Get a template", "description": "The template plus its `tools`, the same shape as a project GET; `dock` is the same deprecated alias.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" }, { "name": "templateId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "The template with its tools", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ProjectDetail" } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "404": { "$ref": "#/components/responses/NotFound" } } }, "patch": { "operationId": "updateTemplate", "tags": [ "Templates" ], "summary": "Update a template", "description": "`clients_enabled` is the template's Work-with-clients flag; projects made from it inherit it.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" }, { "name": "templateId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "name": { "type": "string", "minLength": 1, "maxLength": 150 }, "description": { "type": [ "string", "null" ], "maxLength": 2000 }, "clients_enabled": { "type": "boolean" } } } } } }, "responses": { "200": { "description": "The updated template", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Project" } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "403": { "$ref": "#/components/responses/Forbidden" }, "404": { "$ref": "#/components/responses/NotFound" }, "422": { "$ref": "#/components/responses/Validation" } } }, "delete": { "operationId": "deleteTemplate", "tags": [ "Templates" ], "summary": "Trash a template", "description": "Moves the template to the trash.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" }, { "name": "templateId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "204": { "description": "Trashed" }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "404": { "$ref": "#/components/responses/NotFound" } } } }, "/api/v1/{orgSlug}/templates/{templateId}/use": { "post": { "operationId": "useTemplate", "tags": [ "Templates" ], "summary": "Start a project from a template", "description": "Creates a real project from the template. It inherits the template's client setting; dates shift relative to today (anchored on the template's `starts_on`, else its save date); people assigned in the template join the new project. Instantiating enforces the plan's project limit (402) even though templates themselves never count against it. Archived templates cannot instantiate (404).", "parameters": [ { "$ref": "#/components/parameters/orgSlug" }, { "name": "templateId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "name" ], "properties": { "name": { "type": "string", "minLength": 1, "maxLength": 200 }, "description": { "type": "string", "maxLength": 2000 }, "all_access": { "type": "boolean" } } } } } }, "responses": { "201": { "description": "The new project", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Project" } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "402": { "$ref": "#/components/responses/PlanLimit" }, "404": { "$ref": "#/components/responses/NotFound" }, "422": { "$ref": "#/components/responses/Validation" } } } }, "/api/v1/{orgSlug}/trash": { "get": { "operationId": "listTrash", "tags": [ "Trash & admin" ], "summary": "The account-wide trash", "description": "Owner only, oldest first: everything deleted across all projects. Items purge permanently after 30 days. Members and clients receive 403; the project-scoped trash is GET /projects/{projectId}/trash.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" } ], "responses": { "200": { "description": "Trashed items, oldest first", "content": { "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/TrashedItem" } } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "403": { "$ref": "#/components/responses/Forbidden" }, "404": { "$ref": "#/components/responses/NotFound" } } } }, "/api/v1/{orgSlug}/trash/{recordingId}": { "delete": { "operationId": "destroyTrashedRecording", "tags": [ "Trash & admin" ], "summary": "Permanently delete a trashed item", "description": "Owner only and irreversible: stored files and the row go, with the subtree, assignments, subscriptions, notifications, events, cheers, bookmarks, and links cascading away.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" }, { "name": "recordingId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "Destroyed", "content": { "application/json": { "schema": { "type": "object", "required": [ "ok" ], "properties": { "ok": { "type": "boolean" } } } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "403": { "$ref": "#/components/responses/Forbidden" }, "404": { "$ref": "#/components/responses/NotFound" } } } }, "/api/v1/{orgSlug}/trash/{recordingId}/restore": { "post": { "operationId": "restoreTrashedRecording", "tags": [ "Trash & admin" ], "summary": "Restore a trashed item", "description": "Owner trash-page restore: brings back the item and exactly the subtree trashed with it in the same batch (independently trashed children stay trashed).", "parameters": [ { "$ref": "#/components/parameters/orgSlug" }, { "name": "recordingId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "The restored recording", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Recording" } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "403": { "$ref": "#/components/responses/Forbidden" }, "404": { "$ref": "#/components/responses/NotFound" } } } }, "/api/v1/{orgSlug}/uploads/{recordingId}/download": { "get": { "operationId": "downloadUpload", "tags": [ "Files" ], "summary": "Download a file", "description": "Redirects (302) to a short-lived presigned URL with attachment disposition; follow the redirect to get the bytes. The presigned URL expires quickly, so request a fresh one per download rather than storing it.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" }, { "name": "recordingId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" }, "description": "The upload recording's id" } ], "responses": { "302": { "description": "Redirect to the short-lived presigned download URL", "headers": { "Location": { "schema": { "type": "string", "format": "uri" } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "404": { "$ref": "#/components/responses/NotFound" } } } }, "/api/v1/{orgSlug}/uploads/{recordingId}/file": { "get": { "operationId": "getUploadFile", "tags": [ "Files" ], "summary": "Serve a file inline", "description": "The stable same-origin route rich-text `img` and `audio` srcs point at, so stored HTML never carries expiring URLs. Access is re-checked on every request, then 302 to a short-lived presigned URL with inline disposition.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" }, { "name": "recordingId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" }, "description": "The upload recording's id" } ], "responses": { "302": { "description": "Redirect to the short-lived presigned inline URL", "headers": { "Location": { "schema": { "type": "string", "format": "uri" } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "404": { "$ref": "#/components/responses/NotFound" } } } }, "/api/v1/{orgSlug}/uploads/{recordingId}/versions": { "get": { "operationId": "listUploadVersions", "tags": [ "Files" ], "summary": "List a file's versions", "description": "The current version plus every past one, newest first: every past version is kept on file.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" }, { "name": "recordingId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" }, "description": "The upload recording's id" } ], "responses": { "200": { "description": "Current file plus kept versions", "content": { "application/json": { "schema": { "type": "object", "required": [ "current", "versions" ], "properties": { "current": { "type": [ "object", "null" ], "required": [ "filename", "byte_size", "content_type" ], "properties": { "filename": { "type": "string" }, "byte_size": { "type": "integer" }, "content_type": { "type": "string" } } }, "versions": { "type": "array", "items": { "$ref": "#/components/schemas/UploadVersion" } } } } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "404": { "$ref": "#/components/responses/NotFound" } } }, "post": { "operationId": "replaceUploadVersion", "tags": [ "Files" ], "summary": "Replace the file with a new version", "description": "Multipart body with `file` (up to 50 MB). Same recording keeps its URL, comments, and subscribers; the outgoing version moves to the kept list and the title and size flip to the new file. `notify` picks who hears about the new version; omitted defaults to the project audience. Returns 402 when the plan's storage cap is reached.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" }, { "name": "recordingId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" }, "description": "The upload recording's id" } ], "requestBody": { "required": true, "content": { "multipart/form-data": { "schema": { "type": "object", "required": [ "file" ], "properties": { "file": { "type": "string", "format": "binary", "description": "The replacement file bytes, up to 50 MB, non-empty" }, "notify": { "type": "string", "description": "\"everyone\", \"none\", or a JSON array of membership ids; omitted defaults to the project audience" } } } } } }, "responses": { "201": { "description": "The upload recording, now pointing at the new version", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Recording" } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "402": { "$ref": "#/components/responses/PlanLimit" }, "404": { "$ref": "#/components/responses/NotFound" }, "422": { "$ref": "#/components/responses/Validation" } } } }, "/api/v1/{orgSlug}/uploads/{recordingId}/versions/{versionId}/download": { "get": { "operationId": "downloadUploadVersion", "tags": [ "Files" ], "summary": "Download a past version", "description": "Redirects (302) to a short-lived presigned URL for a past version of the file; follow the redirect to get the bytes.", "parameters": [ { "$ref": "#/components/parameters/orgSlug" }, { "name": "recordingId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" }, "description": "The upload recording's id" }, { "name": "versionId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" }, "description": "A version id from the versions listing" } ], "responses": { "302": { "description": "Redirect to the short-lived presigned download URL", "headers": { "Location": { "schema": { "type": "string", "format": "uri" } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "404": { "$ref": "#/components/responses/NotFound" } } } }, "/api/v1/authorization": { "get": { "operationId": "getAuthorization", "tags": [ "Authorization" ], "summary": "Introspect the credential", "description": "Who am I, which organizations can I reach, what may this credential do. Make this your integration's first call; each organization's `href` is its API base URL.", "responses": { "200": { "description": "The credential's authorization document", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Authorization" } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" } } } }, "/api/v1/cli/authorizations": { "post": { "operationId": "createCliAuthorization", "tags": [ "Authorization" ], "summary": "Approve a CLI sign-in (session-only)", "description": "The approve half of `thicket auth login`: records a PKCE-style grant for the presented challenge and returns a single-use code (10-minute expiry) for the CLI's loopback callback. Session-only like token minting: bearer calls receive 403 `session_required`. The code itself is not a credential; it only pays out at the exchange to whoever holds the matching `code_verifier`.", "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "challenge", "device_name" ], "properties": { "challenge": { "type": "string", "pattern": "^[A-Za-z0-9_-]{43}$", "description": "base64url(SHA-256(code_verifier)) — the PKCE S256 transform" }, "scope": { "type": "string", "enum": [ "read", "full" ], "default": "read" }, "device_name": { "type": "string", "maxLength": 100, "description": "Shown on the approve page and baked into the minted token's name" } } } } } }, "responses": { "201": { "description": "The single-use authorization code", "content": { "application/json": { "schema": { "type": "object", "required": [ "code", "expires_at" ], "properties": { "code": { "type": "string" }, "expires_at": { "type": "string", "format": "date-time" } } } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "403": { "$ref": "#/components/responses/Forbidden" }, "422": { "$ref": "#/components/responses/Validation" } } } }, "/api/v1/cli/token": { "post": { "operationId": "exchangeCliAuthorization", "tags": [ "Authorization" ], "summary": "Exchange a CLI authorization code for a token", "security": [], "description": "The exchange half of `thicket auth login`. Anonymous by design: the code plus the PKCE `code_verifier` are the credential. Any exchange attempt burns the code (single use), and refusals are indistinguishable (unknown, expired, used, or mismatched verifier all answer 401 `invalid_grant`). The response's `token` field is the only time the minted personal access token exists in plaintext. Rate-limited per caller address; 429 carries Retry-After.", "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "code", "code_verifier" ], "properties": { "code": { "type": "string" }, "code_verifier": { "type": "string", "minLength": 43, "maxLength": 200 } } } } } }, "responses": { "201": { "description": "The minted token", "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/PersonalAccessToken" }, { "type": "object", "required": [ "token" ], "properties": { "token": { "type": "string", "description": "The secret; shown exactly once" } } } ] } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "422": { "$ref": "#/components/responses/Validation" }, "429": { "$ref": "#/components/responses/RateLimited" } } } }, "/api/v1/me/preferences": { "get": { "operationId": "getPreferences", "tags": [ "Me" ], "summary": "Your personal preferences", "description": "User-scoped, not org-scoped: one set of preferences follows you across organizations.", "responses": { "200": { "description": "The full preferences document", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Preferences" } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" } } }, "patch": { "operationId": "updatePreferences", "tags": [ "Me" ], "summary": "Update preferences", "description": "Send only the fields to change; nullable fields accept null to clear. Returns the full updated document.", "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Preferences" } } } }, "responses": { "200": { "description": "The full updated document", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Preferences" } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "422": { "$ref": "#/components/responses/Validation" } } } }, "/api/v1/me/push-devices": { "put": { "operationId": "registerPushDevice", "tags": [ "Me" ], "summary": "Register this device for mobile push", "description": "Upserts the device's Expo push token; the mobile app calls it on start and after login, refreshing `last_seen_at`. User-scoped like preferences: notifications follow the person across orgs. Registering a token claims it from any previous signer, so on a shared device the previous user's notifications stop arriving. Anything that is not a valid Expo push token is a 422. Dead tokens are pruned automatically when Expo reports DeviceNotRegistered.", "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "expo_push_token", "platform" ], "properties": { "expo_push_token": { "type": "string", "maxLength": 4096, "description": "ExponentPushToken[…]" }, "platform": { "type": "string", "enum": [ "ios", "android" ] }, "device_name": { "type": [ "string", "null" ], "maxLength": 200 } } } } } }, "responses": { "200": { "description": "The registered device", "content": { "application/json": { "schema": { "type": "object", "required": [ "expo_push_token", "platform", "last_seen_at" ], "properties": { "expo_push_token": { "type": "string" }, "platform": { "type": "string", "enum": [ "ios", "android" ] }, "device_name": { "type": [ "string", "null" ] }, "last_seen_at": { "type": "string", "format": "date-time" } } } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "422": { "$ref": "#/components/responses/Validation" } } } }, "/api/v1/me/push-devices/{token}": { "delete": { "operationId": "unregisterPushDevice", "tags": [ "Me" ], "summary": "Unregister a push device", "description": "Sign-out unregisters the device. Idempotent: an unknown or already-removed token is still a 204.", "parameters": [ { "name": "token", "in": "path", "required": true, "schema": { "type": "string" }, "description": "The Expo push token, URL-encoded" } ], "responses": { "204": { "description": "Unregistered" }, "401": { "$ref": "#/components/responses/Unauthenticated" } } } }, "/api/v1/me/tokens": { "get": { "operationId": "listTokens", "tags": [ "Authorization" ], "summary": "List your personal access tokens", "description": "Never returns secrets: `token_prefix` and `last_used_at` exist so the list stays recognizable. Session-only: bearer calls receive 403 `session_required`.", "responses": { "200": { "description": "Your tokens, newest first", "content": { "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/PersonalAccessToken" } } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "403": { "$ref": "#/components/responses/Forbidden" } } }, "post": { "operationId": "createToken", "tags": [ "Authorization" ], "summary": "Mint a personal access token", "description": "The response's `token` field is the only time the secret exists in plaintext. Session-only: a leaked token must not mint successors.", "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "name" ], "properties": { "name": { "type": "string", "maxLength": 100 }, "scope": { "type": "string", "enum": [ "read", "full" ], "default": "read", "description": "Defaults to read (GET/HEAD only), matching the CLI; pick full deliberately for writes" }, "expires_at": { "type": [ "string", "null" ], "format": "date-time", "description": "Omit or null: the token does not expire" } } } } } }, "responses": { "201": { "description": "The minted token", "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/PersonalAccessToken" }, { "type": "object", "required": [ "token" ], "properties": { "token": { "type": "string", "description": "The secret; shown exactly once" } } } ] } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "403": { "$ref": "#/components/responses/Forbidden" }, "422": { "$ref": "#/components/responses/Validation" } } } }, "/api/v1/me/tokens/{tokenId}": { "delete": { "operationId": "revokeToken", "tags": [ "Authorization" ], "summary": "Revoke a token", "description": "Effective immediately. Session-only.", "parameters": [ { "name": "tokenId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "204": { "description": "Revoked" }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "403": { "$ref": "#/components/responses/Forbidden" }, "404": { "$ref": "#/components/responses/NotFound" } } } }, "/api/v1/orgs": { "get": { "operationId": "listOrganizations", "tags": [ "Organizations" ], "summary": "List your organizations", "description": "Every organization the credential's user belongs to, ordered by name. Cancelled-in-grace organizations are excluded (see /api/v1/orgs/cancelled). `sole_owner` means the caller is the org's only active owner, which blocks account deletion.", "responses": { "200": { "description": "Your organizations", "content": { "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/OrgListItem" } } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" } } }, "post": { "operationId": "createOrganization", "tags": [ "Organizations" ], "summary": "Create an organization", "description": "The caller becomes its owner. The slug is generated server-side ({slugified-name}-{6-char suffix}) and the org starts in the same server-decided state as web onboarding: a 14-day Pro trial with no card, plus the seeded Getting Started sample project. Deliberately takes no plan or billing parameter: plan selection is never a client concern.", "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "name" ], "properties": { "name": { "type": "string", "minLength": 1, "maxLength": 100, "description": "Must contain a letter or number in some script" } } } } } }, "responses": { "201": { "description": "The new organization, in the orgs-list row shape (role is always owner, sole_owner always true)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/OrgListItem" } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "409": { "description": "Slug generation collided five times in a row (code conflict, practically unreachable); retry", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "422": { "$ref": "#/components/responses/Validation" } } } }, "/api/v1/orgs/{orgId}/restore": { "post": { "operationId": "restoreOrganization", "tags": [ "Organizations" ], "summary": "Restore a cancelled organization", "description": "Bring a cancelled account back inside its 30-day window (owner only, no body). Non-members and unknown ids alike receive 404: an org id must not be probeable.", "parameters": [ { "name": "orgId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "Restored", "content": { "application/json": { "schema": { "type": "object", "required": [ "ok", "slug", "name" ], "properties": { "ok": { "type": "boolean" }, "slug": { "type": "string" }, "name": { "type": "string" } } } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" }, "403": { "$ref": "#/components/responses/Forbidden" }, "404": { "$ref": "#/components/responses/NotFound" }, "410": { "description": "The 30-day window has passed (code restore_window_passed)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } }, "/api/v1/orgs/cancelled": { "get": { "operationId": "listCancelledOrganizations", "tags": [ "Organizations" ], "summary": "List your cancelled-but-restorable organizations", "description": "Cancelled organizations still inside their 30-day restore window. `can_restore` is true where the caller is an owner; restoring is POST /api/v1/orgs/{orgId}/restore.", "responses": { "200": { "description": "Cancelled organizations inside the restore window", "content": { "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/CancelledOrg" } } } } }, "401": { "$ref": "#/components/responses/Unauthenticated" } } } } }, "components": { "securitySchemes": { "personalAccessToken": { "type": "http", "scheme": "bearer", "description": "Personal access token from My settings → API tokens (`thicket_pat_…`). The token acts as its user: same organizations, project access, role, and row-level security as their sign-in. `read`-scope tokens may only GET/HEAD (writes → 403 `read_only_token`). Bearer requests must send a User-Agent naming the app and a contact (URL or email) or they receive 400 `user_agent_required`. The web app's own session cookie is also accepted on every route." } }, "schemas": { "Error": { "type": "object", "description": "Every non-2xx response carries this envelope.", "required": [ "error" ], "properties": { "error": { "type": "object", "required": [ "code", "message" ], "properties": { "code": { "type": "string", "description": "Machine-readable cause: unauthenticated, user_agent_required, forbidden, read_only_token, session_required, limit_reached, not_found, invalid, bad_request, rate_limited, …" }, "message": { "type": "string" } } } } }, "RecordingType": { "type": "string", "description": "The polymorphic content vocabulary. Tool containers: message_board, todos, folder, calendar, board, chat, check_ins, clients (and the lazily-created health gauge). Content: everything else.", "enum": [ "message_board", "message", "comment", "todos", "todolist", "todo", "progress_update", "folder", "document", "upload", "linked_file", "calendar", "calendar_event", "board", "column", "card", "step", "chat", "chat_message", "check_ins", "question", "answer", "dm", "clients", "client_approval", "client_correspondence", "personal_note", "health", "health_update" ] }, "RecordingStatus": { "type": "string", "enum": [ "active", "archived", "trashed", "drafted" ] }, "Recording": { "type": "object", "description": "The uniform shape every recording serializes to; type-specific data rides in `meta`. List rows on children endpoints additionally carry creator_name, creator_image, comment_count, last_comment_at (and chat_message rows their cheers, client_approval rows their assignees).", "required": [ "id", "type", "status", "created_at", "updated_at" ], "properties": { "id": { "type": "string", "format": "uuid" }, "type": { "$ref": "#/components/schemas/RecordingType" }, "project_id": { "type": [ "string", "null" ], "format": "uuid" }, "parent_id": { "type": [ "string", "null" ], "format": "uuid" }, "status": { "$ref": "#/components/schemas/RecordingStatus" }, "title": { "type": [ "string", "null" ] }, "content": { "type": [ "string", "null" ], "description": "Sanitized HTML" }, "position": { "type": [ "number", "null" ] }, "due_on": { "type": [ "string", "null" ], "format": "date" }, "starts_on": { "type": [ "string", "null" ], "format": "date" }, "starts_at": { "type": [ "string", "null" ], "format": "date-time" }, "ends_at": { "type": [ "string", "null" ], "format": "date-time" }, "all_day": { "type": [ "boolean", "null" ] }, "completed": { "type": "boolean" }, "completed_at": { "type": [ "string", "null" ], "format": "date-time" }, "completed_by_id": { "type": [ "string", "null" ], "format": "uuid" }, "creator_id": { "type": [ "string", "null" ], "format": "uuid" }, "visible_to_clients": { "type": "boolean" }, "pinned_at": { "type": [ "string", "null" ], "format": "date-time" }, "scheduled_at": { "type": [ "string", "null" ], "format": "date-time" }, "category_id": { "type": [ "string", "null" ], "format": "uuid" }, "approval_status": { "type": [ "string", "null" ], "enum": [ "pending", "approved", "rejected", null ] }, "meta": { "type": [ "object", "null" ], "additionalProperties": true }, "created_at": { "type": "string", "format": "date-time" }, "updated_at": { "type": "string", "format": "date-time" } } }, "SearchResult": { "allOf": [ { "$ref": "#/components/schemas/Recording" }, { "type": "object", "properties": { "project_name": { "type": [ "string", "null" ] }, "creator_name": { "type": [ "string", "null" ] }, "parent_title": { "type": [ "string", "null" ] }, "byte_size": { "type": [ "integer", "null" ] }, "content_type": { "type": [ "string", "null" ] }, "excerpt": { "type": [ "string", "null" ], "description": "Tag-stripped text, matches wrapped in " }, "title_excerpt": { "type": [ "string", "null" ] } } } ] }, "ProjectMember": { "type": "object", "required": [ "membership_id", "name" ], "properties": { "membership_id": { "type": "string", "format": "uuid" }, "name": { "type": "string" }, "image": { "type": [ "string", "null" ] } } }, "Project": { "type": "object", "required": [ "id", "name", "status", "all_access", "clients_enabled", "created_at", "updated_at" ], "properties": { "id": { "type": "string", "format": "uuid" }, "name": { "type": "string" }, "description": { "type": [ "string", "null" ] }, "status": { "type": "string", "enum": [ "active", "archived", "trashed" ] }, "all_access": { "type": "boolean" }, "clients_enabled": { "type": "boolean" }, "client_company_name": { "type": [ "string", "null" ] }, "folder_id": { "type": [ "string", "null" ], "format": "uuid" }, "color": { "type": [ "string", "null" ] }, "logo": { "type": [ "string", "null" ] }, "starts_on": { "type": [ "string", "null" ], "format": "date" }, "ends_on": { "type": [ "string", "null" ], "format": "date" }, "show_activity": { "type": "boolean" }, "is_template": { "type": "boolean" }, "is_sample": { "type": "boolean" }, "starred": { "type": "boolean", "description": "Default listing only" }, "joined": { "type": "boolean", "description": "?scope=all (owner) only" }, "members": { "type": "array", "items": { "$ref": "#/components/schemas/ProjectMember" }, "description": "?include=members only (first 5)" }, "member_count": { "type": "integer", "description": "?include=members only" }, "created_at": { "type": "string", "format": "date-time" }, "updated_at": { "type": "string", "format": "date-time" } } }, "ToolEntry": { "type": "object", "description": "One row of a project's tool list.", "required": [ "tool", "label", "enabled", "position" ], "properties": { "tool": { "type": "string", "enum": [ "message_board", "todos", "folder", "calendar", "board", "chat", "check_ins", "clients" ] }, "label": { "type": "string", "description": "Display name after org-wide and per-project renames" }, "enabled": { "type": "boolean" }, "position": { "type": "number" }, "container_id": { "type": [ "string", "null" ], "format": "uuid", "description": "The tool's container recording; content is created under it" } } }, "ProjectDetail": { "allOf": [ { "$ref": "#/components/schemas/Project" }, { "type": "object", "required": [ "tools" ], "properties": { "tools": { "type": "array", "items": { "$ref": "#/components/schemas/ToolEntry" } }, "dock": { "type": "array", "items": { "$ref": "#/components/schemas/ToolEntry" }, "deprecated": true, "description": "Deprecated alias of tools (pre-2026-08-21 vocabulary); removed after the transition window" } } } ] }, "OrganizationSummary": { "type": "object", "required": [ "id", "name", "slug" ], "properties": { "id": { "type": "string", "format": "uuid" }, "name": { "type": "string" }, "slug": { "type": "string" }, "href": { "type": "string", "format": "uri", "description": "The organization's API base URL" } } }, "Authorization": { "type": "object", "description": "Credential introspection: make this your integration's first call.", "required": [ "identity", "organizations", "scope", "expires_at" ], "properties": { "identity": { "type": "object", "required": [ "id" ], "properties": { "id": { "type": "string", "description": "Stable user id; names live on People endpoints" } } }, "organizations": { "type": "array", "items": { "allOf": [ { "$ref": "#/components/schemas/OrganizationSummary" }, { "type": "object", "required": [ "membership_id", "role" ], "properties": { "membership_id": { "type": "string", "format": "uuid", "description": "Who the caller is inside this organization — the id assignee pickers and attribution speak" }, "role": { "type": "string", "enum": [ "owner", "admin", "member", "client" ] } } } ] } }, "scope": { "type": "string", "enum": [ "read", "full" ] }, "expires_at": { "type": [ "string", "null" ], "format": "date-time", "description": "null for sessions and non-expiring tokens" } } }, "PersonalAccessToken": { "type": "object", "required": [ "id", "name", "token_prefix", "scope", "created_at" ], "properties": { "id": { "type": "string", "format": "uuid" }, "name": { "type": "string" }, "token_prefix": { "type": "string", "description": "Enough of the secret to recognize it in a list; never the whole thing" }, "scope": { "type": "string", "enum": [ "read", "full" ] }, "last_used_at": { "type": [ "string", "null" ], "format": "date-time", "description": "Minute precision" }, "expires_at": { "type": [ "string", "null" ], "format": "date-time" }, "revoked_at": { "type": [ "string", "null" ], "format": "date-time" }, "created_at": { "type": "string", "format": "date-time" } } }, "CalendarRecurrence": { "type": "object", "description": "How a series repeats. `time_zone` anchors the series' wall clock across DST and fixes the calendar days `exdates`, `until`, and `occurrence_key` are read in.", "required": [ "frequency" ], "properties": { "frequency": { "type": "string", "enum": [ "daily", "weekly", "monthly", "yearly", "custom" ], "description": "UI presets \"every weekday\" and \"every other week\" map onto custom" }, "interval": { "type": "integer", "minimum": 1, "maximum": 52, "description": "Every N periods" }, "by_weekday": { "type": "array", "items": { "type": "integer", "minimum": 0, "maximum": 6 }, "maxItems": 7, "description": "custom frequency: weekdays to repeat on, 0 = Sunday" }, "monthly_mode": { "type": "string", "enum": [ "day_of_month", "nth_weekday" ], "description": "monthly: follow the base date's day number, or its weekday instance (months missing the instance skip, RFC 5545 BYDAY)" }, "exdates": { "type": "array", "items": { "type": "string", "format": "date" }, "maxItems": 400, "description": "Anchor-zone day keys of occurrences deleted via the trash prompt" }, "until": { "type": "string", "format": "date", "description": "Last day the series may occur on" }, "time_zone": { "type": "string", "maxLength": 64, "description": "IANA zone anchoring the series' wall clock; default: the caller's zone" } } }, "CalendarOccurrence": { "type": "object", "description": "One row of the global calendar: an event, one occurrence of a recurring event, or (with ?tasks=true) a due to-do or card.", "required": [ "id", "kind", "type", "title", "start", "all_day", "circled", "mine" ], "properties": { "id": { "type": "string", "format": "uuid", "description": "The underlying recording; occurrences of the same series share it" }, "kind": { "type": "string", "enum": [ "event", "task" ] }, "type": { "$ref": "#/components/schemas/RecordingType" }, "title": { "type": [ "string", "null" ] }, "start": { "type": "string", "format": "date-time" }, "end": { "type": [ "string", "null" ], "format": "date-time" }, "all_day": { "type": "boolean" }, "project_id": { "type": [ "string", "null" ], "format": "uuid", "description": "null: the account calendar" }, "project_name": { "type": [ "string", "null" ] }, "parent_id": { "type": [ "string", "null" ], "format": "uuid" }, "circled": { "type": "boolean" }, "link": { "type": [ "string", "null" ], "format": "uri" }, "mine": { "type": "boolean", "description": "You created it or are invited" }, "occurrence_key": { "type": [ "string", "null" ], "format": "date", "description": "Recurring occurrences only: the anchor-zone day key the occurrences DELETE endpoint speaks. Never derive it from `start` client-side; the viewer's zone may disagree with the anchor's" } } }, "CalendarFeed": { "type": "object", "required": [ "id", "url", "scope", "created_at" ], "properties": { "id": { "type": "string", "format": "uuid" }, "url": { "type": "string", "format": "uri", "description": "The public /feeds/{token}.ics capability URL; the token is the credential" }, "scope": { "type": "object", "description": "The filters the feed was minted with (stored camelCase)", "properties": { "projectId": { "type": "string", "format": "uuid" }, "includeTasks": { "type": "boolean" }, "just": { "type": "string", "enum": [ "me", "everyone" ] } } }, "created_at": { "type": "string", "format": "date-time" } } }, "CalendarParseResult": { "type": "object", "description": "A prefill suggestion. When no date was found, `starts_at`, `ends_at`, and `all_day` are all null and `title` is the trimmed input.", "required": [ "title", "starts_at", "ends_at", "all_day" ], "properties": { "title": { "type": "string", "description": "The input with the date phrase removed" }, "starts_at": { "type": [ "string", "null" ], "format": "date-time" }, "ends_at": { "type": [ "string", "null" ], "format": "date-time", "description": "Set when a range was given" }, "all_day": { "type": [ "boolean", "null" ], "description": "true when no clock time was named" } } }, "DmSummary": { "type": "object", "required": [ "id", "title", "status", "cover_upload_id", "participants", "last_line", "updated_at" ], "properties": { "id": { "type": "string", "format": "uuid", "description": "The dm recording" }, "title": { "type": [ "string", "null" ], "description": "Group-chat name (PATCH /recordings/{id} renames); null: render the participant names" }, "status": { "type": "string", "enum": [ "active", "archived" ] }, "cover_upload_id": { "type": [ "string", "null" ], "format": "uuid", "description": "Group-chat cover image: an upload posted in the chat" }, "participants": { "type": "array", "items": { "type": "object", "required": [ "membership_id", "name" ], "properties": { "membership_id": { "type": "string", "format": "uuid" }, "name": { "type": [ "string", "null" ] } } } }, "last_line": { "type": [ "object", "null" ], "required": [ "preview", "created_at" ], "properties": { "preview": { "type": "string", "description": "Plain-text preview of the newest line" }, "created_at": { "type": "string", "format": "date-time" } } }, "updated_at": { "type": "string", "format": "date-time" } } }, "SearchMetadata": { "type": "object", "required": [ "types", "file_types", "default_type_label", "default_file_type_label" ], "properties": { "types": { "type": "array", "description": "Valid `type` filters; the null key is the unfiltered default", "items": { "type": "object", "required": [ "key", "value" ], "properties": { "key": { "type": [ "string", "null" ], "description": "A recording type, or null for no filter" }, "value": { "type": "string", "description": "Display label" } } } }, "file_types": { "type": "array", "description": "Valid `file_type` filters; the null key is the unfiltered default", "items": { "type": "object", "required": [ "key", "value" ], "properties": { "key": { "type": [ "string", "null" ], "enum": [ "image", "audio", "video", "pdf", null ] }, "value": { "type": "string" } } } }, "default_type_label": { "type": "string" }, "default_file_type_label": { "type": "string" } } }, "ActivityEvent": { "type": "object", "required": [ "id", "action", "created_at" ], "properties": { "id": { "type": "string", "format": "uuid" }, "action": { "type": "string", "description": "What happened: created, completed, moved, and the rest of the event vocabulary" }, "detail": { "type": [ "string", "null" ], "description": "Human-readable qualifier for the action" }, "created_at": { "type": "string", "format": "date-time" }, "actor_id": { "type": [ "string", "null" ], "format": "uuid" }, "actor_name": { "type": [ "string", "null" ] }, "project_id": { "type": [ "string", "null" ], "format": "uuid" }, "project_name": { "type": [ "string", "null" ] }, "recording_id": { "type": [ "string", "null" ], "format": "uuid" }, "recording_type": { "type": [ "string", "null" ] }, "recording_title": { "type": [ "string", "null" ] }, "excerpt": { "type": [ "string", "null" ], "description": "Plain-text excerpt of the content involved" } } }, "UpcomingReportEvent": { "type": "object", "description": "A dated row of the upcoming report; assignable rows extend it.", "required": [ "id", "type", "title", "starts_at", "ends_at", "all_day", "project_id", "project_name", "recurring" ], "properties": { "id": { "type": "string", "format": "uuid" }, "type": { "$ref": "#/components/schemas/RecordingType" }, "title": { "type": [ "string", "null" ] }, "starts_at": { "type": "string", "format": "date-time" }, "ends_at": { "type": [ "string", "null" ], "format": "date-time" }, "all_day": { "type": "boolean" }, "project_id": { "type": [ "string", "null" ], "format": "uuid" }, "project_name": { "type": [ "string", "null" ] }, "recurring": { "type": "boolean" } } }, "UpcomingReportAssignable": { "allOf": [ { "$ref": "#/components/schemas/UpcomingReportEvent" }, { "type": "object", "required": [ "due_on", "assignees" ], "properties": { "due_on": { "type": "string", "format": "date" }, "parent_id": { "type": [ "string", "null" ], "format": "uuid" }, "parent_title": { "type": [ "string", "null" ] }, "assignees": { "type": "array", "items": { "$ref": "#/components/schemas/ReportAssignee" } } } } ] }, "UpcomingReport": { "type": "object", "description": "`events`, `recurring_occurrences`, and `assignables` are the canonical keys. `schedule_entries` and `recurring_schedule_entry_occurrences` are deprecated aliases still served during the vocabulary transition; stop reading them.", "required": [ "events", "recurring_occurrences", "assignables" ], "properties": { "events": { "type": "array", "items": { "$ref": "#/components/schemas/UpcomingReportEvent" }, "description": "One-time events in the window" }, "recurring_occurrences": { "type": "array", "items": { "$ref": "#/components/schemas/UpcomingReportEvent" }, "description": "Expanded occurrences of recurring events" }, "assignables": { "type": "array", "items": { "$ref": "#/components/schemas/UpcomingReportAssignable" }, "description": "Dated open to-dos, cards, and card steps due in the window" }, "schedule_entries": { "type": "array", "items": { "$ref": "#/components/schemas/UpcomingReportEvent" }, "deprecated": true, "description": "Deprecated alias of events" }, "recurring_schedule_entry_occurrences": { "type": "array", "items": { "$ref": "#/components/schemas/UpcomingReportEvent" }, "deprecated": true, "description": "Deprecated alias of recurring_occurrences" } } }, "HealthReportGauge": { "type": "object", "required": [ "project_id", "project_name", "status", "position", "updated_at", "updated_by" ], "properties": { "project_id": { "type": "string", "format": "uuid" }, "project_name": { "type": "string" }, "status": { "type": "string", "enum": [ "on_track", "some_risk", "concerned" ] }, "position": { "type": [ "number", "null" ], "minimum": 0, "maximum": 100, "description": "Needle position, 0 to 100" }, "updated_at": { "type": [ "string", "null" ], "format": "date-time", "description": "When the needle last moved" }, "updated_by": { "type": [ "string", "null" ], "description": "Who last moved it" } } }, "ReportAssignee": { "type": "object", "required": [ "membership_id", "name" ], "properties": { "membership_id": { "type": "string", "format": "uuid" }, "name": { "type": [ "string", "null" ] } } }, "ReportPerson": { "type": "object", "required": [ "membership_id", "name", "email", "role" ], "properties": { "membership_id": { "type": "string", "format": "uuid" }, "name": { "type": [ "string", "null" ] }, "email": { "type": [ "string", "null" ], "format": "email" }, "role": { "type": "string", "enum": [ "owner", "admin", "member", "client" ] }, "title": { "type": [ "string", "null" ] }, "company_name": { "type": [ "string", "null" ] } } }, "AssignedReportItem": { "allOf": [ { "$ref": "#/components/schemas/Recording" }, { "type": "object", "required": [ "project_name", "assignees" ], "properties": { "project_name": { "type": "string" }, "parent_title": { "type": [ "string", "null" ] }, "assignees": { "type": "array", "items": { "$ref": "#/components/schemas/ReportAssignee" } } } } ] }, "AssignedReport": { "type": "object", "required": [ "person", "grouped_by", "items" ], "properties": { "person": { "type": "object", "required": [ "membership_id", "name", "role" ], "properties": { "membership_id": { "type": "string", "format": "uuid" }, "name": { "type": [ "string", "null" ] }, "role": { "type": "string", "enum": [ "owner", "admin", "member", "client" ] }, "title": { "type": [ "string", "null" ] } } }, "grouped_by": { "type": "string", "enum": [ "bucket", "date" ], "description": "The sort the items arrived in" }, "items": { "type": "array", "items": { "$ref": "#/components/schemas/AssignedReportItem" } } } }, "OverdueReportItem": { "allOf": [ { "$ref": "#/components/schemas/AssignedReportItem" }, { "type": "object", "required": [ "days_late" ], "properties": { "days_late": { "type": "integer", "minimum": 1 } } } ] }, "OverdueReport": { "type": "object", "description": "The four Basecamp lateness buckets; every overdue open to-do lands in exactly one.", "required": [ "under_a_week_late", "over_a_week_late", "over_a_month_late", "over_three_months_late" ], "properties": { "under_a_week_late": { "type": "array", "items": { "$ref": "#/components/schemas/OverdueReportItem" } }, "over_a_week_late": { "type": "array", "items": { "$ref": "#/components/schemas/OverdueReportItem" } }, "over_a_month_late": { "type": "array", "items": { "$ref": "#/components/schemas/OverdueReportItem" } }, "over_three_months_late": { "type": "array", "items": { "$ref": "#/components/schemas/OverdueReportItem" } } } }, "AssignmentRow": { "allOf": [ { "$ref": "#/components/schemas/Recording" }, { "type": "object", "properties": { "project_name": { "type": [ "string", "null" ] }, "parent_title": { "type": [ "string", "null" ] } } } ] }, "GroupedAssignment": { "allOf": [ { "$ref": "#/components/schemas/AssignmentRow" }, { "type": "object", "required": [ "children", "assigned_to_me" ], "properties": { "children": { "type": "array", "items": { "$ref": "#/components/schemas/AssignmentRow" }, "description": "Card steps normalized under their parent card" }, "assigned_to_me": { "type": "boolean", "description": "false only for a pulled-in parent card whose step is the actual assignment" }, "priority_id": { "type": "string", "format": "uuid", "description": "Present on priorities rows only" } } } ] }, "UpNextItem": { "allOf": [ { "$ref": "#/components/schemas/AssignmentRow" }, { "type": "object", "required": [ "priority_id" ], "properties": { "priority_id": { "type": "string", "format": "uuid" } } } ] }, "BookmarkRow": { "type": "object", "required": [ "recording_id", "type", "created_at" ], "properties": { "recording_id": { "type": "string", "format": "uuid" }, "type": { "$ref": "#/components/schemas/RecordingType" }, "title": { "type": [ "string", "null" ] }, "project_id": { "type": [ "string", "null" ], "format": "uuid" }, "parent_id": { "type": [ "string", "null" ], "format": "uuid" }, "created_at": { "type": "string", "format": "date-time" } } }, "MyCheerRow": { "type": "object", "required": [ "id", "content", "created_at", "recording_id", "recording_type" ], "properties": { "id": { "type": "string", "format": "uuid" }, "content": { "type": "string", "description": "The reaction: up to 16 chars of emoji or text" }, "created_at": { "type": "string", "format": "date-time" }, "cheerer_name": { "type": [ "string", "null" ] }, "recording_id": { "type": "string", "format": "uuid" }, "recording_type": { "$ref": "#/components/schemas/RecordingType" }, "recording_title": { "type": [ "string", "null" ] }, "recording_parent_id": { "type": [ "string", "null" ], "format": "uuid" }, "project_id": { "type": [ "string", "null" ], "format": "uuid" }, "project_name": { "type": [ "string", "null" ] } } }, "DoTodayItem": { "type": "object", "required": [ "id", "kind", "start", "all_day" ], "properties": { "id": { "type": "string", "format": "uuid" }, "kind": { "type": "string", "enum": [ "event", "task" ] }, "title": { "type": [ "string", "null" ] }, "start": { "type": "string", "format": "date-time" }, "all_day": { "type": "boolean" }, "project_id": { "type": [ "string", "null" ], "format": "uuid" }, "project_name": { "type": [ "string", "null" ] } } }, "MyEventRow": { "type": "object", "required": [ "id", "start", "all_day", "circled" ], "properties": { "id": { "type": "string", "format": "uuid" }, "title": { "type": [ "string", "null" ] }, "start": { "type": "string", "format": "date-time" }, "end": { "type": [ "string", "null" ], "format": "date-time" }, "all_day": { "type": "boolean" }, "project_id": { "type": [ "string", "null" ], "format": "uuid", "description": "null: an account-calendar event" }, "project_name": { "type": [ "string", "null" ] }, "link": { "type": [ "string", "null" ], "description": "The event's join link, when set" }, "circled": { "type": "boolean" } } }, "NotificationRow": { "type": "object", "required": [ "id", "action", "title", "created_at" ], "properties": { "id": { "type": "string", "format": "uuid" }, "action": { "type": "string", "description": "What happened: assigned, mentioned, commented, chatted, chat_activity, announced, reminder, …" }, "title": { "type": "string", "description": "Usually the actor's name; \"Thicket\" for system rows" }, "body": { "type": [ "string", "null" ], "description": "Reminder phrases are re-derived at read time so a day-before reminder never says Tomorrow on the day itself" }, "bundle_count": { "type": "integer", "description": "Chat rows: lines folded into this one entry while it stayed unread" }, "recording_id": { "type": [ "string", "null" ], "format": "uuid" }, "project_id": { "type": [ "string", "null" ], "format": "uuid" }, "bulletin_id": { "type": [ "string", "null" ], "format": "uuid", "description": "Platform announcements (action announced): open GET /api/v1/{orgSlug}/bulletins/{bulletin_id}" }, "actor_name": { "type": [ "string", "null" ], "description": "null for actor-less rows (announcements, reminders)" }, "read_at": { "type": [ "string", "null" ], "format": "date-time" }, "created_at": { "type": "string", "format": "date-time" } } }, "SnoozeRow": { "type": "object", "required": [ "recording_id", "type", "resurface_at" ], "properties": { "recording_id": { "type": "string", "format": "uuid" }, "type": { "$ref": "#/components/schemas/RecordingType" }, "title": { "type": [ "string", "null" ] }, "project_id": { "type": [ "string", "null" ], "format": "uuid" }, "project_name": { "type": [ "string", "null" ] }, "note": { "type": [ "string", "null" ] }, "resurface_at": { "type": "string", "format": "date-time" } } }, "Preferences": { "type": "object", "description": "The user's personal preferences, one document across organizations. Field families: locale and display (timezone: a valid IANA zone, time_format 12h|24h, week_start sunday…saturday, theme light|dark|system, background_tint); notification routing (notify_scope everything|mentions, though direct actions like mentions, assignments, pings, and approvals always come through; email_notifications gates email copies; notifications_silenced is the in-app \"Shhh\" mute with the badge hidden; desktop_notifications; unread_badge for the \"(N)\" tab title); quiet hours (work_hours_enabled plus work_hours as per-day {start, end} HH:MM windows or null; catch_up_summary opts into one end-of-day summary of held email); email channels (notify_event_reminders, notify_assignment_reminders for due-soon reminders, weekly_assignments_email for the Monday summary, announcement_emails, onboarding_emails, notify_cheers, activity_digest); profile extras (out_of_office_start/out_of_office_end ISO dates, location, current_status); view choices (todos_view list|cards, docs_folders_open). GET returns every field; PATCH accepts any subset." }, "OrgRole": { "type": "string", "description": "Owner and admin administer the org; members are the team; clients see only client-visible recordings in clients-enabled projects they were added to.", "enum": [ "owner", "admin", "member", "client" ] }, "OrgListItem": { "type": "object", "required": [ "id", "name", "slug", "role", "sole_owner" ], "properties": { "id": { "type": "string", "format": "uuid" }, "name": { "type": "string" }, "slug": { "type": "string" }, "role": { "$ref": "#/components/schemas/OrgRole" }, "logo": { "type": "string", "description": "Omitted when unset" }, "sole_owner": { "type": "boolean", "description": "The caller is this org's only active owner" } } }, "CancelledOrg": { "type": "object", "required": [ "id", "name", "slug", "role", "cancelled_at", "purge_after", "can_restore" ], "properties": { "id": { "type": "string", "format": "uuid" }, "name": { "type": "string" }, "slug": { "type": "string" }, "role": { "$ref": "#/components/schemas/OrgRole" }, "cancelled_at": { "type": [ "string", "null" ], "format": "date-time" }, "purge_after": { "type": [ "string", "null" ], "format": "date-time", "description": "When the 30-day restore window closes" }, "can_restore": { "type": "boolean", "description": "True where the caller is an owner" } } }, "Person": { "type": "object", "required": [ "membership_id", "name", "email", "role", "created_at" ], "properties": { "membership_id": { "type": "string", "format": "uuid" }, "name": { "type": "string" }, "email": { "type": "string", "format": "email" }, "image": { "type": [ "string", "null" ], "description": "Same-origin avatar path or external SSO URL; null means render an initials disc" }, "role": { "$ref": "#/components/schemas/OrgRole" }, "title": { "type": [ "string", "null" ] }, "company_id": { "type": [ "string", "null" ], "format": "uuid" }, "company_name": { "type": [ "string", "null" ] }, "out_of_office": { "type": [ "object", "null" ], "description": "null while off", "properties": { "start_date": { "type": "string", "format": "date" }, "end_date": { "type": "string", "format": "date" } } }, "created_at": { "type": "string", "format": "date-time" } } }, "Invitation": { "type": "object", "required": [ "id", "email", "role", "declined", "created_at", "email_status", "join_url" ], "properties": { "id": { "type": "string", "format": "uuid" }, "email": { "type": "string", "format": "email" }, "name": { "type": [ "string", "null" ] }, "role": { "$ref": "#/components/schemas/OrgRole" }, "company_id": { "type": [ "string", "null" ], "format": "uuid" }, "declined": { "type": "boolean", "description": "Declined invites stay listed; a resend revives them" }, "created_at": { "type": "string", "format": "date-time" }, "email_status": { "type": [ "string", "null" ], "enum": [ "skipped", "sent", "delivery_delayed", "delivered", "failed", "bounced", "complained", null ], "description": "The delivery ledger's word on the latest invite email" }, "join_url": { "type": "string", "format": "uri", "description": "The same link the email button carries, for sharing over another channel" } } }, "InviteLink": { "type": "object", "required": [ "enabled", "url" ], "properties": { "enabled": { "type": "boolean" }, "url": { "type": [ "string", "null" ], "format": "uri", "description": "null while disabled" }, "created_at": { "type": "string", "format": "date-time", "description": "Present once the link has been minted" } } }, "AccessProject": { "type": "object", "description": "One row of a change-access listing.", "required": [ "id", "name", "status", "all_access", "clients_enabled" ], "properties": { "id": { "type": "string", "format": "uuid" }, "name": { "type": "string" }, "status": { "type": "string", "enum": [ "active", "archived" ] }, "all_access": { "type": "boolean" }, "clients_enabled": { "type": "boolean" }, "has_row": { "type": "boolean", "description": "Membership listings: an explicit grant exists (non-clients reach all_access projects implicitly)" }, "granted": { "type": "boolean", "description": "Invitation listings: the invitation grants this project on accept" } } }, "PersonProjectAccess": { "type": "object", "required": [ "membership_id", "role", "projects" ], "properties": { "membership_id": { "type": "string", "format": "uuid" }, "role": { "$ref": "#/components/schemas/OrgRole" }, "projects": { "type": "array", "items": { "$ref": "#/components/schemas/AccessProject" }, "description": "Rows carry has_row" } } }, "InvitationProjectAccess": { "type": "object", "required": [ "invitation_id", "email", "role", "declined", "projects" ], "properties": { "invitation_id": { "type": "string", "format": "uuid" }, "email": { "type": "string", "format": "email" }, "role": { "$ref": "#/components/schemas/OrgRole" }, "declined": { "type": "boolean" }, "projects": { "type": "array", "items": { "$ref": "#/components/schemas/AccessProject" }, "description": "Rows carry granted" } } }, "OutOfOffice": { "type": "object", "required": [ "person", "enabled", "ongoing", "start_date", "end_date" ], "properties": { "person": { "type": "object", "required": [ "membership_id", "name" ], "properties": { "membership_id": { "type": "string", "format": "uuid" }, "name": { "type": [ "string", "null" ] } } }, "enabled": { "type": "boolean" }, "ongoing": { "type": "boolean", "description": "Enabled and today falls inside the range" }, "start_date": { "type": [ "string", "null" ], "format": "date" }, "end_date": { "type": [ "string", "null" ], "format": "date" } } }, "PersonEvent": { "type": "object", "description": "One entry of a person's activity trail.", "required": [ "id", "action", "created_at" ], "properties": { "id": { "type": "string", "format": "uuid" }, "action": { "type": "string" }, "detail": { "type": [ "string", "null" ] }, "created_at": { "type": "string", "format": "date-time" }, "project_id": { "type": [ "string", "null" ], "format": "uuid" }, "project_name": { "type": [ "string", "null" ] }, "recording_id": { "type": [ "string", "null" ], "format": "uuid" }, "recording_type": { "type": [ "string", "null" ] }, "recording_title": { "type": [ "string", "null" ] }, "excerpt": { "type": [ "string", "null" ], "description": "Plain-text content excerpt" } } }, "Company": { "type": "object", "required": [ "id", "name", "is_client", "dms_disabled", "member_count", "created_at" ], "properties": { "id": { "type": "string", "format": "uuid" }, "name": { "type": "string" }, "is_client": { "type": "boolean" }, "dms_disabled": { "type": "boolean", "description": "Members drop from chat pickers, can't start DMs, and existing conversations freeze read-only" }, "member_count": { "type": "integer" }, "created_at": { "type": "string", "format": "date-time" } } }, "PeopleGroup": { "type": "object", "required": [ "id", "name", "members" ], "properties": { "id": { "type": "string", "format": "uuid" }, "name": { "type": "string" }, "members": { "type": "array", "items": { "type": "object", "required": [ "membership_id", "name" ], "properties": { "membership_id": { "type": "string", "format": "uuid" }, "name": { "type": "string" } } } } } }, "PersonMerge": { "type": "object", "required": [ "id", "winner_name", "loser_name", "loser_email", "created_at" ], "properties": { "id": { "type": "string", "format": "uuid" }, "winner_name": { "type": [ "string", "null" ] }, "loser_name": { "type": [ "string", "null" ] }, "loser_email": { "type": [ "string", "null" ] }, "created_at": { "type": "string", "format": "date-time" } } }, "OrgSettings": { "type": "object", "required": [ "require_two_factor", "history_retention_days", "tool_names", "voice_notes_enabled", "restrict_project_edits", "restrict_people_edits", "restrict_content_actions", "restrict_public_links", "limit_comment_editing" ], "properties": { "require_two_factor": { "type": "boolean", "description": "Non-client members without TOTP are funneled into 2FA setup at org entry; clients exempt" }, "history_retention_days": { "type": [ "integer", "null" ], "description": "null keeps chat and DM history forever; a number hard-deletes older lines lazily on read, permanently" }, "tool_names": { "type": "object", "additionalProperties": { "type": "string" }, "description": "Per-tool display-name overrides, keyed by tool (message_board, todos, folder, calendar, board, chat, check_ins, clients)" }, "voice_notes_enabled": { "type": "boolean" }, "restrict_project_edits": { "type": "boolean" }, "restrict_people_edits": { "type": "boolean" }, "restrict_content_actions": { "type": "boolean" }, "restrict_public_links": { "type": "boolean" }, "limit_comment_editing": { "type": "boolean" } } }, "OrgAccount": { "type": "object", "description": "The account document: identity, owners, plan limits, can-do flags, and settings.", "required": [ "id", "name", "slug", "created_at", "updated_at", "owner_names", "plan", "limits", "settings" ], "properties": { "id": { "type": "string", "format": "uuid" }, "name": { "type": "string" }, "slug": { "type": "string" }, "logo": { "type": "string", "description": "Omitted when unset" }, "created_at": { "type": "string", "format": "date-time" }, "updated_at": { "type": "string", "format": "date-time" }, "owner_names": { "type": "array", "items": { "type": "string" } }, "plan": { "type": "object", "required": [ "tier", "status", "trial_ends_at", "project_limit", "people_limit", "storage_limit_bytes" ], "properties": { "tier": { "type": "string", "enum": [ "free", "starter", "pro", "comped" ] }, "status": { "type": "string", "enum": [ "active", "trialing", "past_due", "cancelled", "pending", "comped", "lapsed" ] }, "trial_ends_at": { "type": [ "string", "null" ], "format": "date-time" }, "project_limit": { "type": [ "integer", "null" ], "description": "null means unlimited" }, "people_limit": { "type": [ "integer", "null" ], "description": "One pool for members and guests alike, clients included; null means unlimited" }, "storage_limit_bytes": { "type": "integer" } } }, "limits": { "type": "object", "required": [ "can_create_projects", "can_create_users", "can_upload_files" ], "properties": { "can_create_projects": { "type": "boolean" }, "can_create_users": { "type": "boolean" }, "can_upload_files": { "type": "boolean" } } }, "settings": { "type": "object", "required": [ "require_two_factor", "history_retention_days", "tool_names", "voice_notes_enabled" ], "properties": { "require_two_factor": { "type": "boolean" }, "history_retention_days": { "type": [ "integer", "null" ] }, "tool_names": { "type": "object", "additionalProperties": { "type": "string" } }, "voice_notes_enabled": { "type": "boolean" } } } } }, "StorageUsage": { "type": "object", "required": [ "total_bytes", "file_count", "limit_bytes", "largest" ], "properties": { "total_bytes": { "type": "integer" }, "file_count": { "type": "integer" }, "limit_bytes": { "type": "integer" }, "largest": { "type": "array", "description": "The largest files; private-chat and unposted-draft uploads count toward usage but are not listed", "items": { "type": "object", "required": [ "recording_id", "filename", "byte_size", "content_type", "created_at" ], "properties": { "recording_id": { "type": "string", "format": "uuid" }, "filename": { "type": [ "string", "null" ] }, "byte_size": { "type": "integer" }, "content_type": { "type": [ "string", "null" ] }, "project_id": { "type": [ "string", "null" ], "format": "uuid" }, "status": { "$ref": "#/components/schemas/RecordingStatus" }, "created_at": { "type": "string", "format": "date-time" } } } } } }, "BillingInfo": { "type": "object", "required": [ "tier", "status", "interval", "trial_ends_at", "current_period_ends_at", "cancel_at_period_end", "has_stripe_customer", "usage", "limits" ], "properties": { "tier": { "type": "string", "enum": [ "free", "starter", "pro", "comped" ] }, "status": { "type": "string", "enum": [ "active", "trialing", "past_due", "cancelled", "pending", "comped", "lapsed" ] }, "interval": { "type": [ "string", "null" ], "enum": [ "monthly", "yearly", null ] }, "trial_ends_at": { "type": [ "string", "null" ], "format": "date-time" }, "current_period_ends_at": { "type": [ "string", "null" ], "format": "date-time" }, "cancel_at_period_end": { "type": "boolean" }, "has_stripe_customer": { "type": "boolean" }, "usage": { "type": "object", "required": [ "projects", "members", "guests", "invited_members", "invited_guests", "storage_bytes" ], "properties": { "projects": { "type": "integer" }, "members": { "type": "integer" }, "guests": { "type": "integer" }, "invited_members": { "type": "integer" }, "invited_guests": { "type": "integer" }, "storage_bytes": { "type": "integer" } } }, "limits": { "type": "object", "description": "-1 means unlimited; one people pool covers members and guests alike, clients included", "required": [ "projects", "people", "storage_bytes", "activity_history_days" ], "properties": { "projects": { "type": "integer" }, "people": { "type": "integer" }, "storage_bytes": { "type": "integer" }, "activity_history_days": { "type": "integer" } } } } }, "BulletinSummary": { "type": "object", "required": [ "id", "title", "excerpt", "published_at" ], "properties": { "id": { "type": "string", "format": "uuid" }, "title": { "type": "string", "description": "Personalized for the caller" }, "excerpt": { "type": "string", "description": "Capped at 200 chars" }, "published_at": { "type": [ "string", "null" ], "format": "date-time" } } }, "Bulletin": { "type": "object", "required": [ "id", "status", "title", "body_html", "author_name", "published_at" ], "properties": { "id": { "type": "string", "format": "uuid" }, "status": { "type": "string", "enum": [ "draft", "published" ], "description": "A draft only ever reaches a test recipient; render it behind a test-preview note" }, "title": { "type": "string", "description": "Personalized for the caller" }, "body_html": { "type": "string", "description": "Sanitized HTML, personalized for the caller" }, "author_name": { "type": [ "string", "null" ] }, "author_avatar_url": { "type": [ "string", "null" ], "format": "uri" }, "published_at": { "type": [ "string", "null" ], "format": "date-time" } } }, "MessageCategory": { "type": "object", "required": [ "id", "name", "icon" ], "properties": { "id": { "type": "string", "format": "uuid" }, "name": { "type": "string" }, "icon": { "type": [ "string", "null" ], "description": "An emoji" } } }, "TrashedItem": { "type": "object", "required": [ "id", "type", "title", "project_id", "project_name", "trashed_at", "trashed_by_name" ], "properties": { "id": { "type": "string", "format": "uuid" }, "type": { "$ref": "#/components/schemas/RecordingType" }, "title": { "type": [ "string", "null" ], "description": "Comments list as Re: {thread title}" }, "project_id": { "type": [ "string", "null" ], "format": "uuid" }, "project_name": { "type": [ "string", "null" ] }, "trashed_at": { "type": "string", "format": "date-time" }, "trashed_by_name": { "type": [ "string", "null" ] } } }, "ReassignImpact": { "type": "object", "required": [ "count", "projects" ], "properties": { "count": { "type": "integer", "description": "Open to-dos affected" }, "projects": { "type": "array", "items": { "type": "object", "required": [ "id", "name", "count" ], "properties": { "id": { "type": "string", "format": "uuid" }, "name": { "type": "string" }, "count": { "type": "integer" } } } } } }, "ProjectLink": { "type": "object", "description": "An external link shown with the project's tools.", "required": [ "id", "url", "title", "visible_to_clients", "position", "created_at" ], "properties": { "id": { "type": "string", "format": "uuid" }, "url": { "type": "string", "format": "uri" }, "title": { "type": "string", "maxLength": 26 }, "description": { "type": [ "string", "null" ] }, "visible_to_clients": { "type": "boolean" }, "position": { "type": "number" }, "created_at": { "type": "string", "format": "date-time" } } }, "ProjectEvent": { "type": "object", "description": "One row of the project timeline.", "required": [ "id", "action", "created_at" ], "properties": { "id": { "type": "string", "format": "uuid" }, "action": { "type": "string", "description": "What happened: created, completed, archived, …" }, "detail": { "type": [ "object", "null" ], "additionalProperties": true }, "recording_id": { "type": [ "string", "null" ], "format": "uuid" }, "recording_title": { "type": [ "string", "null" ] }, "recording_type": { "type": [ "string", "null" ], "description": "A RecordingType value, or null for project-level events" }, "actor_name": { "type": [ "string", "null" ] }, "created_at": { "type": "string", "format": "date-time" }, "excerpt": { "type": [ "string", "null" ], "description": "Plain text pulled from the recording's content" } } }, "ProjectPerson": { "type": "object", "description": "One person with access to the project.", "required": [ "membership_id", "name", "role" ], "properties": { "membership_id": { "type": "string", "format": "uuid" }, "name": { "type": "string" }, "email": { "type": [ "string", "null" ] }, "image": { "type": [ "string", "null" ], "description": "Same-origin path or external URL; null means render an initials disc" }, "role": { "type": "string", "enum": [ "owner", "admin", "member", "client" ] }, "title": { "type": [ "string", "null" ] }, "company_name": { "type": [ "string", "null" ] } } }, "HealthUpdate": { "type": "object", "description": "One saved move of the health needle, newest first in the gauge's updates.", "required": [ "id", "created_at" ], "properties": { "id": { "type": "string", "format": "uuid", "description": "A health_update recording: commentable, cheerable, trashable" }, "status": { "type": [ "string", "null" ], "enum": [ "on_track", "some_risk", "concerned", null ] }, "position": { "type": [ "number", "null" ], "minimum": 0, "maximum": 100 }, "previous_status": { "type": [ "string", "null" ], "enum": [ "on_track", "some_risk", "concerned", null ] }, "previous_position": { "type": [ "number", "null" ], "minimum": 0, "maximum": 100 }, "note": { "type": [ "string", "null" ], "description": "Sanitized HTML" }, "visible_to_clients": { "type": "boolean" }, "comments_count": { "type": "integer" }, "creator_name": { "type": [ "string", "null" ] }, "created_at": { "type": "string", "format": "date-time" } } }, "HealthGauge": { "type": "object", "description": "The project health gauge and its update history.", "required": [ "enabled", "status", "position", "updated_at", "updates" ], "properties": { "enabled": { "type": "boolean" }, "status": { "type": [ "string", "null" ], "enum": [ "on_track", "some_risk", "concerned", null ], "description": "null until the first update" }, "position": { "type": [ "number", "null" ], "minimum": 0, "maximum": 100 }, "updated_at": { "type": [ "string", "null" ], "format": "date-time" }, "updates": { "type": "array", "items": { "$ref": "#/components/schemas/HealthUpdate" } } } }, "ProjectTrashItem": { "type": "object", "description": "One trashed recording on the project trash page.", "required": [ "id", "type", "trashed_at" ], "properties": { "id": { "type": "string", "format": "uuid" }, "type": { "$ref": "#/components/schemas/RecordingType" }, "title": { "type": [ "string", "null" ] }, "trashed_at": { "type": "string", "format": "date-time" }, "trashed_by_name": { "type": [ "string", "null" ] } } }, "RoadmapMarker": { "type": "object", "description": "An org-wide roadmap marker (bc3 lineup_markers conventions).", "required": [ "id", "name", "date", "created_at", "updated_at" ], "properties": { "id": { "type": "string", "format": "uuid" }, "name": { "type": "string" }, "date": { "type": "string", "format": "date" }, "created_at": { "type": "string", "format": "date-time" }, "updated_at": { "type": "string", "format": "date-time" } } }, "RecordingRecurrence": { "type": "object", "description": "The repeat schedule for a recurring to-do series (stored as meta.recurrence, the calendar's Recurrence shape). Anchored on starts_on when the to-do spans days, else due_on.", "required": [ "frequency" ], "properties": { "frequency": { "type": "string", "enum": [ "daily", "weekly", "monthly", "yearly", "custom" ] }, "interval": { "type": "integer", "minimum": 1, "maximum": 52 }, "by_weekday": { "type": "array", "items": { "type": "integer", "minimum": 0, "maximum": 6 }, "maxItems": 7 }, "monthly_mode": { "type": "string", "enum": [ "day_of_month", "nth_weekday" ] }, "until": { "type": "string", "format": "date", "description": "Caps the last occurrence" } } }, "QuestionSchedule": { "type": "object", "description": "Check-in questions only: when the question is asked.", "required": [ "frequency", "hour" ], "properties": { "frequency": { "type": "string", "enum": [ "daily", "weekly", "every_other_week", "monthly" ] }, "days": { "type": "array", "items": { "type": "integer", "minimum": 0, "maximum": 6 }, "minItems": 1, "description": "daily: which weekdays" }, "day_of_week": { "type": "integer", "minimum": 0, "maximum": 6, "description": "weekly / every_other_week" }, "day_of_month": { "type": "integer", "minimum": 1, "maximum": 28, "description": "monthly" }, "hour": { "type": "integer", "minimum": 0, "maximum": 23 }, "minute": { "type": "integer", "minimum": 0, "maximum": 59 } } }, "RecordingContentAttachment": { "type": "object", "description": "Structured metadata for one file embedded in a recording's rich text.", "required": [ "id", "filename", "content_type", "byte_size", "previewable", "url", "download_url" ], "properties": { "id": { "type": "string", "format": "uuid" }, "filename": { "type": "string" }, "content_type": { "type": [ "string", "null" ] }, "byte_size": { "type": [ "integer", "null" ] }, "previewable": { "type": "boolean" }, "url": { "type": "string" }, "download_url": { "type": "string" } } }, "RecordingDetail": { "allOf": [ { "$ref": "#/components/schemas/Recording" }, { "type": "object", "required": [ "content_attachments" ], "properties": { "creator_name": { "type": [ "string", "null" ], "description": "Author byline; survives the person leaving the org" }, "creator_image": { "type": [ "string", "null" ] }, "content_attachments": { "type": "array", "items": { "$ref": "#/components/schemas/RecordingContentAttachment" }, "description": "Files embedded in the rich text; always present, [] when none" } } } ] }, "ChildListRow": { "allOf": [ { "$ref": "#/components/schemas/Recording" }, { "type": "object", "properties": { "creator_name": { "type": [ "string", "null" ] }, "creator_image": { "type": [ "string", "null" ] }, "comment_count": { "type": "integer" }, "last_comment_at": { "type": [ "string", "null" ], "format": "date-time" }, "cheers": { "type": "array", "items": { "$ref": "#/components/schemas/InlineCheer" }, "description": "chat_message rows only" }, "assignees": { "type": "array", "items": { "type": "object", "required": [ "membership_id", "name" ], "properties": { "membership_id": { "type": "string", "format": "uuid" }, "name": { "type": "string" } } }, "description": "client_approval rows only" } } } ] }, "CommentRow": { "allOf": [ { "$ref": "#/components/schemas/Recording" }, { "type": "object", "properties": { "creator_name": { "type": [ "string", "null" ] }, "creator_image": { "type": [ "string", "null" ] } } } ] }, "InlineCheer": { "type": "object", "description": "A cheer as embedded on chat rows and change-log events.", "required": [ "id", "content", "membership_id", "person" ], "properties": { "id": { "type": "string", "format": "uuid" }, "content": { "type": "string" }, "membership_id": { "type": "string", "format": "uuid" }, "person": { "type": [ "string", "null" ] }, "image": { "type": [ "string", "null" ], "description": "Chat rows only" } } }, "RecordingCheerRow": { "type": "object", "description": "One row of the standalone cheers listing.", "required": [ "id", "content", "membership_id", "name", "created_at" ], "properties": { "id": { "type": "string", "format": "uuid" }, "content": { "type": "string" }, "membership_id": { "type": "string", "format": "uuid" }, "name": { "type": [ "string", "null" ] }, "created_at": { "type": "string", "format": "date-time" } } }, "RecordingEvent": { "type": "object", "description": "One change-log row: what happened, who did it, and its cheers.", "required": [ "id", "action", "created_at", "cheers" ], "properties": { "id": { "type": "string", "format": "uuid" }, "action": { "type": "string", "description": "created, completed, moved, version_added, visibility/hold/comment toggles, ..." }, "detail": { "type": [ "object", "null" ], "additionalProperties": true, "description": "moved rows carry fromParentTitle/toParentTitle captured at move time" }, "created_at": { "type": "string", "format": "date-time" }, "actor_id": { "type": [ "string", "null" ], "format": "uuid" }, "actor_name": { "type": [ "string", "null" ] }, "cheers": { "type": "array", "items": { "$ref": "#/components/schemas/InlineCheer" } } } }, "RecordingReference": { "type": "object", "description": "A recording whose content links to this one (the References tab).", "required": [ "id", "type", "referenced_at" ], "properties": { "id": { "type": "string", "format": "uuid" }, "type": { "$ref": "#/components/schemas/RecordingType" }, "title": { "type": [ "string", "null" ] }, "project_id": { "type": [ "string", "null" ], "format": "uuid" }, "project_name": { "type": [ "string", "null" ] }, "creator_name": { "type": [ "string", "null" ] }, "referenced_at": { "type": "string", "format": "date-time" }, "excerpt": { "type": [ "string", "null" ] } } }, "RecordingPersonRow": { "type": "object", "description": "One person on an assignee or subscriber list.", "required": [ "membership_id", "name" ], "properties": { "membership_id": { "type": "string", "format": "uuid" }, "name": { "type": [ "string", "null" ] }, "email": { "type": [ "string", "null" ] } } }, "RecordingBatchResult": { "type": "object", "description": "What a bulk verb touched.", "required": [ "count", "ids" ], "properties": { "count": { "type": "integer", "description": "Rows the action actually changed" }, "ids": { "type": "array", "items": { "type": "string", "format": "uuid" }, "description": "The affected ids; for copy, the copies' ids" }, "group_id": { "type": "string", "format": "uuid", "description": "group action only: the created group" } } }, "CheckInSchedule": { "type": "object", "description": "When a check-in question asks. Daily uses `days`; weekly and every_other_week use `day_of_week`; monthly means the first `day_of_week` of the month unless `day_of_month` is set.", "required": [ "frequency", "hour" ], "properties": { "frequency": { "type": "string", "enum": [ "daily", "weekly", "every_other_week", "monthly" ] }, "days": { "type": "array", "items": { "type": "integer", "minimum": 0, "maximum": 6 }, "minItems": 1, "description": "Daily only: which weekdays to ask (0=Sunday to 6=Saturday; default Monday to Friday, weekends allowed)" }, "day_of_week": { "type": "integer", "minimum": 0, "maximum": 6, "description": "Weekly, every_other_week, and default monthly shape; the every-other-week alternation anchors at creation" }, "day_of_month": { "type": "integer", "minimum": 1, "maximum": 28, "description": "Monthly alternative to the first-weekday shape" }, "hour": { "type": "integer", "minimum": 0, "maximum": 23 }, "minute": { "type": "integer", "minimum": 0, "maximum": 59 } } }, "CheckInAskee": { "type": "object", "required": [ "membership_id", "name" ], "properties": { "membership_id": { "type": "string", "format": "uuid" }, "name": { "type": "string" } } }, "ProgressDot": { "type": "object", "description": "One tracked to-do list on the progress chart.", "required": [ "id", "title", "position", "color" ], "properties": { "id": { "type": "string", "format": "uuid", "description": "The todolist's id" }, "title": { "type": [ "string", "null" ] }, "position": { "type": "number", "minimum": 0, "maximum": 1, "description": "0 = working it out, 1 = getting it done" }, "color": { "type": "string", "description": "Assigned at first track, stable across re-tracks" } } }, "ProgressSnapshotDot": { "type": "object", "description": "A dot as captured in a saved update's snapshot.", "additionalProperties": true, "properties": { "id": { "type": "string", "format": "uuid" }, "title": { "type": [ "string", "null" ] }, "color": { "type": "string" }, "position": { "type": "number", "minimum": 0, "maximum": 1 } } }, "PublicLinkState": { "type": "object", "required": [ "published", "url" ], "properties": { "published": { "type": "boolean" }, "url": { "type": [ "string", "null" ], "format": "uri", "description": "The /public/{token} capability URL: anyone holding it can read the sanitized content" } } }, "ProjectFolder": { "type": "object", "description": "An account-wide home-screen grouping of projects.", "required": [ "id", "name", "color", "position", "created_at" ], "properties": { "id": { "type": "string", "format": "uuid" }, "name": { "type": "string" }, "color": { "type": [ "string", "null" ] }, "position": { "type": "number" }, "created_at": { "type": "string", "format": "date-time" } } }, "UploadVersion": { "type": "object", "description": "A kept past version of a file.", "required": [ "id", "filename", "byte_size", "content_type", "created_at", "download_url" ], "properties": { "id": { "type": "string", "format": "uuid" }, "filename": { "type": "string" }, "byte_size": { "type": "integer" }, "content_type": { "type": "string" }, "created_at": { "type": "string", "format": "date-time" }, "download_url": { "type": "string", "description": "Relative API path of this version's download redirect" } } }, "EventCheer": { "type": "object", "required": [ "id", "content", "membership_id", "person" ], "properties": { "id": { "type": "string", "format": "uuid" }, "content": { "type": "string", "description": "Up to 16 characters of text or emoji" }, "membership_id": { "type": "string", "format": "uuid" }, "person": { "type": [ "string", "null" ] } } } }, "parameters": { "orgSlug": { "name": "orgSlug", "in": "path", "required": true, "schema": { "type": "string" }, "description": "The organization's URL slug; list yours via GET /api/v1/authorization or /api/v1/orgs" }, "page": { "name": "page", "in": "query", "schema": { "type": "integer", "minimum": 1 }, "description": "1-based page number" }, "perPage": { "name": "per_page", "in": "query", "schema": { "type": "integer", "minimum": 1, "maximum": 100 }, "description": "Page size, capped at 100" } }, "responses": { "Unauthenticated": { "description": "No valid credential (invalid, expired, or revoked tokens included)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "Forbidden": { "description": "Authenticated but not allowed (role, client visibility, read-scope write, session-only route)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "NotFound": { "description": "Missing or not yours: cross-tenant probes are indistinguishable from missing", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "Validation": { "description": "The body parsed but a field is invalid", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "PlanLimit": { "description": "The organization's plan cap was reached (code limit_reached)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "RateLimited": { "description": "Over the 50-requests-per-10-seconds token budget; honor Retry-After (seconds)", "headers": { "Retry-After": { "schema": { "type": "integer" }, "description": "Seconds to wait" } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } }