# SaaS Changelog (/docs/changelog/saas) Release notes for the CentraKit SaaS web application. # saas ## 0.1.1 ### Patch Changes - [#82](https://github.com/ScaleDockHQ/CentraKit/pull/82) [`9e82eee`](https://github.com/ScaleDockHQ/CentraKit/commit/9e82eeec1cb77b1cab9a9e475c1c612aa94d9448) Thanks [@mick340](https://github.com/mick340)! - Remove the Sentry feedback button. Bug reports go through the Marker.io widget only, so pages no longer show two report buttons. - [#82](https://github.com/ScaleDockHQ/CentraKit/pull/82) [`fd21c6c`](https://github.com/ScaleDockHQ/CentraKit/commit/fd21c6c35b19a3487b78fe640d980a8db5b0f659) Thanks [@mick340](https://github.com/mick340)! - Restructure the local demo seeds. `test@example.com` now owns four shop organizations (a tractor repair shop, an auto garage, a bicycle shop and an equestrian center), each with its own staff, customers and industry data. Every seeded person has a distinct name, every document, workflow, billing and people status has at least one example row, and `test5@example.com` adds a portal-only stable boarder. The local development guide lists every seeded login. ## 0.1.0 ### Minor Changes - [#76](https://github.com/ScaleDockHQ/CentraKit/pull/76) [`07c9cdf`](https://github.com/ScaleDockHQ/CentraKit/commit/07c9cdf13b7b35c6d40659bcdafe605f4905f504) Thanks [@mick340](https://github.com/mick340)! - Fix the issues found while testing the People module. - Submitted and approved timesheet weeks are locked, hours are grouped by the employee's time zone, and editing a timesheet cell no longer removes timer entries. - Time off counts pending requests, respects the maximum negative balance, asks for confirmation before approving into a negative balance, and shows sick leave details only to the employee, their managers and approvers. - Approved absences appear in planning, half days show their times, and requests without a manager go to an approver or are approved automatically. - The default schedule is 40 hours, working hours can follow the contract, and equipment can be assigned while creating an employee. - Hiring creates the contract and copies the CV, candidate emails use the applicant's language, and the careers page has its own public layout. - [`90bed53`](https://github.com/ScaleDockHQ/CentraKit/commit/90bed5373f5dec65721ac83078346362ee234953) Thanks [@martijn00](https://github.com/martijn00)! - Adopt the CentraKit repo standard. Breaking: API errors now use RFC 9457 problem details (`application/problem+json` with `type`, `title`, `status`, `detail` and `code`). This applies to REST routes and public token routes. Clients that read the old `{ error }` body must read `code` and `detail` instead. The CLI and the generated API client already use the new shape. - MCP clients can register themselves through OAuth dynamic client registration. - Audit logs record which columns changed on an update, keep old and new values in restricted details, and never store values of token, secret or credential columns. Updates that only touch timestamps or leases no longer create an audit event. - Forms use TanStack Form with inline validation. - The docs site has a docs MCP server, a changelog page built from `CHANGELOG.md`, and a last-updated date on every page. ### Patch Changes - [#76](https://github.com/ScaleDockHQ/CentraKit/pull/76) [`140fc6c`](https://github.com/ScaleDockHQ/CentraKit/commit/140fc6c0d6c23131f61964fa539883592b2624fd) Thanks [@mick340](https://github.com/mick340)! - Sign-in, registration and the other sign-in steps load from the prerendered page again instead of rendering again in the browser, so the forms are ready to use straight away. - [#76](https://github.com/ScaleDockHQ/CentraKit/pull/76) [`b04ab42`](https://github.com/ScaleDockHQ/CentraKit/commit/b04ab420044248ae52a30536862ab7f3d60b2aa2) Thanks [@mick340](https://github.com/mick340)! - Fix several issues found while restoring the end-to-end tests. - A first visit no longer reloads the page when the offline service worker takes control. - With the sidebar collapsed, the user menu, organization switcher and navigation groups stay open after you click them. - Switching the language on the sign-in pages now also translates the other sign-in pages. - The dashboard pipeline progress bar and the activity charts have accessible names, and the organization switcher keeps its name for screen readers when the sidebar is collapsed. - [#76](https://github.com/ScaleDockHQ/CentraKit/pull/76) [`53413cf`](https://github.com/ScaleDockHQ/CentraKit/commit/53413cf8ff0b6b6b4edaa38ab5b2a196d313e013) Thanks [@mick340](https://github.com/mick340)! - Fix the open bug reports from the latest feedback round. - Users can save their own name, profile photo and interface language again. - Admins who belong to several organizations can sign in again, because their access token stays small. - A new profile photo shows right after saving and no longer renders zoomed in, and the working hours editor fits its card again. - Default leave types such as Vacation are shown in the user's language. - A new invoice from a template includes the template lines and discount. - New brands can get tags while they are created, and selecting a tag twice no longer shows a database error. - After creating a team, tag, brand or other record, the back button returns to the overview instead of the create form. - The task list updates subtask progress after changes on the task page. - Creating a task from an accepted quote asks whether to take over the quote materials, which then go on the task right away. - The finish dialog mentions quote materials that are not added to the task yet. - Clicking into a note no longer opens the reference suggestions. - Creating a quote or invoice without a customer or title now opens the missing field with its error and marks it in red. - Sending a quote or invoice to a customer without an email address asks for a billing email and sends right after saving it. - [#78](https://github.com/ScaleDockHQ/CentraKit/pull/78) [`6921172`](https://github.com/ScaleDockHQ/CentraKit/commit/6921172b1f8ef62a53f43c2e12477289564c4b89) Thanks [@mick340](https://github.com/mick340)! - Updated the AI SDK, Chat SDK, Workflow, Resend, Nodemailer and Zod packages to their latest patch and minor releases. # Changelog (/docs/changelog) Product releases for CentraKit across every app. # Changelog Product releases for CentraKit. Every release lists its changesets here; per-app details live in [apps/saas](apps/saas/CHANGELOG.md), [apps/api](apps/api/CHANGELOG.md) and [apps/cli](apps/cli/CHANGELOG.md). ## 0.1.1 (2026-10-03) - Remove the Sentry feedback button. Bug reports go through the Marker.io widget only, so pages no longer show two report buttons. - Restructure the local demo seeds. `test@example.com` now owns four shop organizations (a tractor repair shop, an auto garage, a bicycle shop and an equestrian center), each with its own staff, customers and industry data. Every seeded person has a distinct name, every document, workflow, billing and people status has at least one example row, and `test5@example.com` adds a portal-only stable boarder. The local development guide lists every seeded login. ## 0.1.0 (2026-10-02) - Sign-in, registration and the other sign-in steps load from the prerendered page again instead of rendering again in the browser, so the forms are ready to use straight away. - The CLI now asks for missing values when it runs in a terminal: emails, names and passwords for `register` and `forgot-password`, and an organization picker for every command that takes an organization ID. `logout` and `orgs delete` ask for confirmation unless you pass `--yes`, and pressing Ctrl+C at a prompt exits with status 130. `register` no longer takes the password as an argument; enter it at the prompt or pipe it in with `--password-stdin`. Settings can now also come from `centrakit.config.ts` or `.centrakitrc` files, with `defineConfig` exported from `centrakit/config`. Help is available for every subcommand, unknown subcommands suggest the closest match, `--debug` prints stack traces, output uses colors unless `--no-color` or `NO_COLOR` is set, and the CLI now needs Node.js 24 or later. - Rewrite the product, admin, developer, getting started, architecture and operations docs to match the shipped features, with new pages for quotes, invoices, orders, products, people, hiring, time, expenses, insights, inbox, webhooks and AI agents. The docs MCP server now runs on the official MCP SDK with the standard Fumadocs `list_pages`, `get_page` and `search` tools. Markdown output for agents renders cards and callouts as plain Markdown, API reference pages include only their own operations, and Markdown responses negotiated through the `Accept` header carry `Vary: Accept`. Retired page URLs redirect to their replacements. - Fix several issues found while restoring the end-to-end tests. - A first visit no longer reloads the page when the offline service worker takes control. - With the sidebar collapsed, the user menu, organization switcher and navigation groups stay open after you click them. - Switching the language on the sign-in pages now also translates the other sign-in pages. - The dashboard pipeline progress bar and the activity charts have accessible names, and the organization switcher keeps its name for screen readers when the sidebar is collapsed. - Fix the issues found while testing the People module. - Submitted and approved timesheet weeks are locked, hours are grouped by the employee's time zone, and editing a timesheet cell no longer removes timer entries. - Time off counts pending requests, respects the maximum negative balance, asks for confirmation before approving into a negative balance, and shows sick leave details only to the employee, their managers and approvers. - Approved absences appear in planning, half days show their times, and requests without a manager go to an approver or are approved automatically. - The default schedule is 40 hours, working hours can follow the contract, and equipment can be assigned while creating an employee. - Hiring creates the contract and copies the CV, candidate emails use the applicant's language, and the careers page has its own public layout. - Fix the open bug reports from the latest feedback round. - Users can save their own name, profile photo and interface language again. - Admins who belong to several organizations can sign in again, because their access token stays small. - A new profile photo shows right after saving and no longer renders zoomed in, and the working hours editor fits its card again. - Default leave types such as Vacation are shown in the user's language. - A new invoice from a template includes the template lines and discount. - New brands can get tags while they are created, and selecting a tag twice no longer shows a database error. - After creating a team, tag, brand or other record, the back button returns to the overview instead of the create form. - The task list updates subtask progress after changes on the task page. - Creating a task from an accepted quote asks whether to take over the quote materials, which then go on the task right away. - The finish dialog mentions quote materials that are not added to the task yet. - Clicking into a note no longer opens the reference suggestions. - Creating a quote or invoice without a customer or title now opens the missing field with its error and marks it in red. - Sending a quote or invoice to a customer without an email address asks for a billing email and sends right after saving it. - Adopt the CentraKit repo standard. Breaking: API errors now use RFC 9457 problem details (`application/problem+json` with `type`, `title`, `status`, `detail` and `code`). This applies to REST routes and public token routes. Clients that read the old `{ error }` body must read `code` and `detail` instead. The CLI and the generated API client already use the new shape. - MCP clients can register themselves through OAuth dynamic client registration. - Audit logs record which columns changed on an update, keep old and new values in restricted details, and never store values of token, secret or credential columns. Updates that only touch timestamps or leases no longer create an audit event. - Forms use TanStack Form with inline validation. - The docs site has a docs MCP server, a changelog page built from `CHANGELOG.md`, and a last-updated date on every page. - Updated the AI SDK, Chat SDK, Workflow, Resend, Nodemailer and Zod packages to their latest patch and minor releases. # API Changelog (/docs/changelog/api) Release notes for the CentraKit REST and MCP API. # api ## 0.1.1 ### Patch Changes - [#82](https://github.com/ScaleDockHQ/CentraKit/pull/82) [`9e82eee`](https://github.com/ScaleDockHQ/CentraKit/commit/9e82eeec1cb77b1cab9a9e475c1c612aa94d9448) Thanks [@mick340](https://github.com/mick340)! - Remove the Sentry feedback button. Bug reports go through the Marker.io widget only, so pages no longer show two report buttons. ## 0.1.0 ### Minor Changes - [#76](https://github.com/ScaleDockHQ/CentraKit/pull/76) [`07c9cdf`](https://github.com/ScaleDockHQ/CentraKit/commit/07c9cdf13b7b35c6d40659bcdafe605f4905f504) Thanks [@mick340](https://github.com/mick340)! - Fix the issues found while testing the People module. - Submitted and approved timesheet weeks are locked, hours are grouped by the employee's time zone, and editing a timesheet cell no longer removes timer entries. - Time off counts pending requests, respects the maximum negative balance, asks for confirmation before approving into a negative balance, and shows sick leave details only to the employee, their managers and approvers. - Approved absences appear in planning, half days show their times, and requests without a manager go to an approver or are approved automatically. - The default schedule is 40 hours, working hours can follow the contract, and equipment can be assigned while creating an employee. - Hiring creates the contract and copies the CV, candidate emails use the applicant's language, and the careers page has its own public layout. - [`90bed53`](https://github.com/ScaleDockHQ/CentraKit/commit/90bed5373f5dec65721ac83078346362ee234953) Thanks [@martijn00](https://github.com/martijn00)! - Adopt the CentraKit repo standard. Breaking: API errors now use RFC 9457 problem details (`application/problem+json` with `type`, `title`, `status`, `detail` and `code`). This applies to REST routes and public token routes. Clients that read the old `{ error }` body must read `code` and `detail` instead. The CLI and the generated API client already use the new shape. - MCP clients can register themselves through OAuth dynamic client registration. - Audit logs record which columns changed on an update, keep old and new values in restricted details, and never store values of token, secret or credential columns. Updates that only touch timestamps or leases no longer create an audit event. - Forms use TanStack Form with inline validation. - The docs site has a docs MCP server, a changelog page built from `CHANGELOG.md`, and a last-updated date on every page. ### Patch Changes - [#76](https://github.com/ScaleDockHQ/CentraKit/pull/76) [`53413cf`](https://github.com/ScaleDockHQ/CentraKit/commit/53413cf8ff0b6b6b4edaa38ab5b2a196d313e013) Thanks [@mick340](https://github.com/mick340)! - Fix the open bug reports from the latest feedback round. - Users can save their own name, profile photo and interface language again. - Admins who belong to several organizations can sign in again, because their access token stays small. - A new profile photo shows right after saving and no longer renders zoomed in, and the working hours editor fits its card again. - Default leave types such as Vacation are shown in the user's language. - A new invoice from a template includes the template lines and discount. - New brands can get tags while they are created, and selecting a tag twice no longer shows a database error. - After creating a team, tag, brand or other record, the back button returns to the overview instead of the create form. - The task list updates subtask progress after changes on the task page. - Creating a task from an accepted quote asks whether to take over the quote materials, which then go on the task right away. - The finish dialog mentions quote materials that are not added to the task yet. - Clicking into a note no longer opens the reference suggestions. - Creating a quote or invoice without a customer or title now opens the missing field with its error and marks it in red. - Sending a quote or invoice to a customer without an email address asks for a billing email and sends right after saving it. # AI settings (/docs/admin/ai-settings) How system administrators enable AI Gateway models, pick default models per use case, edit per-function instructions, and set the agent policy and runtime defaults under /admin/ai. ## Overview [#overview] AI settings is the platform-level AI configuration at `/admin/ai`, under **Admin > Operations > AI**. It has three tabs: | Tab | Route | Purpose | | --- | --- | --- | | Models | `/admin/ai` | Enable models and choose the default model per use case. | | Instructions | `/admin/ai/instructions` | Edit system instructions and generation settings per AI function. | | Policy | `/admin/ai/policy` | Set the agent execution policy and runtime defaults. | These settings apply to every organization. Organization-level AI preferences and user-facing chat are described in [Chat](/docs/product/chat). ## Key concepts [#key-concepts] - **Model.** A model from the Vercel AI Gateway catalog, identified as `provider/model`. Only enabled models can be used. - **Use case.** A product surface with its own default model: Chat, Embeddings, Product recognition, Suggestions, Transcribe and Web search. - **AI function.** One prompt-driven capability, such as `chat.ask`, `chat.agent` or `contextual.customer.summarize`. Each function has English system instructions and generation settings. - **Customized or Default.** A function with saved overrides shows **Customized**; one that uses the shipped values shows **Default**. ## Enable models [#enable-models] The Models tab lists the AI Gateway catalog grouped by provider. Each provider row shows how many of its models are enabled, for example "4 of 84 enabled". Expand a provider to see its models with context window, release date, capability tags (shown in the Permissions column), and input and output cost. Toggle **Enable model** on a row to enable or disable it. Use **Search providers or models…** and **Filters** (Provider, Permissions, Status) to narrow the list. ## Choose default models [#choose-default-models] The **Default models** card has one selector per use case. Pick an enabled model for each; you must enable at least one model before you can set a default. The Chat default also powers the public docs assistant on the documentation site. The docs app reads the enabled Chat default and refreshes it every five minutes. When no enabled Chat default exists, the docs assistant falls back to `anthropic/claude-haiku-5.5`. New installations start with these defaults: Chat uses `anthropic/claude-opus-5.5`, Suggestions uses `anthropic/claude-haiku-5.5`, Product recognition uses `google/gemini-3.8-flash`, Embeddings uses `google/gemini-embedding-2`, Transcribe uses `openai/gpt-4o-transcribe` and Web search uses `perplexity/sonar`. Embedding defaults must produce 1536-dimensional vectors, because knowledge chunks are stored in that size. See [Knowledge](/docs/admin/knowledge). ## Edit function instructions [#edit-function-instructions] The Instructions tab lists every AI function with its name, key, category, status and last update. Categories include Chat, Contextual, Suggestions and Recognition. Filter by category or status, or search by name. Open a function at `/admin/ai/instructions/{functionKey}`, for example `/admin/ai/instructions/chat.ask`. Depending on the function, you can edit: | Field | Purpose | | --- | --- | | System instructions | The English system prompt for this function. | | Input template | The constructed input text sent with each request. | | Max steps | The maximum number of tool-calling steps. | | Max output tokens | The cap on generated tokens. | | Timeout (ms) | How long a call may run. | | Temperature | Optional sampling temperature. Empty uses the provider default. | Select **Save** to store an override, or **Reset to default** to remove it. Instructions stay in English. The output language and date and time formatting are applied at runtime from the signed-in user's preferences. ## Set the agent policy [#set-the-agent-policy] The **Agent policy** card on the Policy tab controls what Agent chats may do: - **Execution ceiling**: Agent, Review or Read. Agent allows mutations. Review and Read refuse mutations even after the user selects Approve. The default is Agent. - **Approval risk threshold**: Low, Medium, High or Critical. Tools at this risk level or higher need approval. The default is High. Access changes, admin actions and workflow starts always need approval. Ordinary create, update and delete tools run without approval unless they are high risk or always gated. ## Set runtime defaults [#set-runtime-defaults] The **Runtime defaults** card sets platform defaults that users can still override per turn: | Setting | Default | | ------------------------------------------ | -------------- | | Default composer mode (Ask, Plan or Agent) | Agent | | Default reasoning | high | | Default speed (Normal or Fast) | Normal | | Default context window | 200,000 tokens | | Suggestion count | 3 | | Suggestion min words and max words | 8 and 22 | | Knowledge retrieve limit and preload limit | 8 and 6 | | Memory always-on limit and semantic limit | 4 and 6 | | Memory min confidence | 0.85 | The agent policy and runtime defaults are stored as the `ai_agent_policy` and `ai_runtime_defaults` keys in [platform settings](/docs/admin/platform-settings). ## Permissions [#permissions] | Key | Effect | | --- | --- | | `platform.admin.access` | Opens the AI tabs in read-only form. | | `platform.aiModel.manage` | Enables the controls and is checked by the model, default, instruction, agent policy and runtime default saves. | ## Related pages [#related-pages] - [Knowledge](/docs/admin/knowledge) - [Platform settings](/docs/admin/platform-settings) - [Chat](/docs/product/chat) - [AI agents](/docs/developers/ai-agents) - [MCP server](/docs/developers/mcp) # Analytics (/docs/admin/analytics) The cross-organization business analytics page at /admin/analytics, with platform KPIs, deterministic forecasts, rule-based insights and source data exports for system administrators. ## Overview [#overview] Admin Analytics at `/admin/analytics` shows business analytics across every organization on the platform. Open it from **Admin > Operations > Analytics**. It follows the same rules as tenant analytics at `/{orgSlug}/insights/analytics`, but it reads platform data such as subscriptions and usage instead of one organization's sales. See [Insights](/docs/product/insights) for the tenant version. The page has two tabs: | Tab | Purpose | | --- | --- | | Overview | KPIs, a trend chart, composition, biggest movers and insights. | | Data & exports | The source evidence behind the numbers, with export. | ## Key concepts [#key-concepts] - **Internal data only.** Analytics reads CentraKit's own subscription and usage records. It does not pull data from external analytics tools. - **Separate currencies.** Amounts in different currencies are never added together. - **Deterministic forecasts.** Forecasts are computed from history with fixed rules, not from AI. A forecast needs at least six history points; with fewer, the chart shows "Forecast unavailable" and how many points exist. - **Rule-based insights.** Insights are triggered by fixed rules on the selected range. When no rule fires, the page says so. ## Choose a range [#choose-a-range] Both tabs share a filter bar: - **Date range**: Last 30 days, Last 90 days, Last 365 days or Custom. The selected dates show next to the selector. - **Comparison**: Previous period, Previous year or No comparison. The range uses the signed-in user's time zone. ## Read the overview [#read-the-overview] The Overview tab shows these KPIs, each compared with the comparison period when enough data exists: | KPI | Meaning | | --- | --- | | MRR | Monthly recurring revenue from platform subscriptions. | | Paid organization growth | The number of organizations on a paid plan. | | Plan mix | How organizations are spread across plans. | | Usage | Metered platform usage across organizations. | Below the KPIs are the trend chart with observed periods and any forecast, a **Composition** panel that ranks organizations and plans, **Biggest movers** between the two periods, and **Insights**. ## Export source data [#export-source-data] The Data & exports tab lists the source evidence: Date, Item, Metric, Source and Value. Rows include subscription MRR entries and usage meters such as AI tokens, API requests, inbox messages and file storage, with a usage cost proxy. Filter the table with **Filters**. Select **Export** to download the rows as CSV or Excel (XLSX). Export needs `platform.analytics.export`; without it, the button is not available. ## Permissions [#permissions] | Key | Effect | | --- | --- | | `platform.admin.access` | Opens the admin area. | | `platform.analytics.read` | Opens Admin Analytics. Without it, the page returns not found. | | `platform.analytics.export` | Exports the source data. | System Admin has both analytics keys. System Support has neither. ## Related pages [#related-pages] - [Operations dashboard](/docs/admin/dashboard) - [Billing](/docs/admin/billing) - [Plans](/docs/admin/plans) - [Insights](/docs/product/insights) # Announcements (/docs/admin/announcements) How system administrators publish in-app announcements at /admin/announcements, who sees them, and how users dismiss them. ## Overview [#overview] Announcements are banners for every signed-in user, such as "Scheduled maintenance tonight". Publish and remove them at `/admin/announcements`, under **Admin > Platform > Announcements**. ## Key concepts [#key-concepts] | Term | Meaning | | --- | --- | | Severity | Information, Success, Warning or Critical. Warning and Critical banners stand out in the app. | | Dismissible | When on, each user can hide the banner for themselves. When off, it stays until you remove it. | | Live | An announcement shows from the moment you publish it until you remove it. | Announcements are stored by the better-supabase `announcements` module. Open pages load the list again within seconds when an announcement is published or removed, over a private Realtime topic. ## Publish an announcement [#publish-an-announcement] 1. Open **Admin > Platform > Announcements**. 2. Enter a **Title** and, optionally, a **Message**. 3. Choose a **Severity** and whether **Users can dismiss it**. 4. Select **Publish**. The banner appears above the page content in every organization, for every signed-in user. ## Remove an announcement [#remove-an-announcement] Select **Remove** next to it in the list. The banner disappears for everyone, including users who had not dismissed it. ## Permissions [#permissions] | Key | Effect | | --- | --- | | `platform.admin.access` | Opens the admin area. | | `platform.announcement.manage` | Shows the Announcements page and publishes and removes banners. | Publishing and removing are checked on the server and recorded in the [audit log](/docs/admin/audit-log). Any signed-in user can read the live announcements and dismiss the dismissible ones for themselves. # Audit log (/docs/admin/audit-log) How system administrators search platform-wide audit events, inspect event details, reveal restricted fields and export events as CSV under /admin/audit-log. ## Overview [#overview] The admin audit log at `/admin/audit-log` shows audit events from every organization and from the platform itself. Open it from **Admin > Platform > Audit log**. | Route | Purpose | | ---------------------------- | ----------------------------------- | | `/admin/audit-log` | Search and filter all audit events. | | `/admin/audit-log/{eventId}` | One event with its full details. | Organization users with `auditLog.read` see their own organization's events at `/{orgSlug}/audit-log`. See [Audit log](/docs/product/audit-log). How events are captured and stored is described in [Audit logs architecture](/docs/architecture/audit-logs). ## Key concepts [#key-concepts] Each event records: - **Event**: a readable name such as "Updated Usage Periods" and a key such as `organization-usage-periods.updated`. - **Actor**: who caused the event. Actor kinds are user, platform admin, system, API key, integration, workflow and webhook. - **Target**: the affected record and its type. - **Organization**: the tenant the event belongs to. Platform events have no organization. - **Category**: authentication, users, permissions, billing, data, settings, integrations, security or system. - **Source**: where the event came from: the SaaS app, the API, MCP, AI, a workflow, a webhook, the database or the system. - **Outcome**: success, failure, denied or attempted. Denied attempts are logged too. For example, a user without `platform.user.manage` who tries to invite a user produces a denied event. ## Search events [#search-events] The list shows Timestamp, Event, Actor, Target, Organization, Category and Source / outcome, newest first. Search with **Search events and targets…** and narrow the list with **Filters**: - Event types, Categories, Actor kinds, Outcomes and Sources. - Actor ID, Target type, Target ID, Correlation ID and Organization ID. - Occurred on, for a date range. The row menu offers **View details**, **Open affected resource**, **View related events**, **Filter by actor**, **Filter by event type**, **Copy event ID** and **Copy request ID**. ## Inspect an event [#inspect-an-event] The detail page at `/admin/audit-log/{eventId}` shows: - The event name, key and outcome. - Event ID, actor, organization, local time and UTC time, source, category, target and target type. - **Structured changes**, such as the list of changed columns. Redacted columns are never shown. - **Request context**: request ID and correlation ID. - **Related events**, when other events are linked to this one. ## Reveal restricted details [#reveal-restricted-details] Some fields, such as IP address, session ID and user agent, are hidden by default. Select **Reveal restricted details** to show them. Revealing requires `platform.auditLog.reveal`, and the reveal itself is recorded as an audit event. ## Export events [#export-events] 1. Filter the list to the events you want. The export uses the current filters, and the **Occurred on** range must be valid and no longer than 90 days. 2. Open **Exports** from the toolbar. 3. Optionally select **Include restricted details**. This needs `platform.auditLog.reveal`. 4. Select **Create export**. The export runs as a background workflow. Its status moves through Pending, Running and Completed, or ends as Failed. Completed exports later show Expired. Select **Refresh** to update the list, then **Download export** to get a CSV file. The download link is short-lived, and a completed export expires after 24 hours. ## Permissions [#permissions] | Key | Effect | | --- | --- | | `platform.admin.access` | Opens the admin area. | | `platform.auditLog.read` | Opens the admin audit log. Without it, the page returns not found. | | `platform.auditLog.export` | Creates and downloads exports. | | `platform.auditLog.reveal` | Reveals restricted details and includes them in exports. | System Admin has all three audit log keys. System Support has `platform.auditLog.read` only. ## Related pages [#related-pages] - [Audit log](/docs/product/audit-log) - [Audit logs architecture](/docs/architecture/audit-logs) - [Users](/docs/admin/users) - [Roles and permissions](/docs/admin/roles-and-permissions) - [Plans](/docs/admin/plans) for retention per plan # Billing (/docs/admin/billing) How system administrators review and change organization subscriptions, manage subscription invoices, download invoice PDFs, refresh billing from Stripe and review Stripe payment accounts under /admin/subscriptions, /admin/invoices and /admin/payment-accounts. ## Overview [#overview] Admin billing covers the subscriptions organizations pay for the platform itself. It lives under **Admin > Finance**: | Route | Purpose | | --- | --- | | `/admin/subscriptions` | Every organization with its plan, status and open invoices. | | `/admin/subscriptions/{orgSlug}` | One organization's subscription and invoices. | | `/admin/invoices` | Every subscription invoice across organizations. | | `/admin/invoices/{invoiceId}` | One subscription invoice with amounts and lifecycle. | | `/admin/subscriptions/platform-fees` | The platform fee invoices and credit notes of a month, and the ICP listing download. | | `/admin/payment-accounts` | Every organization's Stripe payment account with its status, online payment volume and earned platform fee. | These are platform subscription invoices from Stripe, not the invoices organizations send to their own customers. Tenant invoicing is described in [Invoices](/docs/product/invoices). The Stripe application fee taken on customer invoice payments is set per plan in [Plans](/docs/admin/plans), with a default in [Platform settings](/docs/admin/platform-settings) and an optional override per organization. ## Key concepts [#key-concepts] ### Subscription statuses [#subscription-statuses] Active, Trialing, Past due, Unpaid, Paused, Incomplete, Incomplete expired, Canceled and No subscription. ### Invoice statuses [#invoice-statuses] Draft, Open, Paid, Uncollectible, Void, Upcoming and Deleted. ### Source of truth [#source-of-truth] Stripe owns subscriptions, invoices, payment methods and the billing contact. The Stripe Sync Engine stores them in the database as Stripe webhooks arrive, and the pages read those tables. **Refresh from Stripe** fetches the organization's subscriptions, invoices, payment methods and tax ids again when a webhook was missed. ## Review subscriptions [#review-subscriptions] The Subscriptions page starts with summary cards for organizations, active subscriptions, monthly recurring revenue and open invoices. The table shows Organization, Plan, Status, Monthly / MRR, Period end and Open invoices. Monthly recurring revenue uses the same definition as the admin dashboard MRR: the catalog price of each active or past due subscription, annual prices divided by twelve, multiplied by the subscription quantity. Search with **Search organizations…** and filter with **Filters**. Open a row to see the organization's billing detail: - **Plan**, **Stripe customer** and **Billing email**. - **Operations**: **Refresh from Stripe** fetches the latest subscription, invoices, payment methods and tax ids into the Sync Engine tables. - **Plan and billing cycle**: the current plan, credit package and interval. - **Platform fee per online payment**: the fee the organization pays now and where it comes from. - **Current period**, **Default payment method** and **Open invoices**. - **Invoice history** for this organization. ## Change a subscription [#change-a-subscription] 1. Open the organization at `/admin/subscriptions/{orgSlug}`. 2. In **Plan and billing cycle**, pick the plan, credit package and billing interval (Monthly or Yearly). 3. Select **Change subscription** and confirm. Changes use Stripe's standard proration and may immediately create an adjustment invoice. ## Schedule or undo a cancellation [#schedule-or-undo-a-cancellation] Select **Cancel at period end** and confirm to schedule a cancellation at the end of the current billing period. While a cancellation is scheduled, select **Undo cancellation** to keep the subscription active. ## Override the platform fee for one organization [#override-the-platform-fee-for-one-organization] The platform fee per online invoice payment comes from the first of these that is set: 1. The organization override, for agreements such as an Enterprise contract. 2. The fee of the organization's plan, set in [Plans](/docs/admin/plans). 3. The platform default, set in [Platform settings](/docs/admin/platform-settings). The **Platform fee per online payment** card on `/admin/subscriptions/{orgSlug}` shows the current fee with its source (Organization override, Plan, or Platform default) next to the platform default. With `platform.billing.manage`, choose a **Fee type**, enter the fee, optionally add a **Note** such as the contract reference, and select **Set override**. **Remove override** returns the organization to its plan fee. New online payments use the new fee. Each change is recorded in the [audit log](/docs/admin/audit-log). ## Manage subscription invoices [#manage-subscription-invoices] The Invoices page lists Invoice, Organization, Plan, Status, Due, Paid, Remaining, Issued and Due date. Search with **Search invoices or organizations…**. The row menu offers **Open invoice**, **View hosted invoice** and **Download PDF**. The invoice detail page shows identifiers (local invoice ID, Stripe invoice ID, Stripe subscription ID and organization ID), amounts, lifecycle dates and the subscription context. For an invoice with the status Open, you can: - **Void invoice**: cancel the invoice in Stripe. - **Mark uncollectible**: record that the invoice will not be paid. Both actions ask for confirmation. ## Download an invoice PDF [#download-an-invoice-pdf] **Download PDF** calls the API route below, which checks `platform.billing.read` and then redirects to the Stripe invoice PDF. ```text GET /api/v1/admin/subscriptions/{orgSlug}/invoices/{invoiceId}/pdf ``` The route returns an RFC 9457 problem response with the code `BILLING_FORBIDDEN` (403), `BILLING_VALIDATION` (400) or `BILLING_NOT_FOUND` (404) when the request cannot be served. See [Errors](/docs/developers/errors). ## Platform fee invoices [#platform-fee-invoices] ScaleDock invoices the platform fee it earns on online invoice payments itself. By the 15th of each month every organization with fees from the previous month receives one collective invoice per VAT treatment and rate, marked as already settled through Stripe, and a credit note when a fee was refunded after it was invoiced. Dutch organizations pay 21% VAT, EU organizations with a VAT number that VIES confirms get reverse charge, and organizations outside the EU get no Dutch VAT. Each payment stores the VAT rate its fee was charged at, so a rate change in the platform settings starts a separate invoice instead of restating fees already charged. Open **Platform fees** under Admin > Finance (`/admin/subscriptions/platform-fees`) to review the documents of a month and open their PDFs. The **ICP listing** section downloads a CSV with the net amount invoiced under reverse charge per buyer VAT number for a quarter, with credit notes subtracted, for the quarterly intra-community listing. The issuer details on the documents come from the platform fee issuer settings in [Platform settings](/docs/admin/platform-settings), which must be complete before the monthly job can run. Both pages need the platform billing read permission. The PDFs and the CSV are served by the API at `/api/v1/admin/platform-fees/{id}/pdf` and `/api/v1/admin/platform-fees/icp?year=…&quarter=…`; an organization's own fee documents come from `/api/v1/organizations/{organizationId}/platform-fees/{id}/pdf`, which needs the billing manage permission on that organization. ## Payment accounts [#payment-accounts] Open **Payment accounts** under Admin > Finance (`/admin/payment-accounts`) to see the Stripe connected account of every organization that activated online payments or pays its plan through Stripe. Summary cards show the number of accounts, how many are ready for online payments, how many need attention, and the total online payment volume with the platform fee earned on it. The table shows the organization with its Stripe account id, brand and country, the status, the card payments and payouts capabilities, whether the plan is billed on the account, the online payment volume and the platform fee. The volume counts successful online invoice payments minus refunds and lost disputes. The platform fee is the fee net of VAT minus refunded fees. Search by organization name, slug or account id, and filter on status. | Status | Meaning | | --- | --- | | Ready | Stripe enabled the account and card payments are active. Customers can pay invoices online. | | Verification needed | Stripe needs more details from the organization. The deadline shows under the status, in red when it has passed. | | Restricted | Stripe restricted or disabled the account or its card payments. | | Onboarding not finished | The organization started online payments but has not completed Stripe onboarding. | | Plan billing only | The account only pays the organization's plan. Online payments were never activated. | **Open in Stripe** opens the connected account in the Stripe dashboard, in test or live mode to match the account. The page needs the platform billing read permission. ## Permissions [#permissions] | Key | Effect | | --- | --- | | `platform.admin.access` | Opens the admin area. | | `platform.billing.read` | Reads subscriptions and invoices, downloads PDFs and refreshes from Stripe. | | `platform.billing.manage` | Changes subscriptions, schedules or undoes cancellations, voids invoices and marks them uncollectible. | System Admin has both billing keys. System Support has `platform.billing.read` only. ## Related pages [#related-pages] - [Plans](/docs/admin/plans) - [Platform settings](/docs/admin/platform-settings) - [Operations dashboard](/docs/admin/dashboard) - [Billing and usage](/docs/product/billing-and-usage) - [Stripe billing operations](/docs/operations/stripe-billing) # Operations dashboard (/docs/admin/dashboard) The system admin landing page at /admin that summarizes tenants, subscriptions, revenue, failed payments, usage anomalies and operational failures across the platform. ## Overview [#overview] The operations dashboard is the landing page of the admin area at `/admin`. It gives system administrators a live, cross-organization view of platform health and a queue of items that may need action. Open it from **Admin > Dashboard** in the sidebar. The admin area is only available to users whose system role grants `platform.admin.access`; other users are redirected to the app home. Locally, the seeded `test@example.com` user holds the System Admin role. The dashboard reads existing operational data at request time and caches it for a few minutes. It does not store incidents, keep historical analytics or send notifications. For trends over time, use [Analytics](/docs/admin/analytics). ## Key concepts [#key-concepts] ### Metric cards [#metric-cards] | Card | What it counts | | --- | --- | | Active organizations | Enabled platform tenants. Disabled organizations are excluded. | | Active subscriptions | Subscriptions with the status active or past due. | | MRR | Catalog recurring revenue from active and past due subscriptions. | | Failed payments | Subscription invoices that are overdue or uncollectible. | | Usage anomalies | Current-period overages and usage spikes. | | Operational failures | Failures from agents, workflows, webhooks, integrations and deliveries in the last 24 hours. | MRR uses the recurring catalog price of each subscription. Annual prices are divided by twelve, each price is multiplied by the subscription quantity, and each currency is kept separate. Trials, canceled or paused subscriptions, usage overages and quote-only Enterprise amounts are not included. ### Subscriptions by plan [#subscriptions-by-plan] The plan distribution chart groups enabled organizations by their effective plan: Starter, Pro, Business or Enterprise. An organization without a paid subscription counts toward Starter. ### Recent signups [#recent-signups] This panel lists the latest user profiles created in the last 30 days. When nobody signed up in that window, it shows "No recent signups". ### Attention queue [#attention-queue] The attention queue merges payment, usage and operational signals into one list, newest first. Each item shows the organization, a short description, the time it happened and a severity of **Warning** or **Critical**. | Signal | Source | | --- | --- | | Payment needs attention | A subscription invoice that is overdue or uncollectible. | | Usage limit exceeded | A meter over its included quantity in the current period. | | Unusual usage spike | A material 24-hour spike against the prior seven-day baseline. | | Workflow run failed | A failed workflow run. | | Agent run needs attention | An AI agent run that failed. | | Incoming webhook failed | A Stripe, Resend or WhatsApp webhook that failed processing, was given up after its retries, or is still waiting after 15 minutes. | | Integration installation error | An integration installation in an error state. | | Calendar integration error | A calendar connection in an error state. | | Channel connection is unhealthy | An inbox channel that reports an unhealthy connection. | | Channel delivery failed | An outbound inbox message that failed to deliver. | | Email delivery failed | A transactional email that bounced or failed. | A payment item is Critical when the invoice is uncollectible or the subscription is unpaid, and Warning otherwise. Usage and operational items carry the severity the platform assigns to that signal. When there is nothing to act on, the queue shows "Nothing needs attention". ## Follow up on an attention item [#follow-up-on-an-attention-item] Select an item to open the admin page that owns it: - Payment items open the organization's subscription at `/admin/subscriptions/{orgSlug}`, where you can review open invoices and refresh billing from Stripe. See [Billing](/docs/admin/billing). - Workflow run failures open the run at `/admin/workflows/{workflowId}/runs/{runId}`. See [Workflows](/docs/admin/workflows). - Integration failures open the catalog entry at `/admin/integrations/{integrationSlug}`. See [Integrations](/docs/admin/integrations). - Usage anomalies and the remaining operational failures link to the organization when the failure belongs to one. Items without an organization have no link. If the dashboard fails to load, it shows "Operations dashboard is unavailable" with a **Try again** button. ## Permissions [#permissions] | Key | Effect | | --- | --- | | `platform.admin.access` | Opens the admin area, including the operations dashboard. | | `platform.job.manage` | Shows **Retry failed** on job queues with failed jobs and runs it. | The built-in System Admin and System Support roles both include `platform.admin.access`; only System Admin includes `platform.job.manage`. See [Roles and permissions](/docs/admin/roles-and-permissions). ## Related pages [#related-pages] - [Analytics](/docs/admin/analytics) - [Billing](/docs/admin/billing) - [Organizations](/docs/admin/organizations) - [Audit log](/docs/admin/audit-log) - [Stripe billing operations](/docs/operations/stripe-billing) # Feature flags (/docs/admin/feature-flags) How system administrators create feature flags at /admin/feature-flags, roll them out to a share of users and turn them on or off for single organizations. ## Overview [#overview] Feature flags turn a feature on for some users or organizations while it rolls out. Manage them at `/admin/feature-flags`, under **Admin > Platform > Feature Flags**. What an organization pays for stays with its [plan](/docs/admin/plans); flags only decide who sees a feature during a rollout. ## Key concepts [#key-concepts] | Term | Meaning | | --- | --- | | Key | The name the code checks, such as `new_editor`: lowercase letters, digits, dots, dashes or underscores, starting with a letter. | | Enabled | A disabled flag is off for everyone, whatever its rollout or overrides. | | Rollout percentage | The share of users the flag is on for. A user stays in or out of the rollout as long as the percentage does not drop below them. | | Organization override | Turns the flag on or off for every user of one organization, before the rollout is checked. | Flags are stored by the better-supabase `flags` module. The same rules decide a flag in the app and in database policies. ## Create a flag [#create-a-flag] 1. Open **Admin > Platform > Feature Flags**. 2. Enter a **Key** and a **Rollout percentage**, and choose whether the flag is **Enabled**. 3. Select **Create flag**. Saving a flag with an existing key changes that flag. ## Roll out and override [#roll-out-and-override] - Switch **Enabled** on a flag to turn it on or off for everyone. - Change the **Rollout percentage** and select **Save rollout**. - Under **Organization overrides**, enter an **Organization ID**, choose **On** or **Off** and select **Add override**. Select **Remove** to drop an override again. Select **Delete** to remove a flag with its overrides. ## Permissions [#permissions] | Key | Effect | | --- | --- | | `platform.admin.access` | Opens the admin area. | | `platform.flag.manage` | Shows the Feature Flags page and creates, changes and deletes flags. | Every change is checked on the server and recorded in the [audit log](/docs/admin/audit-log). ## Related pages [#related-pages] - [Plans](/docs/admin/plans) - [Platform settings](/docs/admin/platform-settings) # Integrations (/docs/admin/integrations) How system administrators manage the integration marketplace catalog under /admin/integrations, including availability, install scopes, categories, logos and resource links. ## Overview [#overview] Admin Integrations manages the marketplace catalog that organizations browse at `/{orgSlug}/integrations`. It lives at `/admin/integrations`, under **Admin > Operations > Integrations**. | Route | Purpose | | --- | --- | | `/admin/integrations` | List every integration in the catalog. | | `/admin/integrations/{integrationSlug}` | Edit one integration's catalog entry. | The catalog entries themselves ship with the platform. This page edits how each entry appears and how it can be installed; it does not create new integrations or change how a provider connects. Tenant installation and connection flows are described in [Integrations](/docs/product/integrations). ## Key concepts [#key-concepts] ### Status [#status] Each integration has a catalog status: **Available**, **Coming soon**, **Planned**, **Suggested** or **Deprecated**. Only Available integrations are ready to install; the other statuses label roadmap or retired entries in the marketplace. Separately, an integration is **Enabled** or disabled. Disabled integrations are hidden from tenant marketplace pages. ### Categories [#categories] Categories control marketplace filters: Inbox, Agenda, Messaging, Calendar, Native, Automation, Billing, Files and Coming soon. ## Review the catalog [#review-the-catalog] The list shows Integration (name, provider and slug), Status, Categories, Usage and Requests. Usage is the number of installations. Requests is the number of times organizations requested a requestable integration. Search the list and use **Filters** for visibility, catalog status and category. ## Edit an integration [#edit-an-integration] Open an integration, for example `/admin/integrations/google-calendar`. The page has four sections, each with **Save changes** and **Cancel**. ### Catalog details [#catalog-details] Edit the public name, provider, short description, long description and keywords. Upload, replace or remove the logo; logos accept SVG, PNG or WebP up to 5 MB. ### Availability [#availability] | Setting | Effect | | --- | --- | | Enabled | Shows or hides the integration in tenant marketplaces. | | Status | Sets the catalog status described above. | | Featured | Pins the integration to the top of the marketplace. | | Requestable | Lets organizations request the integration. | | Allow multiple installs | Allows more than one installation. | | Organization installs | Allows installation for the whole organization. | | User installs | Allows installation for an individual user. | ### Categories [#categories-1] Select the categories the integration appears under. ### Resources [#resources] Set the website, support, documentation, terms and privacy policy URLs shown on the integration detail page. ## Troubleshoot installations [#troubleshoot-installations] Integration installation errors appear in the attention queue on the [operations dashboard](/docs/admin/dashboard) and link back to the integration's admin page. ## Permissions [#permissions] | Key | Effect | | --- | --- | | `platform.admin.access` | Opens Admin Integrations in read-only form. | | `platform.integration.manage` | Enables the edit controls and is checked when saving catalog changes and logos. | ## Related pages [#related-pages] - [Integrations](/docs/product/integrations) - [Inbox](/docs/product/inbox) - [Agenda](/docs/product/agenda) - [Webhooks](/docs/developers/webhooks) - [Operations dashboard](/docs/admin/dashboard) # Knowledge (/docs/admin/knowledge) How system administrators manage platform-wide knowledge collections under /admin/knowledge, and how ingestion and hybrid retrieval work for AI chat. ## Overview [#overview] System knowledge holds platform-wide content for retrieval-augmented generation (RAG) in AI chat and tools. Manage it at `/admin/knowledge`, under **Admin > Operations > Knowledge**. | Route | Purpose | | --------------------------------- | --------------------------------------- | | `/admin/knowledge` | List system collections. | | `/admin/knowledge/create` | Create a collection. | | `/admin/knowledge/{collectionId}` | Edit a collection and manage its files. | System collections are shared with every organization. Each organization also has its own collections at `/{orgSlug}/knowledge`. The seeded "CentraKit product guide" collection is an example of a system collection. ## Key concepts [#key-concepts] ### Collections [#collections] A collection groups related files. It has a name, a description, a retrieval priority and a visibility: | Visibility | Meaning | | ---------- | --------------------------------------------------------------- | | Automatic | Everyone with knowledge access can use the collection in chats. | | Restricted | Only granted teams and users can use the collection. | | Disabled | The collection is excluded from retrieval. | Team and user grants are managed in the Access section of organization collections. System collection pages have no Access section, so use Automatic or Disabled for system content. Retrieval priority breaks ties between equally relevant results; a lower number ranks first. ### Files and statuses [#files-and-statuses] Files are uploads or web pages. Each file shows a status: **Pending**, **Processing**, **Ready**, **Ready (text only)**, **Failed**, **Stale** or **Deleted**. A failed file shows the reason next to its status. ## Create a collection [#create-a-collection] 1. Select **New collection**. 2. Enter a name, a retrieval priority and a description. 3. Choose a visibility. 4. Select **Create collection**. ## Add content [#add-content] On the collection detail page: - **Upload files**: drop files on the upload area or click to browse. Accepted formats are text, Markdown, CSV, JSON, XML, YAML and HTML, up to 10 MB each. - **Add a web page**: select **Add URL** and enter the address. Pages larger than 5 MB fail ingestion. GitHub and API connections are not yet available. From the files table you can **Delete** a file or **Reingest** it, for example after changing the embedding model. ## How ingestion works [#how-ingestion-works] Each new file starts the system `knowledge-ingestion` workflow: 1. The workflow extracts text. HTML is converted to plain text. 2. The text is split into chunks along headings and blocks, with a budget of about 1,600 characters per chunk. 3. Chunks are embedded with the Embeddings default model from [AI settings](/docs/admin/ai-settings). 4. Chunks are stored with a 1536-dimension vector and a full-text index, and the file becomes Ready. Only configure an embedding default that produces exactly 1536 dimensions. A model with the wrong size fails the file with an actionable error. A temporary provider outage marks the file **Ready (text only)**, so full-text search still works; reingest the file later to add vectors. ## How retrieval works [#how-retrieval-works] Chat and AI tools search knowledge with hybrid retrieval. Vector similarity and full-text rank are combined with reciprocal rank fusion, then ordered by retrieval priority. If the query embedding fails, search falls back to full text so chat keeps working. Retrieval always runs in the caller's organization and permission context. Row Level Security limits results to collections the user may read, so restricted collections need a grant and disabled collections never appear. The number of chunks retrieved per turn is set by the knowledge limits in [AI settings](/docs/admin/ai-settings). ## Permissions [#permissions] | Key | Effect | | --- | --- | | `platform.admin.access` | Opens the admin area. | | `platform.knowledge.manage` | Creates, edits and deletes system collections and their files. | Organization collections use the `organization.knowledge.*` keys instead. ## Related pages [#related-pages] - [AI settings](/docs/admin/ai-settings) - [Workflows](/docs/admin/workflows) - [Content](/docs/product/content) - [Chat](/docs/product/chat) - [Workflow operations](/docs/operations/workflows) # Organizations (/docs/admin/organizations) How system administrators list, create, edit, disable and delete tenant organizations under /admin/organizations. ## Overview [#overview] Admin Organizations is the platform-wide list of tenants at `/admin/organizations`. System administrators use it to find any organization, create new ones and change settings on behalf of a tenant. Each organization is addressed by its slug, not an internal ID: | Route | Purpose | | --- | --- | | `/admin/organizations` | List and filter organizations. | | `/admin/organizations/create` | Create a new organization. | | `/admin/organizations/{orgSlug}/settings` | Edit one organization's settings. | Tenant admins manage their own organization at `/{orgSlug}/settings`. See [Settings and branding](/docs/product/settings-and-branding). ## Key concepts [#key-concepts] - **Slug.** The URL identifier for an organization, used in every tenant route. See [Tenancy](/docs/architecture/tenancy). - **Enabled or disabled.** A disabled organization keeps all of its data, but tenant routes are blocked for its members. Disabling is reversible. - **Deleted.** Deleting an organization permanently removes it and all of its data. ## Find an organization [#find-an-organization] The list shows Organization, Slug, Website, Members and Updated. Search with **Search organizations…** and use **Filters**: - **Profile**: Has website or Has logo. - **Account status**: Enabled or Disabled. The row menu offers **Settings** and **Copy slug**. ## Create an organization [#create-an-organization] 1. Select **New organization**. 2. Fill in **General**: organization name, slug and website. 3. Optionally fill in **Company information**: address, Chamber of Commerce (KVK) number, VAT number (BTW), IBAN and phone. 4. Optionally add **Socials**: LinkedIn, X, Facebook, Instagram and YouTube. These links appear in the customer portal. 5. Optionally set **Branding**: a logo (PNG, JPEG or WebP up to 5 MB), primary and secondary colors, and up to 12 brand images of up to 8 MB each. 6. Select **Create organization**. The admin who creates the organization becomes its owner, and the app opens the new organization's admin settings page. ## Edit organization settings [#edit-organization-settings] The settings page at `/admin/organizations/{orgSlug}/settings` shows the same sections a tenant owner sees, each with its own **Save** button: | Section | Fields | | --- | --- | | General | Name, slug and website. | | Company information | Address, KVK number, VAT number, IBAN and phone, used on invoices and official correspondence. | | Socials | Social links shown in the portal. | | Finance | Base currency, default hourly rate, quote validity days, fiscal year start, number prefixes, invoice number format and VAT rates. | | Email | Inbox, forwarding and reply-to addresses. | | Other settings | Operating country, default export format and export date format. | | Branding | Logo, primary and secondary colors, and brand images. | | Admin tools | Enable, disable or delete the organization. | The invoice number format must contain `{n}`, the invoice's sequence number, which `{n:4}` pads to four digits. It also accepts `{yyyy}`, `{yy}`, `{mm}` and `{dd}`, filled from the invoice date. A year token restarts the counter each year, also when the format shows `{mm}`. Without a year token, the counter never restarts. When a format change adds or removes the year token, the counter continues past the numbers already issued, so no number repeats. An invoice gets its number when it is first sent, and a format change applies only to invoices sent after it. ## Disable or enable an organization [#disable-or-enable-an-organization] In **Admin tools**, select **Disable organization** and confirm. Members lose access to tenant routes, but nothing is deleted. Select **Enable organization** to restore access. ## Delete an organization [#delete-an-organization] In **Admin tools**, select **Delete organization** and confirm. This cannot be undone; the organization and all of its data are permanently deleted. ## Permissions [#permissions] | Key | Effect | | --- | --- | | `platform.admin.access` | Opens the admin area. | | `platform.organization.read` | Reads organizations, including disabled ones. | | `platform.organization.manage` | Edits settings, branding and lifecycle state, and deletes organizations. | System Admin has both organization keys. System Support has `platform.organization.read` only. Creating, disabling, enabling and deleting an organization is recorded in the [audit log](/docs/admin/audit-log). ## Related pages [#related-pages] - [Organizations](/docs/product/organizations) - [Settings and branding](/docs/product/settings-and-branding) - [Users](/docs/admin/users) - [Billing](/docs/admin/billing) - [Tenancy architecture](/docs/architecture/tenancy) - [RBAC architecture](/docs/architecture/rbac) # Plans (/docs/admin/plans) How system administrators review the Starter, Pro, Business and Enterprise plan catalog, edit credit package prices, and publish prices to Stripe under /admin/plans. ## Overview [#overview] Plans is the subscription catalog at `/admin/plans`, under **Admin > Finance > Plans**. It lists every plan with its price, included credits and Stripe state, and lets system administrators edit package prices and publish them to Stripe. | Route | Purpose | | ------------------------ | -------------------------------------- | | `/admin/plans` | List all plans. | | `/admin/plans/{planKey}` | Edit one plan's credit package prices. | CentraKit is the source of truth for the plan catalog, the included quantities (usage quotas) and the usage counters. Stripe is the source of truth for subscriptions, invoices and payment methods. Customers see the result in [Billing and usage](/docs/product/billing-and-usage). ## Key concepts [#key-concepts] ### Plans [#plans] | Plan | Key | Type | | --- | --- | --- | | Starter | `starter` | The free default plan. It has no credit rollover. | | Pro | `pro` | Self-serve plan for small teams. | | Business | `business` | Self-serve plan with a larger standard credit package. | | Enterprise | `enterprise` | Quote-only plan. Its price shows "On request". | There is no separate Free plan; Starter is the free tier. ### Credit packages [#credit-packages] Pro and Business are sold in credit packages that set the included monthly credits and the matching Stripe price: | Plan | Packages (credits) | | -------- | -------------------------------- | | Pro | 100, 200, 400, 800 and 1,000 | | Business | 400, 600, 1,000, 2,000 and 5,000 | Each package has a monthly and a yearly price. The package marked **Default** is the plan's standard credit amount. Credits cover AI chat and agents, inbox messages, API requests and MCP calls, and workflow steps. Prices are stored in euro cents. Unused credits on Pro and Business roll over for one month on monthly plans, or until one month after the end of an annual plan. Rollover grants are tracked per organization and expire on their own schedule. ### Stripe state [#stripe-state] Each plan shows **In Stripe** when its prices exist in Stripe, or **Not in Stripe** when they still need to be published. Enterprise shows "On request" because it has no self-serve price. ## Review the catalog [#review-the-catalog] The list shows Plan, Base price, Standard credits, Packages, Annual discount, Platform fee and Stripe. Search with **Search plans…** and use **Filters** for plan type (Self-serve or On request) and Stripe status. Open a plan from its row or the row menu. ## Edit a package price [#edit-a-package-price] 1. Open a plan, for example `/admin/plans/pro`. 2. In **Credit packages**, find the package and interval you want to change. 3. Enter the new amount per month or per year. 4. Select **Save** on that row. The new amount is stored in the local catalog. Existing Stripe prices do not change until you publish. ## Set the platform fee per online payment [#set-the-platform-fee-per-online-payment] Each plan can carry its own platform fee per online invoice payment. Organizations on the plan pay it when their customers pay an invoice online. The defaults are €1.50 for Starter, €1.00 for Pro and €0.50 for Business, and Enterprise uses the platform default. 1. Open a plan, for example `/admin/plans/pro`. 2. In **Platform fee per online payment**, choose a **Fee type**: **Use platform default**, **Percentage of invoice total** or **Fixed amount per transaction**. 3. Enter the fee and select **Save**. **Use platform default** follows the fee in [Platform settings](/docs/admin/platform-settings). An override set for one organization in [Billing](/docs/admin/billing) wins over the plan fee. ## Publish prices to Stripe [#publish-prices-to-stripe] On the plan detail page, select **Publish prices to Stripe**. CentraKit creates or updates the Stripe product and a price for each package and interval of that plan, using a lookup key such as `centrakit_pro_100_month`, and records the published version. Enterprise is skipped because it is quote-only. Publish after every price change so that new checkouts use the new amounts. Billing does not work locally, because the local Stripe keys are placeholders. ## Customer-facing features [#customer-facing-features] The plan detail page also lists the features shown in plan comparison and checkout, such as AI chat and agents, inbox channels, API and MCP access, workflows, audit logs and file history. These features are also the organization's entitlements: every included feature key of the plan an organization is on is an entitlement that SQL checks with `better_supabase.has_entitlement(organization_id, key)`. Retention follows the feature key's tier, so `audit-logs-90d` keeps audit logs for 90 days and `file-history-30d` keeps deleted files and earlier versions for 30 days. The seeded catalog gives 1 day of audit logs and 7 days of file history on Starter, 7 and 30 days on Pro, 90 and 90 days on Business, and 365 days on Enterprise, where the subscription's `audit_log_retention_days` metadata and the organization's file retention override take precedence. ## Permissions [#permissions] | Key | Effect | | --- | --- | | `platform.admin.access` | Opens Plans in read-only form. | | `platform.billing.manage` | Shows the price and platform fee controls and is checked when saving and publishing them. | ## Related pages [#related-pages] - [Billing](/docs/admin/billing) - [Billing and usage](/docs/product/billing-and-usage) - [Stripe billing operations](/docs/operations/stripe-billing) - [Audit log](/docs/admin/audit-log) # Platform settings (/docs/admin/platform-settings) How system administrators configure the default platform fee per online payment, the inbound email domain and route distance services at /admin/settings, and how platform settings are stored. ## Overview [#overview] Platform settings holds operable configuration that applies to the whole platform. The settings page is at `/admin/settings`, under **Admin > Finance > Settings**. Settings are stored in the database, not in environment variables, so administrators can change them without a deployment. Secrets and deployment configuration still belong in [environment variables](/docs/operations/environment-variables). ## Key concepts [#key-concepts] Each setting is a row in the platform settings table of the better-supabase `settings` module, with a key, a JSON value, the update time and the user who changed it. Values are validated per key in application code and again by a JSON Schema check in the database, so a malformed value is refused either way. When a key has no row, the platform uses its built-in default. | Key | Edited on | Default | | --- | --- | --- | | `invoice_payment_fee` | Platform settings, Default platform fee per online payment | 150 basis points (1.5%) | | `email_inbound` | Platform settings, Inbound email | Off (no domain) | | `routing` | Platform settings, Route distance | Off (no URLs) | | `ai_agent_policy` | [AI settings](/docs/admin/ai-settings), Policy tab | Agent ceiling, High threshold | | `ai_runtime_defaults` | [AI settings](/docs/admin/ai-settings), Policy tab | See AI settings | Any signed-in user can read platform settings, because features such as invoice sending and chat need them at runtime. Writes are limited per key, as described under Permissions. ## Set the default platform fee per online payment [#set-the-default-platform-fee-per-online-payment] When an organization's customer pays an invoice online, the platform is the merchant of record. The fee is taken as the Stripe application fee on the destination charge before the funds are transferred to the organization. This setting is the default. It applies to organizations whose plan has no fee of its own in [Plans](/docs/admin/plans) and that have no organization override in [Billing](/docs/admin/billing). Choose a **Fee type**: - **Percentage of invoice total**: enter **Fee (basis points)** between 0 and 10000. For example, 150 bps is 1.5%. - **Fixed amount per transaction**: enter **Fixed fee amount** in major currency units, for example 0.30. The fixed fee is applied in the invoice currency and capped at the invoice total. Select **Save**. See [Invoices](/docs/product/invoices) and [Stripe billing operations](/docs/operations/stripe-billing). ## Turn on inbound email [#turn-on-inbound-email] **Inbound email** sets the receiving domain configured in Resend, such as `inbound.example.com`. Employees forward receipts to their personal `expenses+` address at this domain to create draft expense claims. Leave the field empty to turn receipt forwarding off. ## Turn on route distance [#turn-on-route-distance] **Route distance** takes a **Geocoder URL** for a Nominatim-compatible geocoder and a **Router URL** for an OSRM-compatible router. Together they calculate mileage distances for expense claims. Both must be full URLs; leave either empty to keep distance entry manual. ## Audit trail [#audit-trail] Every platform setting change is recorded in the [audit log](/docs/admin/audit-log) as a platform setting mutation. ## Permissions [#permissions] | Key | Effect | | --- | --- | | `platform.billing.manage` | Opens `/admin/settings` and writes `invoice_payment_fee`, `email_inbound` and `routing`. | | `platform.aiModel.manage` | Writes `ai_agent_policy` and `ai_runtime_defaults`. | System Admin has both keys. System Support has neither. ## Related pages [#related-pages] - [Billing](/docs/admin/billing) - [AI settings](/docs/admin/ai-settings) - [Expenses](/docs/product/expenses) - [Invoices](/docs/product/invoices) - [Stripe billing operations](/docs/operations/stripe-billing) - [Environment variables](/docs/operations/environment-variables) # Roles and permissions (/docs/admin/roles-and-permissions) How system administrators manage built-in and platform custom roles, read the permission catalog, and set platform approval rules under /admin/roles. ## Overview [#overview] Admin Roles and Permissions is the platform-level RBAC surface at `/admin/roles`. It has three tabs: | Tab | Route | Purpose | | --- | --- | --- | | Roles | `/admin/roles` | Built-in roles and platform custom roles. | | Permissions | `/admin/roles/permissions` | The read-only permission catalog from the PermDock policy. | | Approval rules | `/admin/roles/approval-rules` | Platform approval floors that apply to every organization. | Detail pages live at `/admin/roles/{roleId}` and `/admin/roles/create`. Organization-owned roles are managed by tenant admins at `/{orgSlug}/roles`. See [Access control](/docs/product/access-control). ## Key concepts [#key-concepts] ### Permission keys [#permission-keys] Every permission key has the form `resource.action`, for example `quote.send`, and platform permissions start with `platform.`, for example `platform.user.manage`. The PermDock policy in code is the source of truth, including each permission's title and description; the admin UI cannot add, rename, relabel or delete keys. The system permissions are: | Key | Grants | | --- | --- | | `platform.admin.access` | Access to the admin area. | | `platform.organization.read` | Read organizations, including disabled ones. | | `platform.organization.manage` | Edit, disable and delete organizations. | | `platform.billing.read` | Read subscriptions, invoices and billing state. | | `platform.billing.manage` | Change subscriptions, invoices and platform settings. | | `platform.user.read` | Read platform users. | | `platform.user.manage` | Invite, edit, disable, ban and delete users. | | `platform.user.impersonate` | View and act as any user for up to 60 minutes. | | `platform.auditLog.read` | Read the platform audit log. | | `platform.auditLog.export` | Export audit events. | | `platform.auditLog.reveal` | Reveal restricted audit details. | | `platform.analytics.read` | Read platform analytics. | | `platform.analytics.export` | Export platform analytics. | | `platform.role.manage` | Create, edit and delete platform custom roles. | | `platform.aiModel.manage` | Change AI models, agent policy and runtime defaults. | | `platform.integration.manage` | Edit the integration catalog and its logos. | | `platform.job.manage` | Retry failed background jobs. | | `platform.knowledge.manage` | Manage system knowledge collections. | | `platform.workflow.manage` | Manage system workflows and lifecycles. | | `platform.approvalPolicy.manage` | Manage platform approval rules. | | `platform.announcement.manage` | Publish and remove in-app announcements. | | `platform.flag.manage` | Create, roll out and override feature flags. | ### Built-in roles [#built-in-roles] | Role | Scope | Default permissions | | --- | --- | --- | | System Admin | System | Every system permission. | | System Support | System | Admin access, organization view, billing view, user view and manage, audit log view. | | Owner | Organization | Billing, delete and ownership transfer, on top of everything Admin has. | | Admin | Organization | All organization permissions except the owner-only ones. | | Member | Organization | View plus create and update for everyday records, without deletes. | | Viewer | Organization | View permissions only. | Owner, Admin, Member and Viewer are global templates that every organization uses. Built-in roles show a lock icon. On their detail page the key, scope and permission matrix are read-only, and the role cannot be deleted. System administrators can still change the display name and description. The matrix shows the permissions the PermDock policy gives the role. In tenant role pages, the four organization templates appear as locked rows that cannot be opened. ### Platform custom roles [#platform-custom-roles] A role created here is a platform custom role: it has system scope and no organization, and users get it as their system role. Its permissions are bounded by the platform ceiling, which is what Platform Support may do. A platform custom role can never grant `platform.role.manage` or any other permission outside that ceiling, and saving one is refused with the keys that do not fit. Organization custom roles are created by each organization at `/{orgSlug}/roles`. See [Access control](/docs/product/access-control). ## Create a role [#create-a-role] 1. On the Roles tab, select **New role**. 2. Enter a name. The key is generated from the name and must be lowercase with dashes. 3. Add an optional description. The scope is always System. 4. In **Permissions**, select permissions individually or use **Select all** per group. The matrix offers only the permissions a platform custom role can hold. Search and filter it to find keys. 5. Select **Create role**. Role keys must be unique. ## Edit or delete a role [#edit-or-delete-a-role] Open a role from the Roles tab. Save the name, key and description in **Details**, and save the permission matrix separately in **Permissions**. A role's scope and organization do not change after it is created. Save stays disabled until something changes. To delete a custom role, use **Delete role** in the danger zone or the row menu, and type the role name to confirm. Built-in roles show "Built-in system roles cannot be deleted." ## Read the permission catalog [#read-the-permission-catalog] The Permissions tab lists every permission in the PermDock policy with its title, description and scope. Titles and descriptions come from the policy in code; use **Copy key** in the row menu to copy a key. ## Set platform approval rules [#set-platform-approval-rules] The Approval rules tab defines platform floors. They apply to every organization, and organizations can only add stricter rules on top. Without rules, risky AI actions still ask for confirmation. Select **Add rule** and configure: - **Name** and **Description**. - **Action** and **Effect**, such as Customer-facing, Moves money, Destructive, Changes access, Calls an outside system or Bulk change. - **Who is acting**: People, AI agents, Workflows, MCP clients or API keys. Selecting none means everyone. - **Only when** conditions on a path, operator and value. - **Requires**: someone else approves, or the requester confirms. - **Approvers**, **Approval mode** (any approver, every approver or in order) and **Approvals needed**. - **Expires after (hours)**, **Escalate after (hours)** and **Escalate to**. - Whether the requester may approve their own request, and whether the rule is active. See [Approvals](/docs/product/approvals) for how requests are decided. ## Permissions [#permissions] | Key | Effect | | --- | --- | | `platform.admin.access` | Opens the admin area and these tabs. | | `platform.role.manage` | Creates, edits and deletes platform custom roles. | | `platform.approvalPolicy.manage` | Creates, edits and deletes platform approval rules. | Role writes are checked on the server and recorded in the [audit log](/docs/admin/audit-log). Client-side checks only hide controls. ## Related pages [#related-pages] - [Users](/docs/admin/users) - [Access control](/docs/product/access-control) - [Approvals](/docs/product/approvals) - [RBAC architecture](/docs/architecture/rbac) # Users (/docs/admin/users) How system administrators list, invite, edit, disable, ban and delete platform users, and assign system and organization roles, under /admin/users. ## Overview [#overview] Admin Users is the platform-wide user directory at `/admin/users`. It lists every user across all organizations, shows their access, and gives system administrators lifecycle tools that tenant admins do not have. The page has two tabs: | Tab | Route | Purpose | | --- | --- | --- | | Users | `/admin/users` | Every user profile on the platform. | | Pending invitations | `/admin/users/invitations` | Invitations that grant a system role. | Organization-scoped user management for tenant admins lives at `/{orgSlug}/users`. See [Access control](/docs/product/access-control). Organization admins can suspend or remove a member in their organization, but only a system administrator can disable or ban an account. ## Key concepts [#key-concepts] - **System role.** A role with system scope, such as System Admin or System Support. It grants `platform.*` permissions and is managed only here. - **Organization role.** A user's role inside one organization, such as Owner, Admin, Member or Viewer. - **Disabled.** A reversible CentraKit state. Disabling a user also blocks sign-in in Supabase Auth, so they cannot sign in and an open session is signed out on the next page load, with a notice on the sign-in page. Disabling also ends every session of the user, so a sign-in from before the disable does not come back when the user is enabled again. Enabling the user lifts that block unless the user is also banned. Their Supabase Auth account still exists. - **Banned.** An authentication-level block in Supabase Auth. A banned user cannot sign in at all. A ban also ends every session of the user. A ban and a disable are kept apart: enabling a banned user keeps the ban, and unbanning a disabled user keeps the sign-in block until the user is enabled. - **Deleted.** A permanent hard delete of the user. ## Find a user [#find-a-user] The Users tab shows Name, Email, Role, Organizations, Joined and Last active. Search by name or email with **Search users…**, sort by Name, Email, Joined or Last active, and narrow the list with **Filters**: - **Access**: System access, Organization access or No assigned access. - **Account status**: Enabled or Disabled. Open a row, or use the row menu to **Open** the user or **Copy email**. A switch icon next to a name means an admin is viewing the app as that user right now. ## Edit a user [#edit-a-user] The user detail page at `/admin/users/{userId}` has four sections, followed by **View as user** when you may impersonate users. ### Personal information [#personal-information] Change the profile photo, first name, last name and username. Photos accept PNG, JPEG or WebP up to 5 MB and can be cropped before upload. Usernames use lowercase letters, numbers and underscores, with at least 3 characters. ### Organizations [#organizations] This table lists every organization the user belongs to, with the role and join date. Change the role in a row and select **Save** to update it. The list holds the organization's Admin, Member and Viewer roles and its custom roles. Owner is never offered, and an owner's row cannot be changed here: only the owner hands over ownership, through Transfer ownership in the organization settings. The first row, labeled with the product name and "System-wide organization", holds the user's system role. Pick a system role there to grant platform access, or choose **None** to remove it. ### Statistics [#statistics] A read-only summary of email, username, account creation time, last activity, organization count, system roles, Platform access (Enabled or Disabled) and sign-in state (Allowed or Banned). ### Admin tools [#admin-tools] | Action | Effect | | --- | --- | | Disable user | Blocks platform access without touching the auth account. **Enable user** restores it. | | Ban user | Blocks authentication in Supabase Auth. **Unban user** lets the user sign in again. | | Delete user | Permanently deletes the user. You must type the confirmation text before deleting. | The admin tools protect against lockouts. You cannot disable, ban or delete your own user, and you cannot disable, ban or delete the last active system admin. ## View the app as a user [#view-the-app-as-a-user] System Admins can open the app as another user to reproduce a problem or fix data the way that user would. The session is a better-supabase support session: you stay signed in as yourself, and every page, action and query runs as the user, under their organizations, roles and row level security, for up to 60 minutes. 1. Open the user at `/admin/users/{userId}`. 2. In **View as user**, select **View as user**. 3. Optionally enter a reason, such as a ticket number, and turn on **Read-only** to block every change during the session. 4. Select **Start session**. The app opens on the user's home page. While the session runs, a banner across the app shows who you are viewing as, the time left and a **Stop** button. The account menu shows **Stop viewing as** with the user's name instead of **Log out**. Admin pages are not available during the session, because they need your own platform permissions. The session ends when you select **Stop**, when it expires after 60 minutes, or when another System Admin ends it. An expired session is closed and recorded within five minutes, even when nobody opens the app again. You then see a page that says the session ended, and the next request runs as you again. A support cookie that outlives its session never acts as the user. Private files, receipts and attachments open as the user during a session. In a session without **Read-only** you can also upload, replace and delete them, within what the user may do: the user becomes the owner, and the stored file records the session and the admin. Changing the user's email or password is not available, because it would reach Supabase Auth with your own account. You cannot view the app as yourself, as another user with a system role, or as a disabled or banned user. Starting a second session ends your first one. While a session runs, the user's detail page shows an alert with the admin and the end time, and the Users tab marks the user. ### Audit trail [#audit-trail] Every change made during a session is recorded on the changed row's audit entry with the user as the actor, the actor kind Platform admin, and the admin, the reason and the session id in `impersonated_by`, `impersonation_reason` and `support_session_id`. Starting and ending a session writes `support.started` and `support.ended` in platform scope and in every organization the user belongs to, with the same three columns filled. Events the app records during the session also carry the admin, the reason and the session id. ## Invite a system user [#invite-a-system-user] 1. Open the Pending invitations tab and select **Invite**, or go to `/admin/users/invitations/create`. 2. Enter the email address and pick a system role. 3. Optionally add a first and last name to prefill the profile. 4. Select **Send invitation**. The invitee receives an email with a link to `/invitations/{token}`. If the email cannot be sent, the invitation is still saved and the error is shown, so you can resend it later. ## Manage pending invitations [#manage-pending-invitations] The Pending invitations tab lists Email, Role, Sent and Expires. Filter by status (Active or Expired) and search by email. - Open an invitation at `/admin/users/invitations/{invitationId}` to edit its details while it is still pending. - Use the row menu to **Resend invitation** or **Revoke invitation**. - Select several rows to revoke them in one step. Accepted invitations can no longer be edited. An expired invitation must be resent before you can edit it. ## Permissions [#permissions] | Key | Effect | | --- | --- | | `platform.admin.access` | Opens the admin area. | | `platform.user.read` | Reads platform users. | | `platform.user.manage` | Invites, edits, disables, bans and deletes users, and changes their roles. | | `platform.user.impersonate` | Views and acts as any user for up to 60 minutes, and ends other admins' sessions. | Both System Admin and System Support include `platform.user.manage`. Only System Admin includes `platform.user.impersonate`; System Support sees running sessions through `platform.user.read`. Every admin user mutation, including denied attempts, is recorded in the [audit log](/docs/admin/audit-log). ## Related pages [#related-pages] - [Organizations](/docs/admin/organizations) - [Roles and permissions](/docs/admin/roles-and-permissions) - [Access control](/docs/product/access-control) - [Profile](/docs/product/profile) - [RBAC architecture](/docs/architecture/rbac) - [Authentication architecture](/docs/architecture/authentication) # Workflows (/docs/admin/workflows) How system administrators manage the system automations and record lifecycles that apply to every organization under /admin/workflows, including versions, default lifecycles and run history. ## Overview [#overview] Admin workflows are the system-scoped counterpart of tenant workflows. They live at `/admin/workflows`, under **Admin > Operations > Workflows**, and apply to every organization. | Route | Purpose | | --- | --- | | `/admin/workflows` | List system workflows and lifecycles. | | `/admin/workflows/create` | Create a system workflow. | | `/admin/workflows/{workflowId}` | Editor tab: the visual graph. | | `/admin/workflows/{workflowId}/details` | Details tab: name and description. | | `/admin/workflows/{workflowId}/runs` | Runs tab: run history for this workflow. | | `/admin/workflows/{workflowId}/runs/{runId}` | One run with its graph and step details. | Organization workflows are built under `/{orgSlug}/workflows`. See [Workflows](/docs/product/workflows). ## Key concepts [#key-concepts] ### Purpose [#purpose] Each system workflow has a purpose, shown as **Kind** in the list: - **Automation** workflows react to domain events, schedules, webhooks or manual starts. With automatic visibility, they run for every organization unless the organization turns the workflow off or publishes its own workflow with the same key. - **Lifecycle** workflows describe the stages of one record type. They do not match events directly. A record runs the lifecycle chosen by its template, then the organization default, then the system default. ### Versions [#versions] A workflow key has at most one published and one draft version. Publishing archives the previous version, removes its schedules and carries access grants and watchers forward. A running lifecycle stays on the version it started with; only records that start later use the new version. System schedules fan out into one run per enabled organization that has not turned the workflow off. ### Shipped system lifecycles [#shipped-system-lifecycles] | Lifecycle | Record type | What it tracks | | --- | --- | --- | | Quote lifecycle | Quote | Send, views and the customer decision, reminders before expiry, expiry, then auto-reject after 30 days. | | Invoice lifecycle | Invoice | Send, views and payments, reminders, overdue, the final notice, then collection costs. | | Task lifecycle | Task | A root task through work and holds to completion. | | Order lifecycle | Order | A confirmed order through prepayment, picking, shipping or pickup, delivery and invoicing. | | Purchase order lifecycle | Purchase order | A sent purchase order through supplier confirmation, transit and receipt until the supplier bill is in. | | Shipment lifecycle | Shipment | A shipment from the warehouse to delivery or pickup, including carrier problems. | ### Shipped system automations [#shipped-system-automations] The seed also ships automations such as Quote accepted (which creates a draft invoice and a follow-up task), Quote rejected, Invoice paid, Invoice cancelled, Invoice payment failed, Invoice disputed, Workflow failed alert, Task assigned, Task due soon, Task overdue, Task due scan, Agenda item follow-up, Customer welcome, Inbox unanswered, Invoice overdue digest, Stale draft digest and Billing usage threshold. Platform jobs such as Knowledge ingestion, Product lookup from image and Customer details lookup also run as system workflows. ## Find a workflow [#find-a-workflow] The list shows Name, Status, Version and Updated. Statuses are Draft, Published and Archived. Search with **Search workflows…** and use **Filters** to show only Automation or Lifecycle workflows. Row actions include **Duplicate** and **Delete**. ## Edit and publish a workflow [#edit-and-publish-a-workflow] 1. Open the workflow. The **Editor** tab shows the graph. 2. Add nodes from the palette: triggers (Domain event, Schedule, Manual, Workflow failed, Webhook), flow nodes (Condition, Merge, Delay, Stage, Clear flag, Wait for event), actions (Send email, AI agent, Call tool, HTTP request, Send webhook, Approval gate, Domain action, Start workflow) and the domain actions for quotes, invoices, tasks, orders, purchasing, knowledge and more. 3. Use **Test run** to try the draft, or **Edit with AI** to change the graph with the assistant. 4. Publish the draft to make it the active version. Change the name and description on the **Details** tab. ## Set the default lifecycle [#set-the-default-lifecycle] Filter the list by Lifecycle, open the row menu of a lifecycle and select **Set as default**. Exactly one system lifecycle per record type is the default, and newer published versions of that key take over automatically. Organizations cannot edit a system lifecycle. They open it read-only and use **Duplicate to customize**. The copy remembers which system version it came from and shows **Upstream updated** when a newer system version is published. ## Review runs [#review-runs] The **Runs** tab lists runs for every version of the key, with statuses such as Queued, Running, Waiting, Succeeded, Failed, Cancelled and Stuck. Select **Run now** to start a run manually. Open a run to see its graph and steps. Select a step to view its input, output, error and attempts. A run waiting on an approval gate shows **Approve** and **Deny**. Use **Cancel run** to stop a queued, running or waiting run; a cancelled run cannot be resumed. Use **Restart workflow** on a failed, cancelled or stuck run to start it again from the record's current state without repeating the emails, notifications and stages it already completed. Failed runs also appear in the attention queue on the [operations dashboard](/docs/admin/dashboard). ## Safety [#safety] Automatic system graphs notify people or create drafts. For example, Quote accepted creates a draft invoice instead of sending one. Lifecycles may email customers, such as quote and invoice reminders and the final notice. Those emails are sent within business hours in the organization's time zone, and the record is checked again before sending, so a settled record does not get a reminder. System graphs do not bypass tenant permissions when they change records. Lifecycle pause, resume and manual advance run with admin context only after the caller's route, action or AI tool has checked the record permission. ## Permissions [#permissions] | Key | Effect | | --- | --- | | `platform.admin.access` | Opens the admin area. | | `platform.workflow.manage` | Creates, edits, publishes and deletes system workflows, sets default lifecycles and manages runs. | ## Related pages [#related-pages] - [Workflows](/docs/product/workflows) - [Approvals](/docs/product/approvals) - [Workflow operations](/docs/operations/workflows) - [Webhooks](/docs/developers/webhooks) - [Operations dashboard](/docs/admin/dashboard) # Agent notes (/docs/agents/README) Things coding agents get wrong in CentraKit, grouped by topic. These pages collect corrections that agents needed more than once. `AGENTS.md` links here, so an agent can load only the topic it is working on. ## When to add a note [#when-to-add-a-note] Add a note when you correct the same mistake a second time. Put it in the page for that topic, or start a new page if none fits. Write one short paragraph per mistake: what goes wrong, why, and what to do instead. ## Topics [#topics] - [Toolchain](/docs/agents/toolchain): pnpm, TypeScript 7, Oxlint and Oxfmt. - [Formatting](/docs/agents/formatting): lint directives after reflows and regenerated output. - [Git and releases](/docs/agents/git-and-release): branches, commit messages, hooks and Changesets. - [Supabase schema](/docs/agents/supabase-schema): declarative files, ordering, migrations and audit triggers. - [Tenancy and routing](/docs/agents/tenancy-and-routing): organization slugs, page shells and Suspense. - [Translations](/docs/agents/translations): `.po` catalogs and extracted messages. - [Testing](/docs/agents/testing): where tests live and how they resolve paths. - [Writing docs](/docs/agents/writing-docs): how to write public docs pages from the shipped code. - [better-supabase](/docs/agents/better-supabase): repositories, errors, mapping and tests in the `supabase-*` packages. # better-supabase in the provider packages (/docs/agents/better-supabase) How the supabase-* packages use better-supabase repositories instead of hand-written supabase-js queries. The `supabase-*` provider packages read and write through better-supabase repositories. `packages/supabase-runtime` owns the definition and the generated schema; every other package imports only `type Db` from `@workspace/supabase-runtime/db`. `plans/050-adopt-better-supabase-and-permdock.md` holds the adoption plan and the implementation log. ## Wiring [#wiring] - A service factory takes the repository connection: `createSupabaseCustomersService(db: Db, ...)`. Core wiring passes `context.db`. - A service that needs the service role takes an admin `Db` (or a function that returns one) that core wiring builds with `connectDb(context.supabaseAdmin)`; it does not import the shared admin client. - The caller's verified claims ride on the connection: core wiring builds `connectDb(client, claims)`, which passes them to better-supabase as `connect(client, { claims })`, and services read them from `db.$context.claims` with `getCurrentUserIdFromClaims(db)`, `getCurrentUserEmailFromClaims(db)` or `getCurrentAuthClaims(db)` from `@workspace/supabase-runtime/auth/claims`. An admin connection carries no claims. Unit tests pass the claims to `connectDb` instead of stubbing auth. - `db.$client` is the supabase-js client of the same request. Use it only for Auth and for the few table calls the repository API cannot express yet (see below), never for table reads and writes or for Storage. - `db.$storage` is Storage as the same caller. Pass it to a bucket's `connect`, to `connectStored`, to the URL resolvers and to the blocks. In a support session it is better-supabase's `supportStorage`, which reads as the target through Postgres. In a writable session it writes after a rehearsal as the target, makes the target the owner and stamps `supportSessionId` and `supportAdminId` in the object's metadata, which needs `adminSql` and `session`; signed uploads work too, and the upload is finished with the bucket's `claimReserved` after the browser stored it (the Files finalize step does this; outside a session it is a no-op). A read-only session refuses writes with `SUPPORT_READ_ONLY` as `forbidden`; `connectDbOverPostgres` takes the Storage for its connection. - `@workspace/supabase-runtime/env` validates its keys with t3-env and passes them, over `process.env`, to better-supabase's `loadEnv`. `getSupabaseServerEnv()` is `toServerEnv` of that (JWKS from `SUPABASE_JWKS`, `SUPABASE_JWKS_URL` or the Auth well-known URL), and `getPostgresConnectionString()` is its `dbUrl`: `DATABASE_URL`, then `POSTGRES_URL`, then `POSTGRES_URL_NON_POOLING`. Keys must be `sb_publishable_` and `sb_secret_` keys, so test stubs use `sb_publishable_test` and `sb_secret_test`. - Never import `better-supabase` outside `packages/supabase-runtime`. `DbError` comes from `@workspace/supabase-runtime/db-result`. ## Queries [#queries] - Rows and filters use camelCase (`organizationId`). RPC arguments, PostgREST select strings on `db.$client` and SQL keep database names (`p_organization_id`). - Reads: `findMany`, `findFirst`, `findUnique`, `findById`, `count`, `exists`. Writes: `create`, `createMany`, `update`, `updateMany`, `upsert`, `delete`, `deleteMany`. Functions: `db.$rpc("name", { p_arg })`. - Replace a follow-up query with an `include` when the result is the same: a primary contact with its methods is one request with `include: { contactProfile: { include: { contactMethods: {...} } } }` instead of three. - Run independent reads with `Promise.all` or `db.$many([...specs])`. - Relation names come from the foreign keys in `src/generated/schema.meta.js`. A table with two foreign keys to the same target gets names from every key column, such as `customerLocationsByCustomer` and `customerLocationsByCustomerOrganization`. - Keep `select` lists explicit (`as const` arrays) so the row type stays narrow. - `findMany` without `orderBy` orders by the primary key. Pass the old order explicitly when it mattered. - A `{ in: [] }` filter skips the request and returns no rows, so an empty id list needs no early return. - Tenant-scoped writes keep their organization filter: use `updateMany` or `deleteMany` with `where: { organizationId, id }`, or `update(id, patch, { where: { organizationId } })`. A row that does not match the `where` returns `not_found` in one request, so the service maps it with its usual not-found error. Use `where` for every condition the row must meet (a tenant, `deletedAt: null`, a status the transition starts from); keep `expect` only where a mismatch must be told apart from a missing row, which then returns `stale`. - An `include` that replaces a follow-up query also fails the whole read when the embedded part fails, where the old code often treated a failed follow-up as empty. That is accepted. - Optional filters use `...(condition ? { column: value } : undefined)`; `exactOptionalPropertyTypes` rejects `{ column: undefined }` and the anti-slop lint rejects `: {}`. A filter built up statement by statement is typed `WhereOf<"timeEntries">` from `@workspace/supabase-runtime/db`, which re-exports the generated `WhereOf`; an `orderBy` built in a helper is typed with the generated `OrderByOf<"tasks">` (one sort or a list), and one sort term with `OrderTermOf<"tasks">`. - An offset list with a total is one `paginate({ offset: range.from, limit: range.range.limit, count })` call; `count` is the list's `"exact"` or `"estimated"` mode. Build the page with `createSupabaseOffsetListPage(items, range.range, page, countMode)` from `@workspace/supabase-runtime/list`, which returns the same `pagination` fields as `createSupabaseListPageForCountMode` did for the old two-step reads. An offset past the last row returns no rows and the total instead of an error. - `paginate` with an `orderBy` adds no tie-breaker, so keep the old `{ id: "asc" }` term where the list had one. - Several conditions on one column (`status` in a list and `status` not `done`) go in `AND: [...]`; a list of `OR` groups works the same way. Date-bucket filters use `dateBucketConditions(buckets)` from `@workspace/supabase-runtime/list`: `OR: dateBucketConditions(buckets).map((dueAt) => ({ dueAt }))`. - `in` and `notIn` values that need no quotes (uuids, numbers, slugs, emails) are sent bare, and a read without `limit`, `offset`, `count` or a single row whose URL gets too long is split along its longest `in` list and merged by the library. Pass the whole id list; do not chunk by hand. A paginated read cannot be split, so keep its id lists bounded. - `db.$rpc` returns rows of a table (`returns setof x`, `returns x`) and `returns table (...)` records in camelCase, typed as the model row or a record with app names. Do not map their keys. - Generated RPC arguments accept `null`, so pass a nullable value directly. - Function results are typed nullable. When the SQL guarantees a value (an `insert ... returning`, a `coalesce`, a `raise` on the missing case, a not-null column), list the function under `functions..notNull` in `better-supabase.config.ts`, as `true` or as the `returns table` columns that are never null, and run `pnpm supabase:types`. Otherwise handle `null` where the type checker reports it. - Columns a `before insert` trigger fills (`variant_id` from the product's default variant) are optional on insert; leave them out instead of reading the value first. - An RPC that returns `jsonb`, a scalar that can be `null`, or a `returns table` column that can be `null` takes a `returns` schema: `db.$rpc("name", args, { returns: schema })` validates the decoded value and types the result from the schema. Use the contract schema when one exists (`inventoryLevelSignalSchema.array()`), otherwise a schema next to the service (`portalQuotePayloadSchema`). A value that fails the schema returns a `validation` error. Never cast an RPC result. - A database `P0002` (`no_data_found`) arrives as `not_found`; do not special-case the code. - A lookup that must match at most one row by a non-unique filter (the `maybeSingle()` of supabase-js) uses `findOnly({ select, where })`, which returns a `multiple_rows` error for several rows. `findFirst` takes the first row and is only for lookups by a unique key or reads that want the first row by an order. - Patch and row types come from `RowOf`, `InsertOf` and `UpdateOf` in `@workspace/supabase-runtime/db`, keyed by the repository's camelCase table key (`RowOf<"shipmentLines">`). Function argument and result types come from `Functions` in the same module (`Functions["list_inbox_threads"]["Returns"][number]`). - Updates and deletes that must return the rows they changed use `updateMany` or `deleteMany` with `returning: true` and a `select`. - A jsonb key is filtered with a path: `where: { metadata: { path: ["supersededByRunId"], isNull: true } }` or `{ input: { path: [field], eq: value } }`. A jsonb containment takes the JSON value as is: `parts: { contains: [{ type: "data" }] }`. Text columns take `match` and `imatch` for POSIX regular expressions. - `orderBy` sorts by a column of a to-one relation: `orderBy: [{ organization: { name: "asc" } }, { id: "asc" }]`. - A CHECK `column is not null` types the column as not null and makes it required on insert, even when a `before insert` trigger fills it. The product variant columns (`variantId` on stock records, movements, customer assets and supplier links) are filled from the product's default variant in the service before the insert. ## Storage [#storage] - Buckets are defined once in `packages/supabase-runtime/src/storage.ts` with better-supabase's `defineBucket`. Bucket ids come from `storageBuckets` in `packages/foundation/src/storage-buckets.ts`. A Storage bucket that holds objects in several layouts has one definition with a `path` array: `buckets.organizationImages` holds logos and brand images, `buckets.productImages` holds product and brand images, and `buckets.files` holds upload reservations, copies, ZIP exports and `{orgId}/{...rest}` for any other stored path under an organization. - A service connects the bucket to its client and builds paths from the template: `buckets.customerLogos.connect(db.$storage).upload({ orgId, customerId, version }, bytes, options)`. The values pick the template whose placeholders they fill. Store `uploaded.data.path` in the `*_path` column. `remove`, `download`, `copy`, `signedUrl(path, { ttl, download })`, `publicUrl`, `reserve` and `uploadReserved` take the stored path, which must match one of the templates. A bucket definition's `path(values)` returns a `Result` with an `invalid_input` error for values Storage would refuse; map the error, or pass the values to `upload` and store the returned path instead of building it first. - Results are `DbError` results: map them with the service's error mapper or keep the old domain error with `error.message`. `publicUrl` returns a `Result` too; read `.data` (`null` for a path outside the templates) in resolvers such as `resolveAvatarPublicUrl` and `resolveOrganizationImageUrl`. - An object whose bucket id is stored on a row next to its path (file versions, ZIP export items, upload reservations, knowledge sources, chat attachments) goes through the `storedObjectBuckets` registry (better-supabase `defineBuckets`): `storedObjectBuckets.connectStored(db.$storage, { bucket, path })` returns the bucket client and the checked path, or an `invalid_input` error for an unregistered bucket id or a path outside its templates. Register a bucket there when rows start to reference it. - Apps and runtimes that may not import `@workspace/supabase-runtime` use `connectStorage(client)` from `@workspace/core/storage`, which returns the bucket clients they need (`chatAttachments`, `auditLogExports`), `storedObject({ bucket, path })` and `organizationImageUrl(path)`. - No code calls `client.storage.from(...)` directly. ## Managed blocks [#managed-blocks] - The comments, attachments, data-lifecycle and api-keys modules are managed: their tables live in `better_supabase`, which the Data API never exposes. Each has `api: "api"` in `better-supabase.config.ts`, so `sql sync` writes `security invoker` entry points into the `api` schema, which is in `[api] schemas` and in the `supabase:diff` schema list. - Connect a block over the caller's `Db` with `connectComments`, `connectAttachments`, `connectDataLifecycle` or `connectApiKeys` from `@workspace/supabase-runtime/`; they pass `rpcTransport(db.$client, { schema: "api" })`. Service-role work (the attachment scanner, the data exporter, the organization purger) runs as `withBlock` entries in `packages/core/src/api-provider-context.ts` over `ctx.postgresAdmin`, from the `outbox_relay` and `blocks_purge` background jobs. - The organizations module is adopted over `public.organizations` and `public.organization_users`, also with `api: "api"`. `connectOrganizations(db, postgres)` from `@workspace/supabase-runtime/organizations` runs `sqlTransport(postgres)` when the request has a caller Postgres client (API keys and the API's Postgres modes) and `rpcTransport` otherwise; `core-tenant` passes `context.postgres`. Pass role ids; keys resolve within the organization (`sql.modules.tenant.options.roleThrough.tenant`). The invitations module is adopted over `public.organization_invitations` for organization and platform invitations (`token_hash`, `revoked_at`); invite, edit (`updateInvitation`), resend, revoke, preview, accept, the inbox list (`myInvitations`) and the inbox's accept and decline by id go through the same `Organizations` client. The `public.invitation_preview_extra` hook adds the role's key and name and the brand images to the preview and the inbox; clients cannot write `organization_invitations`. - The notifications module is adopted over CentraKit's five notification tables with `api: "api"`. `connectNotifications(db, { types, actionable })` from `@workspace/supabase-runtime/notifications` sends through `notify()` and reads the inbox, counts, subscriptions and preferences; clients cannot write the tables. A send names its people in `recipientUserIds` and adds subject watchers only with `watchers` (`participating` or `all`); members who ignore the subject never get it. `public.notification_audience` adds the run creator and the watchers of the workflow to a workflow run notification. The module writes the recipient broadcast trigger and the `realtime.messages` receive policy for `org:{tenantId}:notifications:{userId}`. - The number-sequences module is adopted over `public.organization_number_sequences`. Its `bs_assign_number_` triggers number tasks, quotes, orders, purchase orders and shipments on insert, so services and SQL functions insert those rows without `number` (`gen` makes the column optional in `InsertOf`), and a client that sends a number gets `NUMBER_MANUAL`. Invoices are numbered when `send_invoice` sets the status to `sent`: the `invoice` entity takes each organization's `invoice_number_format` (`formatFrom`) and its reset period from the generated `invoice_number_reset` column (`periodFrom`: `year` when the format has `{yyyy}` or `{yy}`, otherwise `none`, so a `{mm}` token never restarts the counter monthly), reads the year and month from `issued_on`, writes `formatted_number` and the bare `number`, and `number_year` is generated from `issued_on`. A format change that changes the period runs `sync_number_sequences(organization, 'invoice')` in its transaction through the generated `bs_sync_number_period_invoice` trigger on `organizations`, which `sql sync` writes because the entity sets `periodFrom`, `formatFrom` and `periodColumn`; do not hand-write that sync. The assign trigger numbers an invoice that reaches `sent`, `paid`, `overdue` or `uncollectible` without a number. `better_supabase.preview_number(organization, 'invoice', at)` shows the next invoice number to holders of `invoice.send` (`entities.invoice.permission`); the other entities fall back to `permissions.assign`, `organization.update`. Seeds and data migrations that write numbered rows directly end with `better_supabase.sync_number_sequences()`. - Request paths never open a direct Postgres connection for a module or for `auth.*` (ADR 0012). Give the module an `api` option with a `functions` list of service-role entry points and call it with `rpcTransport(serviceClient, { schema: "api" })` (the audit recorder, billing, `readLastSignIns`, `rpcQueueMonitor` for the admin dashboard's job health). Change Auth users through `auth.admin` and end sessions through `public.end_user_sessions`. `ctx.postgresAdmin` is for the SQL-only libraries in the API and workflow runtimes: job claiming, the webhook inbox, idempotency, the Stripe Sync Engine and agent checkpoints. - Block tables have no foreign key to `organizations`; the organization purge deletes their rows (CentraKit has no `on_organization_purge` hook, so a purge of the adopted tenant rows completes only once better-supabase purges adopted tenants itself), and `012_auth_users.sql` deletes the seeded organizations' block rows before a rerun. - `better-supabase sql data` and `permdock:generate` both stamp migrations with the current second. Rename the later file when they collide, or `supabase db reset` fails on a duplicate version. ## Errors [#errors] - Each service has one `toXError(error: DbError): XError` built on `mapDbError` from `@workspace/supabase-runtime/db-result`, with domain special cases first (for example `foreign_key` to `CustomerInUseError`). `DomainErrorFactories` and `MapDbErrorOptions` come from the same module. - Wrap calls with `toDomainResult(db.x.findMany(...), toXError)` to get a better-result `Result`. For an awaited write whose data the caller drops, use `toVoidResult(await db.x.deleteMany(...), toXError)`. - For a call that still goes through `db.$client`, write `toXError(toDbError(error))` with `toDbError` from `@workspace/supabase-runtime/db-result` (better-supabase's own, re-exported), so one mapper covers both paths. It classifies a PostgREST error by its code and keeps `code`, `hint`, `details` and the constraint name, so special cases on codes such as `P0001`, `P0002` or `22023`, on hints and on constraint names fire on both paths. Do not write a second mapper that takes a `PostgrestError` or a `DbError | PostgrestError` union. - `findById` returns a `not_found` error for a missing row; `findFirst` and `findUnique` return `null`. - A write on a read-only connection (`25006`, as in a read-only support session) arrives as `forbidden` with code `25006`, so the service's forbidden factory maps it and the app shows its translated forbidden message. Map block and Storage results through the same `toXError` instead of wrapping them in an unexpected error. - A read path must not run `insert`, `update` or `select ... for update` when nothing changes, or it fails in a read-only session. Ensure-style functions such as `ensure_user_file_drives` check with `if not exists` before they write. ## Mapping [#mapping] - camelCase rows match the contracts, so a mapper only exists for computed fields (public URLs from `*_path`, nested addresses, parsed jsonb). Delete identity mappers. - No casts on rows. When a type looks wrong, the generated schema is out of date: run `pnpm supabase:types`. ## Known gaps in better-supabase [#known-gaps-in-better-supabase] Table reads and writes go through repositories. PermDock's approval store is the generated one: `rls.approvals` adopts `approval_requests` (`permdock_token`, `permdock_request`, `open: 'attach'`, functions in `public`), and `packages/supabase-identity/src/approvals/store.ts` wraps `supabaseApprovalStore` so that `create` opens the inbox row and claims the token before PermDock attaches the request; a request asked again after it expired gets a new inbox row. It passes `testApprovalStore`. The open gaps are listed under "Upstream findings" in the plan. `toDomainResult` and `toVoidResult` are built on better-supabase's typed `toBetterResult`; keep using them instead of calling `toBetterResult` in a service. The CLI finds `supabase/config.toml` and the root `better-supabase.config.ts` in parent directories, so `pnpm supabase:types` and the Database workflow run it from `packages/supabase-runtime` without `--cwd`. Doctor's `--format github` and SARIF output use repository-relative paths. ## Tests [#tests] - `paginate` with an offset reads one look-ahead row, so a page of 50 asks for `limit=51`. - Unit tests build a real client on a fake PostgREST with `createFakeSupabaseClient` from `@workspace/test-fixtures/fake-postgrest` and pass `connectDb(client)` to the service. Reply with camelCase rows; assert on the recorded request (`path`, `params` with database names such as `id=eq.42`, `body` for writes and RPCs). - `in` values that need no quotes are bare in the request (`status=in.(sent,overdue)`, `id=in.(,)`); values inside an `or` stay quoted (`title.ilike."%x%"`, `number.eq."42"`). An `OR` with one condition is sent as a plain filter (`valid_until=is.null`). - Include embeds name the foreign key in the select string (`contact_profiles!customer_contacts_contact_profile_id_fkey!inner(...)`), so assert on parts of `params.get("select")` with `toContain`. - Live tests keep using the local stack through `createRequestContext({ bearerToken })`, built in `beforeAll` because the integration setup re-stubs the env before each test. # Formatting (/docs/agents/formatting) Oxfmt reflows, lint directives and regenerated output. ## Directives move with reflows [#directives-move-with-reflows] A formatter reflow after a rename can move the line that an `oxlint-disable-next-line` or `@ts-expect-error` directive must sit above. Put the directive directly above the exact property or expression line it covers, and check it again after `pnpm format`. ## Regenerated output [#regenerated-output] Revert unrelated regenerated output, such as a version header that a generator rewrote, instead of committing it. Commit generated files only when the change that regenerated them is part of your task. # Git and releases (/docs/agents/git-and-release) Branches, commit messages, hooks and Changesets. ## Branches [#branches] Commit and push to `develop`. Never commit to `main`; it only moves through the `develop` to `main` pull request. ## Commit messages [#commit-messages] Commits follow Conventional Commits and commitlint checks them. The header is at most 72 characters and body lines are at most 100. Mark breaking changes with `!` and a `BREAKING CHANGE:` footer. ## Hooks stay on [#hooks-stay-on] Lefthook formats and lints the staged files before a commit. Both the commit and the push then run `pnpm verify:affected`: lint, typecheck and `test:unit` for the workspaces affected since `origin/develop`. Because the gates match, a commit that lands can also be pushed, and Turbo's cache makes the push re-run fast. In `tests/integration`, `test:unit` runs only the static `cross-package-node` guards; the integration, DOM, workflow, Supabase and e2e suites run in CI. Do not pass `--no-verify`. When a hook fails, fix the cause and commit again. ## Changesets [#changesets] Every user-visible change adds a changeset with `pnpm changeset`. The apps share one version through a `fixed` group, so pick any app and describe the change for users. Releases do not use release-please. ## Uncommitted work belongs to the user [#uncommitted-work-belongs-to-the-user] Do not run `git checkout`, `git restore`, `git reset` or `git stash` on files you did not change in this task. The one routine exception is restoring the `.po` catalogs described in [Translations](/docs/agents/translations). ## Plans and long work [#plans-and-long-work] - When implementing an attached plan, do not edit the plan body, do not recreate existing todos, keep the plan's todo statuses current (in progress, completed) as work proceeds, refreshing them every few minutes and after each subagent finishes during long runs, and continue through all todos unless blocked. Apply the work on the current working tree so the user can commit; do not create a side branch unless asked. Keep drafted plans as normal Cursor plans (under `~/.cursor/plans`); do not save them as repo `plans/*.md` files. - For long or broad work (migrations, lint or type cleanups), pull the latest `develop` first, then commit and push to `develop` in small, scoped batches as each slice passes the hooks, instead of holding a large uncommitted tree until everything is finished; land remaining gaps as follow-up commits. After the last push of a batch (not after every commit), confirm the `develop` Vercel deployment builds and fix it when it breaks. Subagents still do not commit unless the parent says so. # Supabase schema (/docs/agents/supabase-schema) Declarative schema files, layout, migrations and audit triggers. ## Edit schemas, generate migrations [#edit-schemas-generate-migrations] Change `supabase/schemas/**/*.sql`, then run `pnpm supabase:diff -f `. It runs pg-delta (`supabase db schema declarative sync --no-apply --strict-coverage -s public,api,permdock,better_supabase,pgmq,billing,storage,realtime,auth`), which needs `[experimental.pgdelta] enabled = true` in `supabase/config.toml` and works while the stack runs. `--strict-coverage` fails when an object in a listed schema has no schema file. Do not write migration SQL by hand, except for data migrations and the caveats below. A clean tree reports `No schema changes found`, and CI fails on any drift. Add a schema to the script in `packages/supabase-runtime/package.json` when schema files start managing objects in it. ## Local stack engine [#local-stack-engine] `supabase/config.toml` sets `[experimental] stack = true`. Do not add `port` keys back to the config, and do not hardcode `54321`, `54322` or `54324` in scripts or live tests; read endpoints from `readLocalSupabaseEnv()` in `@workspace/foundation/local-supabase` or from `supabase status --env --output-format text`. Local `start` and `status` have no `-o` flag (use `--output-format json` or `--env`), and `supabase stop --no-backup` is `supabase stack destroy`. The engine refuses to start with Auth email templates that use `content_path`, a custom `edge_runtime.deno_version`, `api.tls` or the OrioleDB `experimental.s3_*` keys, so email templates go under `[remotes.main]` and `[remotes.develop]` only. Never pass `realtime` to `supabase start --exclude`: without the service there is no `realtime.messages` table, and the init migration fails on its topic policies. Services are lazy, so keeping one in costs nothing until it is requested. ## Layout [#layout] `supabase/schemas/` has one folder per domain and no numbered files: - `extensions.sql` at the root, and `shared/` for the cross-domain types, the organization number sequences and the schema-level grants in `shared/grants.sql`. - One folder per domain (`tenant`, `identity`, `crm`, `catalog`, `work`, `people`, `commerce`, `expenses`, `conversations`, `ai`, `automation`, `platform`, `portal`). - In a domain folder, one file per table named after the table (`commerce/document_parties.sql`). The table file holds the table, its indexes and comments, its `track_updated_at` and `track_actor` calls, its triggers including `bs_audit`, `alter table ... enable row level security` and its policies. - `/types.sql` holds the domain's enums, `/functions/.sql` its functions, and `/storage.sql` its bucket policies. - `better_supabase/module_*.sql` is written by `pnpm supabase:blocks`, and `identity/permdock/helpers.sql` and `hook.sql` by `permdock:generate`. `identity/permdock/grants.sql` holds CentraKit's own `service_role` grants on the PermDock functions. pg-delta orders every statement by dependency, so `supabase/config.toml` has no `schema_paths` list and file names do not set the run order. Define each function in one file only. After any move or rename, `pnpm supabase:diff` must report no schema changes. ## better-supabase SQL modules [#better-supabase-sql-modules] The modules CentraKit uses are listed in `sql.modules` of `better-supabase.config.ts`. `better-supabase sql add ` and `sql sync` write them into `supabase/schemas/better_supabase/` and their pgTAP files into `supabase/tests/database/`. Never edit those files; change the module's entry in the config and run `pnpm supabase:blocks`, which syncs the files and writes the modules' rows (`better_supabase.modules`, reserved slugs, role settings) into a data migration with `sql data`. The order for a change is: config, `pnpm supabase:blocks` (files), `pnpm supabase:diff -f ` (schema migration), `pnpm supabase:blocks` again (data migration stamped after it). CI runs `supabase:blocks:check`, which fails on a stale module file or a module behind its version. A table with `updated_at` gets `select better_supabase.track_updated_at('public.');` in its table file, and a table with `created_by` or `updated_by` gets `select better_supabase.track_actor('public.
');` (pass `created_by => null` or `updated_by => null` for a missing column). The actor trigger stamps both from `auth.uid()`, so services do not send `createdBy` or `updatedBy` on writes through the caller's connection; writes through the service role keep the values they send. Keep these calls static: pg-delta records the triggers they create. `better-supabase.config.ts` passes `authorizationProvider()` from `permdock/better-supabase` as `authorization`, built from `packages/identity/permdock.manifest.json` and `packages/identity/permissions.catalog.json`; the modules call the PermDock helpers through it (`sql.modules.access.model: "provider"`) and check their permission keys against the catalog. `pnpm --filter @workspace/identity permdock:generate` rewrites the manifest and `permdock:check` fails when it is stale. ## Realtime topics and jsonb checks [#realtime-topics-and-jsonb-checks] Private Realtime topics are defined once with `defineTopic` in `packages/core/src/realtime.ts` (an `access` policy with the `receive` permission key at `scope: "tenant"`, so the policy calls `better_supabase.tenant_ids_with`, and event payload schemas). Their `realtime.messages` policies live in `supabase/schemas/conversations/realtime.sql`, which `realtime.policies` in `better-supabase.config.ts` has `pnpm supabase:blocks` write from every topic `packages/core/src/realtime.ts` exports (`supabase:blocks:check` fails on drift): change a topic, run `pnpm supabase:blocks`, then `pnpm supabase:diff`. Never write a `realtime.messages` policy by hand. The inbox and chat broadcasts are `realtime.triggers` entries in the same config, which `pnpm supabase:blocks` writes into that file as one `better_supabase.bs_broadcast_*` function and trigger per table: each names its topic template, event and payload columns, with lookups for the channel thread (`org::inbox`, `org::thread:`, `org::chats`). Never write a broadcast trigger function by hand; `supabase/tests/database/inbox_broadcast.test.sql` checks the topics, events and payloads the clients read. The notifications module sends `org::notifications:`. The SaaS app subscribes through `useTopic`. jsonb columns with a stable shape get a `json` entry in `better-supabase.config.ts` with an inline `type` and a JSON Schema `schema`; the `jsonb-schemas` module turns the schema into a pg_jsonschema check. `tests/integration/cross-package/data/jsonb-schemas.test.ts` fails when an entry drifts from the Zod schema the services parse with, so update both together. Prefer typed columns or a child table with `position` over a new jsonb column. ## New extensions [#new-extensions] pg-delta does not plan objects in the `extensions` schema and does not write `create extension` for an extension that installs into its own schema, such as `pgmq`. `better-supabase sql sync` (and `sql add` and `sql upgrade`) writes every module extension that no migration creates into `_better_supabase_extensions.sql`, stamped before the next schema migration, and `sql data` refuses to run while one is missing. Create the schema migration with `pnpm supabase:diff -f ` after it. CentraKit's extensions migration (`pgmq`, `pg_jsonschema`, `pgcrypto`, `vector` and `citext`) is stamped before the init, so a fresh reset has them before any schema migration needs them. Queues (`pgmq.q_*` and `pgmq.a_*`) are created at runtime by `better_supabase.enqueue_job`, never in schema files. pgvector lives in `extensions`, where the `vector-search` module expects it. Qualify pgvector types and operators as `extensions.halfvec` and `operator(extensions.<=>)` in schema files. ## Event triggers [#event-triggers] An event trigger belongs to no schema, so `pnpm supabase:diff` (which passes a schema list) never plans one. When a module adds an event trigger, such as the audit module's `bs_audit_forget_dropped`, create that migration with `pnpm exec supabase db schema declarative sync --no-apply -f ` (no `-s`), check that it holds only the expected statements, and apply it as `postgres`. The `_event_triggers.sql` migration was written that way, and CI runs that unscoped sync too. ## Background jobs from SQL [#background-jobs-from-sql] A SQL function or trigger queues a job with `perform better_supabase.enqueue_job('', , , , '')`; the function must be `security definer` because the jobs functions are granted to `service_role` only. The queue must be listed in `packages/core/src/jobs.ts` with its payload schema and have a handler in `apps/api/lib/jobs/handlers.ts`. ## The `stripe` schema belongs to the Stripe Sync Engine [#the-stripe-schema-belongs-to-the-stripe-sync-engine] The Stripe Sync Engine installs and upgrades the `stripe` schema with its own migrations (`pnpm stripe:sync:migrate`; `supabase:reset` and `supabase:sync` run it with demo rows). Never define `stripe` objects in schema files and never add `stripe` to the `supabase:diff` schema list. A function that reads it is `plpgsql` and returns nothing when `to_regclass('stripe.
')` is null, because the schema can be missing when migrations run; a `language sql` function or a view over `stripe` tables would fail to apply. `platform/functions/billing_subscriptions.sql` holds those readers, and `billing.organization_plans` (an unexposed schema) is the plan of every organization. `public.organization_subscription` reads the tenant's subscription through the block's owner-only `better_supabase.billing_tenant_subscription(tenant)`. Platform-wide lists read the billing block's `better_supabase.billing_platform_subscriptions()`, `billing_platform_invoices()` and `billing_platform_customers()`, which carry the customer email, price metadata, `finalized_at` and `updated_at`, and join the plan catalog on `stripe_price_id`, falling back to the price metadata `plan_key` (`platform/functions/billing.sql`); the demo fixture in `packages/stripe/fixtures/sync-engine-demo.sql` links the catalog rows of its prices. pgTAP and the CI database job install the schema first. ## Grants live in the schema files [#grants-live-in-the-schema-files] pg-delta carries schema, table, column and function grants and revokes, comments and `security_invoker` on views into migrations, including the `supabase_auth_admin` schema usage and the grants on `permdock.custom_access_token_hook(jsonb)`, which `permdock:generate` writes inline. Declare every grant in the file that owns the object or in `shared/grants.sql`. Never append grants to a generated migration. ## No bulk grants [#no-bulk-grants] Never write `grant ... on all tables` or `on all routines in schema public`. pg-delta has no dependency that places such a statement after the per-file revokes, so it re-widened 229 functions and two secret tables to `anon` and `authenticated` in an earlier init. The default privileges in Supabase projects already grant the API roles on new objects, and each file revokes what it must. ## Security definer functions [#security-definer-functions] The default privileges give `anon` and `authenticated` their own EXECUTE grant on every new function, so `revoke ... from public` or `from public, anon` leaves the function callable by every signed-in user. Write `revoke all on function public.f() from public, anon, authenticated;` and then grant only the roles that call it: `service_role` for jobs and internal helpers, `authenticated` only when the app, a policy or a `security invoker` function calls it as the user. A trigger function gets the revoke and no grant. A `security definer` function that the API can reach checks the caller with the PermDock helpers before it reads or locks anything (`permitted_organization_ids('')`, `member_organization_ids()` for active staff, the row helpers for resource grants), and guards only signed-in callers (`(select auth.uid()) is not null and ...`) when triggers or service-role jobs also call it. Never match a storage path, customer id or other caller-supplied id without checking it belongs to the caller's organization. A table with RLS and no policies keeps the default grants, so give it an `expose` entry with `authenticated: []`. ## Multi-step writes go in one function [#multi-step-writes-go-in-one-function] PostgREST runs each request in its own transaction. When a service action writes several rows that must agree, such as a status change with its party snapshot, published version and status history, put the writes in one `security invoker` function and call it once (`send_quote`, `send_invoice`, `accept_quote`). RLS still applies to every statement inside, and the function locks the parent row with `for update` before it checks the status. A helper granted to `authenticated` that changes a document guards the document status itself, because the app is not its only caller. ## Column grants after a table-level revoke [#column-grants-after-a-table-level-revoke] pg-delta writes a column grant (`grant update (first_name) on public.profiles to authenticated`) before the table-level `revoke all on table ... from authenticated` that recreates the table privileges. Postgres drops the column privileges with that table-level revoke. The grants module writes column privileges as `expose` entries, so the generated init keeps them. When a migration rewrites table grants for `profiles` or `organization_billing_customers`, run `pnpm supabase:diff` once more and keep what it writes. `supabase/tests/database/grants.test.sql` checks the column grants. ## Default privileges are not diffed [#default-privileges-are-not-diffed] `alter default privileges` in `shared/grants.sql` does not reach migrations. Hosted Supabase projects already grant the API roles on new objects in `public`, and the schema files grant every table and routine explicitly, so nothing depends on it. ## Schema files hold no rows [#schema-files-hold-no-rows] pg-delta refuses a schema file that leaves rows in a table. Rows the database needs go in data migrations after the schema migration: - `_better_supabase_module_data.sql`, written by `better-supabase sql data`: module versions, reserved slugs and role settings. - `_reference_data.sql`: the built-in system and organization roles and the currencies. - `_integration_catalog.sql`, `_ai_model_settings.sql`, `_billing_plans.sql`, `_platform_settings.sql` and `_system_workflows.sql`: the integration catalog, the AI model allowlist and defaults, the billing plans with their usage quotas, the platform settings and the published system workflows. Production runs no seeds, so it gets these only from migrations. - `_permdock_seeds.sql`, written by `pnpm --filter @workspace/identity permdock:generate` (`--seeds-out supabase/migrations/`). Run it after `pnpm supabase:diff` so the seeds migration sorts after the schema migration. Add new reference rows in a new data migration named after the set it extends (`_billing_plans.sql`) with idempotent `on conflict` statements. Tests read these files with `readMigration("")` from `@workspace/test-fixtures/supabase-files`, which concatenates every migration with that name oldest first, so never reference a migration by its timestamp. Seeds hold demo data only. ## Squashing migrations [#squashing-migrations] `pnpm supabase:squash` (`packages/supabase-runtime/scripts/squash-migrations.sh`) deletes `supabase/migrations/` and writes it again from the schema files: the extensions, one `init`, the event triggers, the module data, the data migrations (each merged with its later increments) and the PermDock seeds. Run `pnpm supabase:blocks` first, then `pnpm supabase:reset`, `pnpm supabase:types`, `pnpm supabase:test` and `pnpm supabase:diff`, which must report no schema changes. A squash replaces migration versions that hosted databases have already applied, and the GitHub integration then fails with `relation already exists` because it starts the new init on a database that has the old schema. Squash only together with a reset of every hosted database, in the order of `docs/operations/database-reset.mdx`. Never squash for convenience. ## No data-driven DDL [#no-data-driven-ddl] pg-delta orders statements by dependency and cannot see what a `do` block reads, so a block that loops over `information_schema` runs before the tables exist and creates nothing. Write generated objects out statically, such as one `create trigger bs_audit` per table file. ## One definition per function [#one-definition-per-function] Define each function in exactly one file. With two definitions, the last one applied wins without any error. ## New tenant tables [#new-tenant-tables] A new table with `organization_id` and `id` gets a static `bs_audit` trigger in its table file whose JSON argument is its registration: `create trigger bs_audit after insert or update or delete on public.
for each row execute function better_supabase.audit_row_change('{"ignore": ["updated_at"], "redact": [], "category": "data", "key_columns": ["id"], "target_type": "
", "event_prefix": ""}');`. `better_supabase.audit_settings()` reads the argument, so no data migration is needed. Ignore timestamps, leases and search vectors; redact tokens, secrets and credentials. A table that must not be audited goes in `sql.modules.audit.options.exempt` in `better-supabase.config.ts`, which doctor BS315 reads. `pnpm supabase:blocks` writes one pgTAP file per audited table. Every table the schema files create gets the restrictive `bs_session_active` policy from `sql.modules.sessions.options.policies` in `better-supabase.config.ts`, so a signed-out, banned or deleted user's token stops working at once. `pnpm supabase:blocks` writes the policies into the sessions module file; list a table in `options.exclude` only when it must stay readable without a session. `rls.test.sql` fails on a table without the policy. The grants module writes every table's complete privilege set. `sql.modules.grants.options.fromPolicies` derives it from the table's permissive policies in the schema files (a role gets a privilege only when a policy for that command names it; `to public` counts for `authenticated`), so a new table needs no grant statement. A table whose privileges differ from its policies, such as an append-only log or a service-only table, gets an entry in `expose` in `better-supabase.config.ts`. Never write `grant ... on table` in a schema file; column privileges are `expose` entries too, such as `"update(username, first_name)"` for `profiles`. A client role without a privilege gets `42501` instead of an empty result, so a pgTAP check for a table it cannot reach uses `throws_ok(..., '42501', ...)`. Status columns are enums from the domain's `types.sql`, and keys are `uuid`. Document amounts are `numeric(14, 2)` and unit prices `numeric(14, 4)`; amounts that mirror a payment provider are `bigint` columns ending in `_cents`. Dates end in `_on` (a `date`) or `_at` (a `timestamptz`). ## Renamed columns and the ORM [#renamed-columns-and-the-orm] The better-supabase ORM rejects an object whose keys are all unknown, but accepts a stale key next to a valid one (`{ employeeId, retentionUntil: null }`). After renaming a column, search the services for the old camelCase name and add a request test for each write that touched it. ## Generated types [#generated-types] `pnpm supabase:types` runs `better-supabase gen` with `better-supabase.config.ts` at the repo root. It writes `packages/supabase-runtime/src/generated/` for both `public` and `graphql_public`, so keep both in the config's `schemas`. Never edit the generated files. ## Demo seeds [#demo-seeds] Files `012` to `048` in `supabase/seeds/` hold the seeded logins, their profiles and the seeded system staff roles. Catalogs, plans, settings and system workflows are data migrations. Files `100` to `170` hold the demo tenants, one domain per file, and each is one `do` block that starts with a `-- Covers:` comment. The header of `100_organizations.sql` is the registry: the twelve organizations, every persona and the table codes used in seeded ids. - Seeded ids follow `{TT}{O}00000-0000-4000-8000-{NNNNNNNNNNNN}`. Pick a table code that the registry does not list yet and add it there. `demo-seeds-static.test.ts` fails on an unregistered code. - Delete earlier rows with the exact pattern `'TT_00000-0000-4000-8000-%'`. A bare `'TT%'` also matches rows that triggers created with random uuids. - Write every scenario by hand. Message text cites names and document numbers (`TASK-1`, `Q-8`, `INV-2026-4`), never uuids. - Use only product and supplier names that exist in `111_catalog.sql`. - No seeded person shares a surname with another seeded person, and near-duplicates such as Jansen and Janssen are avoided too. Check before you add a contact, candidate or invitation. - Audit logs are append-only. Wrap a seed delete in `set_config('better_supabase.audit_purge', 'on', true)` and set it back to `off`. - Seeds create documents in their final state, so they insert rows such as `quote_accepted_optional_lines` and `document_parties` directly instead of calling the RPCs that guard the document status. - `170_integrity.sql` runs last. It recomputes allocations, order totals and number sequences, so add new numbered documents before it. - Storage objects for seeded rows live in `supabase/seed-buckets//` and load on `pnpm supabase:reset`. Columns store the object path only. - Test a single file without a reset by piping it through `begin; ... rollback;` into `psql "$(pnpm exec supabase status --output-format json | jq -r .env.DB_URL)"`. ## Seeds on the persistent develop branch [#seeds-on-the-persistent-develop-branch] The Supabase integration records every seed file it runs in `supabase_migrations.seed_files`. On a push it runs only seed files it has not recorded yet. It skips a file that already ran, even when the file changed. Adding new seed files on top of an older seed set therefore runs them against data they did not expect, and edits to `012` or other earlier files never reach develop. - The seeds must still survive a rerun locally. `012_auth_users.sql` deletes the seeded organizations by slug and by id before it deletes the seeded users, with `app.skip_allocation_release` and `app.shipment_transition` switched on. Team ids are pinned in `101_teams.sql` because later files reference them. - When a seed restructure breaks the Supabase Preview check, reset the develop database. _Reset branch_ in the dashboard does not drop data; it only redeploys. - Reset develop from the CLI. `branches get` returns the develop connection string with its password, and never returns one for `main`: ```sh pnpm exec supabase db reset --db-url "$(pnpm exec supabase branches get develop --project-ref gunugimgrrnxqynjghqe -o env | grep '^POSTGRES_URL=' | cut -d= -f2- | tr -d '"')" ``` - Never use `--linked` for this. The local link points at production. - A remote reset drops every object in one transaction and fails with `out of shared memory` at the default lock limit. Develop runs with `max_locks_per_transaction=1024`. If a new branch needs it, set it with `pnpm exec supabase postgres-config update --experimental --project-ref --config max_locks_per_transaction=1024`, which restarts that database. - Production keeps seeding disabled, so seed changes never need a production reset. A migration squash does; see `docs/operations/database-reset.mdx`. # Tenancy and routing (/docs/agents/tenancy-and-routing) Organization slugs, brands, synchronous page shells and Suspense islands. ## The slug is the tenant [#the-slug-is-the-tenant] Tenant routes live under `app/[locale]/[brand]/(app)/[orgSlug]/`. There is no current-organization cookie and no organization header. Pass `orgSlug` from route `params` into Suspense islands with `params.then(...)`, and resolve access with `requireAccess({ tenant: orgSlug, permission })` from `apps/saas/lib/organizations/require-access.ts`. ## The host is the brand [#the-host-is-the-brand] The proxy maps the request host to a brand and inserts it after the locale in the next-intl rewrite, so `[brand]` never appears in public URLs. Read it with `getBrand()` from `apps/saas/lib/brand/current-brand.ts`. It uses the `brand` root param during rendering and falls back to the host header in Server Actions and route handlers, where root params are unavailable. The CentraKit host serves every organization, and a product brand host only its own; `brandServesOrganization()` from `@workspace/foundation/brands` decides, and `requireOrgSlug` and `requireAccess` return `notFound()` when it refuses. ## Page shells stay synchronous [#page-shells-stay-synchronous] Pages and layouts do not await data or import feature queries. Put `` around async feature components and export each skeleton beside the component it stands in for. Do not register `orgSlug` in `next/root-params`; only `locale` and `brand` use root params. ## Pass typed hrefs so tsc stays fast [#pass-typed-hrefs-so-tsc-stays-fast] Typed routes are on. The generated patterns include the hidden `[locale]/[brand]` segments, so inferring the route type from a value already typed `Route` takes minutes per call site in `next/link`, `useRouter()` and `redirect` from `next/navigation`. - Write literal and template hrefs inline, without a cast: ``redirect(`/${slug}/roles`)``. Next infers and checks them quickly. - When the href is a variable or prop typed `Route` (or a runtime string cast to `Route` once, with a `SAFETY` comment), pass the type argument: ` href={cancelHref}>`, `router.push(href)`, `redirect(href)`. - Type router parameters as `ReturnType`, not as a hand-written `{ push(href: Route) }` shape. The `next-intl` `Link` and router from `@/lib/i18n/navigation` take plain strings and are not affected. `tests/integration` includes the generated `link.d.ts` so it checks SaaS sources with the same route types; cast test hrefs passed to `Route` props with `as Route`. Time `pnpm --filter saas typecheck` after adding a call site; it should finish in seconds. ## Read the installed Next.js docs [#read-the-installed-nextjs-docs] Next.js 16 differs from older examples. Read the relevant guide in `node_modules/next/dist/docs/` before changing caching, prefetching, or routing behavior. ## Do not redirect to login from the app [#do-not-redirect-to-login-from-the-app] The proxy is the authentication boundary. Adding an in-app redirect to `/login` when a request context has no session can create a refresh loop. # Testing (/docs/agents/testing) Where tests live and how they resolve paths. ## Where a test goes [#where-a-test-goes] A test that covers one workspace lives in that workspace's `tests/` folder, never next to the source. A test that imports from several workspaces goes in `tests/integration/cross-package//`. Database checks go in `supabase/tests/database/` as pgTAP. ## Shared helpers [#shared-helpers] Import setup files and helpers from `@workspace/test-fixtures`. Keep that package free of internal workspace dependencies, because workspaces depend on it and anything else creates a cycle. ## Live database tests [#live-database-tests] Live integration tests reach this worktree's local project through `tests/integration/helpers/local-supabase-status.ts` (API keys) and `tests/integration/helpers/local-sql.ts` (SQL over `DB_URL`, printed like `psql -t -A`). Do not `docker exec` into a database container: the stack engine names containers per project, and the native runtime has none. psql meta-commands such as `\g /dev/null` do not work there; wrap setup calls in a `do` block with `perform`. ## Resolve paths from the test file [#resolve-paths-from-the-test-file] Tests run from different working directories under Turbo, Vitest projects and editors. Build file paths from `import.meta.dirname`, not `process.cwd()`. When a test reads a schema file, include its domain folder, for example `supabase/schemas/platform/functions/audit_log.sql`, and read data migrations with `readMigration("")` instead of a timestamped path. ## Commands [#commands] `pnpm test` runs everything through Turbo. `pnpm test:unit`, `pnpm test:integration` and `pnpm supabase:test` run one layer. Inside a workspace, `pnpm exec vitest run ` runs a single file. # Toolchain (/docs/agents/toolchain) pnpm, TypeScript 7, Oxlint and Oxfmt mistakes to avoid. ## Versions come from the catalog [#versions-come-from-the-catalog] Look up a version with `pnpm view version` when you add or bump it, and pin it exactly in the `pnpm-workspace.yaml` catalog. Workspaces reference it as `"catalog:"`. Do not rely on a version you remember. ## The release-age gate stays on [#the-release-age-gate-stays-on] `minimumReleaseAge` (1440 minutes) blocks packages published less than a day ago. Do not disable it or add broad excludes to install a new release. Pick the newest version that clears the gate, and add a narrow, commented exclude only for the packages already listed in the toolchain rule. ## TypeScript 7 only [#typescript-7-only] Typecheck runs `tsc --noEmit` from TypeScript 7; it keeps the default of four checkers (ADR 0017). Do not add TypeScript 5 or 6 shims for repo code, and do not enable Oxlint's `typeCheck` option. Omit optional properties instead of passing `undefined`, and read index signatures with `obj["key"]`. ## Lint is Ultracite [#lint-is-ultracite] Ultracite 8 supplies the Oxlint and Oxfmt presets. The root `oxlint.config.ts` extends `core`, `vitest`, `shadcn`, and `anti-slop`, plus the `react-doctor` and `tsdoc` plugins, with `typeAware` on. `eslint-plugin-github` stays out: it loads `typescript-eslint`, which rejects TypeScript 7. Next.js apps also extend `react`, `next`, and `next/js-plugins`. React workspaces extend `react`. Other workspaces extend the root config only, and each one repeats `ignorePatterns` and `settings` because Oxlint does not inherit those through `extends`. Run `pnpm lint` (Turbo) for workspaces and `pnpm lint:root` for root tooling files. `pnpm check` and `pnpm fix` run `ultracite check` and `ultracite fix`. Fix the code when a rule fires. Do not turn a rule off. A disable comment is only for a case the code cannot change, and it names the rule and the reason. House rules after the presets ban the schema and icon packages the repo does not use, read `process.env` only in env modules, config files, scripts, and tests, and keep Testing Library checks on test files. ## Type assertions need a reason [#type-assertions-need-a-reason] The anti-slop plugin requires a `// SAFETY:` comment above every `as` assertion that explains why it holds. Prefer a schema parse or a typed helper, and never write `as unknown as T`. ## No code comments [#no-code-comments] Do not add code comments or doc blocks: no `//` explanations, JSDoc or TSDoc, `/* */` blocks, or `--` explanations in hand-written SQL. Say it with names instead. The exceptions are lint suppressions the toolchain requires, the `// SAFETY:` comments above, and the route JSDoc that `pnpm openapi:generate` reads. Before a commit, `git diff --cached | grep -E '^\+\s*(//|/\*|\*|--)'` shows any you added. ## Formatting [#formatting] Oxfmt formats with semicolons at print width 80, from `ultracite/oxfmt` plus the house sort and ignore settings in `oxfmt.config.ts`. Run `pnpm format` (root files plus every workspace). Vendored ReUI, AI Elements, and `cropper.tsx` stay on the lint and format ignore list. ## Typed routes and `redirect` [#typed-routes-and-redirect] Passing the typed-routes `Route` union straight to `redirect()` from `next/navigation` makes `tsc` hang with no output. Write `redirect(toRoute(...))`, as the existing auth and OAuth pages do. ## Skills come from the skills CLI [#skills-come-from-the-skills-cli] Install, update and remove skills with `pnpm dlx skills add`, `pnpm dlx skills update -p -y` and `pnpm dlx skills remove`, so `skills-lock.json` lists them. `reui` is the one exception: the ReUI installer writes it, and its header names the version to compare against the ReUI MCP's `get_agent_skill`. Update it by running the ReUI installer again, not by editing the folder, and keep `.agents/rules/reui.mdc` in step. ## Stale incremental build info [#stale-incremental-build-info] `tsgo` can report phantom errors, such as TS4111 on a property the schema declares, from a stale `tsconfig.tsbuildinfo`. Delete that workspace's `tsconfig.tsbuildinfo` and run the typecheck again before changing code. # Translations (/docs/agents/translations) The .po catalogs and extracted messages. ## Use extracted messages [#use-extracted-messages] Copy uses `useExtracted` in client components and `getExtracted` on the server. Do not use `useTranslations`. ## Call the translator where the messages are [#call-the-translator-where-the-messages-are] The next-intl compiler only rewrites a `t({ id, message })` call when `t` comes from `useExtracted` or `getExtracted` in the same file and scope. Do not pass `t` as a prop, a function argument, a hook return value or a column `extra`. The call then stays uncompiled, the build logs `MISSING_MESSAGE: c.split is not a function`, and the text renders blank. Call `useExtracted` in the component or hook that renders the copy, or pass the resolved strings instead. ## Every catalog needs every message [#every-catalog-needs-every-message] An empty `msgstr` renders as blank text in the app. When you add or change copy, fill in the entry in every `.po` catalog under `apps/saas/translations/`, including `de.po`. German uses the formal address Sie. Dutch stays informal. ## The dev server rewrites the catalogs [#the-dev-server-rewrites-the-catalogs] A running SaaS dev server re-extracts messages and can rewrite `en.po`, `nl.po` and `de.po` with entries removed. Before you run tests or commit, check `git diff` on those files. If you did not change copy, restore them with `git checkout HEAD -- apps/saas/translations/en.po apps/saas/translations/nl.po apps/saas/translations/de.po`. # Writing docs (/docs/agents/writing-docs) How to write and update pages in the public docs so they match the shipped product. The public docs site renders the root `docs/` folder. The site lives under `/docs` on each brand's app host. AI agents read the same pages through `/llms.txt`, `/llms-full.txt`, the per-page `.md` URLs and the docs MCP server at `/docs/mcp`, so every page has to be accurate on its own. ## Describe shipped behavior from code [#describe-shipped-behavior-from-code] Agents tend to rewrite an old page or copy `PRODUCT.md`, and both drift from the product. Write each page from the source instead: 1. Read the matching `PRODUCT.md` section for intent and vocabulary. 2. Read the feature slice in `apps/saas/features//`: the `*-actions.ts` and `*-queries.ts` files and the component names. 3. Read the route folder under `apps/saas/app/[locale]/[brand]/(app)/[orgSlug]/` (or `admin/`) for the real paths and tabs. 4. Read the permission entries in `packages/identity/src/permdock/permissions.ts` and the role grants in `policy.ts`. The PermDock policy is the source of truth. 5. Read the API routes under `apps/api/app/api/v1/` and the MCP allowlist `mcpToolDefinitions` in `packages/api-client/src/mcp.ts`. 6. Confirm labels, tabs and flows in the running app with `agent-browser` on `https://app.localhost` and the seeded logins, or read the English strings in `apps/saas/translations/en.po`. Describe only what ships. For a roadmap item, write one line that starts with "Not yet available" instead of prose about what the feature is intended to do. ## Page template [#page-template] Use these H2 sections in this order and leave out the ones that would be empty: 1. Overview: what the feature is and the route where it lives. 2. Key concepts: entities, statuses and lifecycle stages. 3. How-to sections for the main flows, one H2 or H3 per task. 4. Permissions: the PermDock keys that gate the feature, such as `quote.send` or `platform.user.manage`. 5. Automation and AI: workflow triggers, MCP tools and AI actions. 6. API: the REST resources, with links to the generated `/openapi/...` pages. 7. Related pages. The frontmatter `description` is one specific sentence. `llms.txt` is built from it, so it must say what the page covers. ## Conventions [#conventions] - Prefer plain Markdown over MDX components. `Cards` belongs only on `index.mdx`. Use Markdown tables for reference data. - Link to other pages with root-relative paths such as `/product/quotes`, without a `/docs` prefix. The docs app is served under `basePath: "/docs"` and Next adds the prefix to rendered links. Code that builds absolute URLs, fetches or Markdown output uses `docsPath()` from `apps/docs/lib/base-path.ts` instead. - Public URLs in pages use the brand app host, such as `https://app.centrakit.com/api/v1/...`, `https://app.centrakit.com/mcp` and `https://app.centrakit.com/docs/...`. The `api.` and `docs.` subdomains no longer exist, so do not write them outside historical notes. - Every page must be listed in its folder's `meta.json`. `tests/integration/docs/docs-tree.test.ts` fails on orphan pages, missing frontmatter and broken internal links. - When you remove or rename a page, add a redirect in `apps/docs/next.config.ts`. - Follow the repo writing rules: complete sentences, no em dashes or arrow chains in prose, no emojis or exclamation marks, and none of "simply", "seamless", "robust" or "leverage". # Audit Logs (/docs/architecture/audit-logs) Immutable semantic security events, access boundaries, retention, and export workflows. CentraKit audit logs are the product-facing security record for tenant and platform mutations. They are separate from operational telemetry, PostgreSQL/PGAudit diagnostics, and log drains, which remain debugging and infrastructure-observability tools. ## Architecture [#architecture] The domain follows the standard provider-swappable service layers: - `packages/platform/src/audit-log/` defines event schemas, the semantic event catalog, cursor encoding, errors, and `AuditLogService`. - `packages/supabase-platform/src/audit-log/` implements the service on the better-supabase audit block: `record()` writes semantic events through `better_supabase.audit_event` over a direct admin Postgres connection, passing the trusted actor, request ID and scope, and `list()` reads pages through `list_audit_events` and `count_audit_events` as the caller. - `packages/core` wires request-scoped services and trusted actors; apps never import the provider directly. Coverage is hybrid. The better-supabase `audit` module, adopted over `public.audit_logs` and `public.audit_log_restricted_details`, captures every registered table mutation with its `bs_audit` row trigger as a durable baseline (each trigger carries its registration as a JSON argument, exemptions in `sql.modules.audit.options.exempt`), while application mutation adapters emit semantic events with business-level summaries, targets, outcomes, request IDs, correlation IDs, and idempotency keys. Auth and webhook boundaries use the same semantic model. Audit recording must not add user-visible latency or turn a successful business mutation into a failure. Request paths schedule best-effort recording after the primary mutation; durable workflows record their own outcomes. Trigger capture remains the fallback for registered tables. ## Data Safety And Immutability [#data-safety-and-immutability] Audit rows are append-only: the module's `bs_audit_append_only` trigger rejects updates and deletes, and only `better_supabase.purge_audit_log()` deletes old entries. Row changes keep the changed column names on the entry and the old and new values of each changed column, redacted and cut at 1000 characters, in the restricted details. The module's own values are mapped onto CentraKit's (`tenant` scope to `organization`, `support` to `platform_admin`, `oauth-client` to `integration`). The module's impersonation columns are mapped to `impersonated_by`, `impersonation_reason` and `support_session_id`. A row changed during a support session keeps the target as the actor, stored with the actor kind `platform_admin`, and records the admin, the reason and the session from the token's `act` claim, so `audit_logs_support_session_idx` lists everything one session did. The `support-sessions` module writes `support.started` and `support.ended` in platform scope, and the `public.after_support_start` and `public.after_support_end` hooks add one entry per organization of the target; all of them fill the three impersonation columns. Semantic events recorded through `record()` during a session carry the admin as the trusted actor and pass the session, the admin and the reason as `supportSessionId`, `impersonatedBy` and `impersonationReason`. Safe metadata is limited to 4 KiB and rejects credential, cookie, password, secret, session, and token-like keys. Restricted request context is stored separately and excludes secrets and tokens. Organization views mask platform-admin actor identity. Revealing restricted fields requires a dedicated permission and goes through the module's `reveal_audit_entry`, which records an `audit.revealed` entry. ## Reading And Routes [#reading-and-routes] Overview lists use the block's offset pages with an exact filtered count (`list({ offset, count: true })`) so the table can share the same sort, filter, and pagination chrome as other hubs. Search matches the event type, summary, target, actor and organization labels and the target ID. Export workflows walk stable keyset cursors over `(occurred_at, id)` with the block's `before` cursor. - Organization list/detail: `/{orgSlug}/audit-log` and `/{orgSlug}/audit-log/{eventId}` - Platform list/detail: `/admin/audit-log` and `/admin/audit-log/{eventId}` Organization reads are always scoped by the organization resolved from the route. Platform reads may span organizations. The navigation entries and page guards require `auditLog.read` or `platform.auditLog.read`. Permissions are split by operation: - `auditLog.read|export|reveal` - `platform.auditLog.read|export|reveal` ## Exports And Retention [#exports-and-retention] CSV exports are asynchronous Workflow jobs. Requests normalize a bounded date range, then the workflow reads cursor pages, applies row and byte limits, writes a private storage object, and exposes it for 24 hours. Restricted fields are available only for authorized platform exports. Export request, completion, failure, download, and reveal operations are audited. The daily `audit_log_retention` job purges audit events with `purgeAuditLog` in bounded batches, using the `public.audit_retention(organization)` hook for each organization's interval (365 days for platform events), then a workflow deletes expired export objects before marking jobs expired. Default retention is 1 day for Starter, 7 days for Pro, 90 days for Business, and 365 days for Enterprise. Enterprise subscriptions may set a platform-managed organization override. # Authentication (/docs/architecture/authentication) Supabase Auth ownership, session posture, OAuth linking, and deferred controls. CentraKit uses Supabase Auth for user identity. Application code validates inputs, maps provider errors to domain errors, and avoids returning password material, while Supabase owns password hashing, access-token issuance, refresh-token rotation, and browser/server session cookie format. ## Current Security Posture [#current-security-posture] - Access JWTs are configured for 15 minutes locally via `supabase/config.toml`; mirror this in hosted Supabase settings. - Refresh tokens are Supabase-managed and rotated on use. Local config uses a seven-day session timebox to cap the refresh chain. - Email confirmation is required for email/password signups. - Password hashing is handled by Supabase Auth. Local seed users use `gen_salt('bf', 12)`. - Session cookies are managed by `@supabase/ssr` with Supabase-compatible options. - Logout calls `supabase.auth.signOut()`. Immediate app-owned token blacklisting is deferred. - SaaS application routes are protected at the proxy/router level. Link and Partial Prefetching requests still receive locale rewrites, but skip session cookie refresh so a prefetch cannot consume the single-use refresh token. - API Route Handlers use the policy-driven `withApiRoute` adapter. User routes accept either a refreshed SSR cookie or an OAuth bearer; a present but invalid bearer never falls back to cookies. Cron and provider-webhook policies retain their own credential contracts. - The adapter composes `@supabase/server` after credential normalization. Its RLS-scoped Supabase client is always safe by default, `supabaseAdmin` stays lazy and request-memoized, and direct Postgres clients are available only on routes that explicitly opt into `postgres: "scoped" | "admin" | "both"`. ## JWT Claims And Signing Keys [#jwt-claims-and-signing-keys] Use `supabase.auth.getClaims()` for server-side checks that only need verified identity claims, such as authenticated state, current user ID (`sub`), current email, session ID, or coarse role hints. Use `supabase.auth.getUser()` only when a flow needs the fresh Auth user record, linked identities/providers, email confirmation state, or other Auth-server metadata. Use `getSession()` only when raw access or refresh tokens are needed. Hosted Supabase projects should use the new JWT signing keys system with an asymmetric key, preferably ES256. `getClaims()` can then verify tokens with cached JWKS/WebCrypto instead of putting the Auth server in the hot path. Before rotating away from the legacy JWT secret, confirm no app, API, Edge Function, or integration verifies tokens with the legacy shared secret directly. After rotation, wait at least one access-token lifetime plus safety margin before revoking the previously used legacy key; local config uses a 15-minute JWT lifetime. `permdock.custom_access_token_hook` adds PermDock's claims to access tokens: the global roles (`user_role`, `roles`), the organization `memberships`, `authz_ver` and the `datetime_preferences`. Keep the hook lean because it runs when Supabase issues or refreshes tokens, claims are stale until the next token refresh, and large claims bloat the chunked SSR cookies, which can bounce post-login redirects back to `/login`. The memberships stay within PermDock's byte budget and `memberships_truncated` marks a token that left some out. See [RBAC](/docs/architecture/rbac) for how the claims are read. ## Support Sessions [#support-sessions] Platform admin impersonation uses better-supabase support sessions; no Auth session is minted for the target. The `support-sessions` SQL module stores sessions in `better_supabase.support_sessions`, and `start_support_session` checks `platform.user.impersonate` with the admin's own claims, refuses the admin as target, caps the session at 60 minutes and ends the admin's previous session. `packages/supabase-runtime/src/support.ts` builds `bs.support` with `createServer(betterSupabase, { postgres, support: supportSessions({ store: sqlSupportStore(postgres) }) })` and is the only place that talks to the store. The session id travels in the HttpOnly `bs-support` cookie. `createRequestContext()` reads it after verifying the admin's own token and calls `support.current()`, which loads the session as the admin, checks the permission again and builds the target's claims through `permdock.custom_access_token_hook` (the module's `claimsHook`) plus `act: { kind: "support", sub, session_id, reason, read_only }`. With a running session the request context: - runs `db` over direct Postgres as the target with those claims (`postgres.asUser(claims, { readOnly })`), so RLS sees the target as `auth.uid()` and a read-only session fails every write with `25006`; - runs the blocks (comments, notifications, attachments, organizations, audit, flags, usage, data lifecycle and API keys) over the same connection, because their connectors use `dbTransport(db, { schema: "api" })`, which calls the functions through `db`'s executor; - gives `db.$client` and `supabase` a client without a user token, so Storage and Auth never act as the admin; public Storage URLs still resolve; - builds PermDock from the target's claims, so `subjectFromSupabase` carries the `support` actor; - keeps the admin in `sessionCaller` and the session in `support`, for the banner, the stop action and Sentry. A cookie whose session ended or expired resolves to the admin's own context with `support.state` set to `ended`, so a stale cookie never acts as the target. The banner then clears the cookie and shows `/impersonation/expired`. A token with an `act` claim never holds platform permissions (`better_supabase.platform_can`) and cannot start another session. `start_support_session` refuses a target with a system role (`public.roles.scope = 'system'`) through `platformRoles.through.where`. The API app composes the same context for cookie-authenticated `/api/v1` requests, which the SaaS browser reaches through same-origin rewrites. The route adapter reads the `bs-support` cookie only when the request carries a cookie session, and `composeApiProviderContext` builds the context with `createCookieRequestContext`, so route handlers, the AI chat runtime and its Workflows act as the target and audit the admin. A cookie whose session has ended answers `403` with the problem code `SUPPORT_SESSION_ENDED` before any route authorization runs. Bearer, API-key and MCP requests never run in a support session, even when the browser also sends the cookie. The `support_session_expiry` job runs `bs.support.expire()` every five minutes. It calls `better_supabase.expire_support_sessions()` as the service role, which ends each session past its expiry with `ended_by` `expired`, so `support.ended` is recorded for sessions nobody stopped. ## Google OAuth [#google-oauth] The SaaS login and register forms start Google OAuth through Supabase Auth. The hosted Supabase project owns the Google provider credentials; Vercel environment variables may mirror those values but do not configure the hosted Supabase provider by themselves. Supabase owns account linking. When a confirmed email/password user later signs in with Google and Google returns the same verified email address, Supabase automatically links the Google identity to the existing user. Do not add a public email-existence lookup or app-owned merge path. For users created through Google first, add email/password login only after the user has proved email ownership and has an active Supabase session. Use Supabase recovery or `updateUser({ password })` through the app's existing auth service methods. ## Local OAuth [#local-oauth] For Google Cloud OAuth clients, use the Supabase callback URL as the authorized redirect URI. Local Google OAuth uses Portless aliases under `localtest.me` because Google rejects `.localhost` subdomain callbacks. Register this callback for local testing: ```text https://supabase.localtest.me:1355/auth/v1/callback ``` Then start the OAuth dev target: ```bash pnpm dev:oauth ``` ## Email Confirmation Flow [#email-confirmation-flow] Signup and other token-hash emails (confirmation, recovery, magic link) link to the SaaS app's `/auth/confirm` route, which calls `verifyOtp({ token_hash, type })`. This avoids the PKCE `code` exchange at `/auth/callback`, which requires the verifier cookie set when `signUp` ran and breaks when the email is opened in a different browser, profile, or in-app webview. Locally, the confirmation template lives at `supabase/templates/confirmation.html` and is registered in `supabase/config.toml` under `[auth.email.template.confirmation]`. The `` href uses `{{ .RedirectTo }}&token_hash={{ .TokenHash }}&type=signup`, where `RedirectTo` is the `emailRedirectTo` URL the app sets when calling `signUp` (`https://app.localhost/auth/confirm?redirectPath=...`). After editing the template or config, restart Supabase: `pnpm exec supabase stop && pnpm supabase:start`. For hosted Supabase projects, mirror this in the dashboard: - On the **URL configuration** page under **Authentication**: - Set **Site URL** to the production SaaS origin (`https://app.`). - Add `https:///auth/confirm` (and any preview hosts you use) to **Redirect URLs**, alongside the existing `/auth/callback` entries kept for OAuth. - In the **Confirm sign up** template under **Authentication** and **Email templates**, paste the same HTML as `supabase/templates/confirmation.html` (adjust copy only as needed). The recovery and magic link templates still use the default `/auth/callback` link until those flows are migrated. OAuth must keep using `/auth/callback` because providers return a `code`, not a `token_hash`. ## Deferred Controls [#deferred-controls] The repo does not currently maintain app-owned login counters, account lockout tables, unlock tokens, or a token blacklist. Revisit those controls if Supabase-native limits, short JWTs, and generic errors are not enough for the deployed threat model. ## Resend Boundary [#resend-boundary] Use Supabase's built-in email flows for confirmation and password recovery. If a future security email is not covered by Supabase, add a small Resend boundary with `RESEND_API_KEY`, a verified transactional sending domain, idempotency keys, and disabled click/open tracking for sensitive transactional email. # Bundling and file tracing (/docs/architecture/bundling-and-file-tracing) Keep runtime assets available in Next.js and Workflow server bundles. Next.js uses `@vercel/nft` during production builds to emit an `.nft.json` file for every server route. Each trace lists the files that must be available when that route runs in a Vercel function or another minimal deployment. ## Monorepo tracing root [#monorepo-tracing-root] Every CentraKit app passes the repository root to the shared config factory as `outputFileTracingRoot`. This is explicit even though Next.js can infer the same root from the workspace lockfile. The explicit root is required for two repository conventions: - pnpm stores real package files under the repository-level `node_modules/.pnpm/` directory. - Some apps read generated or authored files from sibling apps at runtime. Do not move the root back to an inferred default. A lockfile or build invocation change could otherwise make valid workspace dependencies fall outside the trace. ## When to add an include [#when-to-add-an-include] Normal static `import`, `require`, and recognizable `fs` calls should be left to nft. Add `outputFileTracingIncludes` only when runtime code deliberately prevents static discovery, or when a deployment treats the file separately from the server function. Current examples: - **Generated OpenAPI spec:** the API MCP route reads `apps/api/public/openapi.json` at request time. Vercel serves `public/` separately, so `/api/mcp` explicitly includes the file in its function trace. - **Cross-app docs sources:** the docs app reads the API spec and app `CHANGELOG.md` files outside `apps/docs`. Its server traces include those sibling files. - **Barcode WASM:** the zxing route assembles the module specifier from string parts so Turbopack serves the Emscripten binary as bytes instead of compiling it. That also hides the file from nft, so the route explicitly includes `zxing_reader.wasm`. - **PDF fonts:** `@workspace/quote-pdf` resolves its Inter TTFs through a runtime `createRequire` because Turbopack rewrites `new URL(..., import.meta.url)`. The quote, invoice, order, and purchase-order PDF routes, the quote/invoice export routes, and the Workflow flow route include the fonts plus the package `package.json` used for the `exports` lookup. Include keys are route globs and include values are resolved from the app directory, not the monorepo root. Keep both as narrow as possible. Fully static routes do not produce server trace files, so includes do not apply to them. ## PDF and Workflow bundles [#pdf-and-workflow-bundles] `@react-pdf/renderer` and `qrcode` stay in `serverExternalPackages`. Their runtime require graphs are visible to nft, and the monorepo tracing root lets nft follow those graphs into pnpm's store. Do not add a broad PDF package include unless a build trace proves a required asset is missing. Workflow v5 owns transpilation for `workflow` and `@workflow/*`; do not add those packages to `serverExternalPackages`. `withWorkflow` generates `app/.well-known/workflow/v1/flow/route.js`, which Next.js traces like any other server route. Step-only imports therefore belong in the flow trace, including React PDF when an invoice or quote step renders a document. Keep `app/.well-known/workflow/**` in `apps/saas/turbo.json` and `apps/api/turbo.json` build outputs (Workflow SDK [Turborepo caching](https://workflow-sdk.dev/docs/getting-started/next#turborepo-caching)). Local builds resolve `@workflow/world-local`; Vercel builds resolve `@workflow/world-vercel`. Because both services ship Workflow consumers in one Vercel project, pin distinct `WORKFLOW_QUEUE_NAMESPACE` values (`saas` / `api`) so queue topics become `__saas_wkf_workflow_*` and `__api_wkf_workflow_*` instead of colliding on `__wkf_workflow_*` / consumer `default`. ## Trace size hygiene [#trace-size-hygiene] Only exclude a package when the server can never execute it. Streamdown performs syntax highlighting during SSR, so Shiki language and theme assets are real server dependencies and must not be excluded. CentraKit instead overrides `@streamdown/code` to the catalog Shiki version. This prevents pnpm and nft from carrying two complete grammar bundles while preserving server-side highlighting. ## Verification [#verification] Build the traced apps and run the regression guard: ```bash pnpm test:tracing ``` The task builds all five Next.js apps, then checks: - every app reports the repository as `outputFileTracingRoot`; - the API and docs runtime data files are present; - the zxing WASM binary is present; - both a PDF route and the Workflow flow route contain React PDF dependencies; - the PDF routes and the Workflow flow route contain the Inter PDF fonts; - the SaaS trace contains one Shiki language-bundle version. The guard lives in `tests/integration/build/output-file-tracing.test.ts`. It resolves every path relative to its `.nft.json` file, matching Next.js trace semantics. For manual investigation, inspect: - `apps//.next/required-server-files.json` for the serialized tracing configuration; - `apps//.next/server/app/**/route.js.nft.json` for route dependencies; - `apps/saas/.next/server/app/.well-known/workflow/v1/flow/route.js.nft.json` for the combined Workflow step bundle. # Cache Components And Streaming (/docs/architecture/cache-components-and-streaming) Next.js static shells, Suspense islands, access gates, and cache tag conventions. The SaaS app uses Next.js Cache Components and a static-first app shell. The goal is perceived instant navigation: persistent chrome, static page shells, and dynamic work confined to small Suspense islands. ## Scopes [#scopes] 1. **App providers + chrome:** `app/[locale]/[brand]/(app)/layout.tsx` owns persistent providers (`SidebarProvider`, command-palette state, viewport sync) and one `AppShell`. Session chrome streams into a sidebar island; page children stay mounted as siblings. 2. **Tenant islands:** `[orgSlug]/layout.tsx` runs slug membership and brand chrome in Suspense. Navigation presentation (`organization` / `customer` / `admin`) is client-side and independent of authoritative access gates. 3. **Page shell:** the page default export stays synchronous and mostly static. 4. **Suspense islands:** dynamic data and access checks (`requireOrganizationAccess`, `requireCustomerPortalAccess`, `requireSystemAccessAdmin`) run inside the smallest useful server component boundary, never in shell layouts. ## Rules [#rules] - Keep layouts synchronous unless they own dynamic chrome. - Treat route `page.tsx` files as synchronous compositors. Pages arrange static shell, error boundaries, Suspense, and feature panels; they do not own domain reads. - Put one async server feature panel behind one Suspense boundary. - Prefer sibling boundaries for independent fetches. - Use shape-matched skeletons to avoid layout shift. - Run `requireOrganizationAccess()` and `requireSystemAccessAdmin()` inside page islands, not layouts. - Keep feature panel skeletons beside the panel they mirror. Route-level `loading.tsx` files should reuse those feature skeletons for cold navigations instead of defining a second fallback shape. - Follow `nextjs-app-architecture` v1.3.9. Page Suspense is primary; shaped `loading.tsx` is a CentraKit exception for hard/cold nav and Partial Prefetch chrome (not next-beats' zero-loader model). A page `` cannot animate the swap out of `loading.tsx`. - Cached query arguments are the cache key: normalized primitives or a fully-normalized filter object produced before the `'use cache'` call. Never clamp or default inside the cached function, and never pass raw `searchParams`. - Keep `data-pending` on the pending node. Do not add `group-has-data-pending:` on wrappers that also contain Links; name the reason (`data-filtering`, `data-saving`) if a subtree has more than one pending source. - Dense skeleton grids use a non-animated `Skeleton` (`animated={false}`); keep the pulse for a handful of bars. ## Feature Panel Ownership [#feature-panel-ownership] CentraKit uses singular feature-sliced folders under `apps/saas/features//`: - `apps/saas/features//-queries.ts` owns cached, server-only reads. - `apps/saas/features//-actions.ts` owns `'use server'` mutations (not route-colocated `app/**/actions.ts` or `lib//actions.ts`). - `apps/saas/features//components/` owns domain UI, request access checks, service orchestration, and colocated skeletons. - `apps/saas/components/` holds only shared app chrome (sidebar, breadcrumbs, page-shell, kanban presentation, `ui/`). - `apps/saas/app/` pages stay sync shells that compose feature components inside Suspense; they contain no domain logic. - Panels use `@workspace/core` services and explicit subpaths from bounded contract packages. They never import provider implementations such as `@workspace/supabase-work` and should not copy direct database-access patterns from example apps. - Client components stay leaf-level and import their feature's Server Actions directly. Generic design components may still accept `action` or `*Action` callback props when they own reusable async coordination. Feature folders represent user-recognizable domains, not only aggregate roots. `tag` remains a standalone feature because Tags have dedicated list/create/detail routes, canonical `tag-queries.ts` / `tag-actions.ts` / `tag-cache.ts` contracts, and consumers across catalog, customer, product, and task features. Folding those contracts into one consumer would invert their ownership and make the other domains depend on an unrelated feature. ## DataGrid Rendering Profiles [#datagrid-rendering-profiles] Choose one profile before composing a list. The profile describes ownership and streaming behavior; it is not a set of cosmetic component flags. 1. **Canonical hub:** A standalone collection route. Its synchronous page owns the static `PageShell`, title, and Suspense boundary. The feature panel owns the `DataGrid` and uses `DataGridListFrame` for the standard toolbar, scroll area, table, pagination, empty state, and bulk-action composition. 2. **Embedded:** A grid inside a detail section, tab, or dashboard panel. The parent surface owns its heading, padding, and outer border. The grid may use `DataGridListFrame` for the same internal toolbar/table/pagination rhythm; configure `DataGridContainer` at the embedding boundary instead of adding visual-mode booleans to the frame. 3. **Server-driven:** A canonical hub or embedded grid whose sorting, filters, or pagination are backed by a server query. Keep that state stable across renders, set the authoritative row count on `DataGrid`, and pass the server-aware pagination control through `DataGridListFrame.pagination`. Server-driven is a data-ownership modifier, not a separate visual frame. 4. **Specialized:** A grid with materially different interaction or layout requirements, such as hierarchy, inline editing, grouped rows, a file browser, or a toolbar shared with another view. Compose the narrow ReUI `DataGrid` primitives directly and document the reason near the component. Do not grow `DataGridListFrame` with one-off booleans to absorb it. ### PPR instant-navigation contract [#ppr-instant-navigation-contract] - A hub route's `page.tsx` remains synchronous. It renders the static page shell immediately and places only the data/access feature panel behind a shape-matched Suspense fallback. - The final grid and fallback reserve the same title, toolbar, table, and pagination regions so streamed data does not shift the page. - URL-backed server state is read inside the feature island. Do not await `params`, `searchParams`, cookies, headers, or uncached data in the page shell merely to configure a grid. - Keep controlled grid state stable: disable automatic page-index resets, use stable pagination change handlers, and memoize column visibility so streaming and navigation do not create render loops or reset the user's place. - Embedded grids inherit the nearest useful parent boundary unless they fetch independently; independent grids use sibling boundaries. Specialized grids are not exempt from the same static-shell and shape-matched-fallback rules. ### Remembered list state [#remembered-list-state] List filters, sort, and rows-per-page are remembered per pathname in `localStorage` (`packages/ui/src/lib/list-state-storage.ts`). Nothing is stored in the database or in cookies. - The URL stays the source of truth. `useDataGridUrlState` and `useDataGridQueryFilters` remember automatically (opt out with `remember: false`). Feature nuqs surfaces call `useRememberedQueryStates` with a `*RememberedSearchParams` subset exported beside their `*-search-params.ts`. Grids with local React sort or filter state use `useRememberSorting` / `useRememberFilters` with a unique `id`. - Restoring runs in an effect after hydration and only when the URL has none of the remembered keys. Server, prerender, and prefetched output never read storage, so the static shell and Partial Prefetch cache stay valid. - Routes revealed from Next's hidden `` reconnect with stale state, so the "URL has a value" check reads `location.search`, not React state. An in-flight navigation can drop the restored shallow URL update; the hook re-applies it (bounded) until the keys appear in the URL and never saves the empty values in between. - Links and prefetch keep pointing at clean URLs. Do not rewrite hrefs from storage. Server-driven grids restore through their nuqs `startTransition` (`history: "replace"`), so cached rows stay painted with pending feedback until the filtered island arrives. - Search text (`q` / `search`), page number, column visibility, navigation keys (Files scope/drive/folder, analytics tab, agenda/planning date), and view choices are never remembered. Signing out clears every remembered entry. ## Caching [#caching] Use `"use cache: private"` for cookie-derived data when cacheable, and pair cached readers with matching `updateTag(...)` (Server Actions) or `revalidateTag(tag, { expire: 0 })` (Route Handlers) calls in mutations. App access and UI permission hints are loaded through one private cached `getCurrentAppAccess()` payload in the authenticated app layout, then reused by guards, the sidebar, the command palette, and feature-panel permission flag helpers. Private cache entries live only in browser memory. They are never stored on the server, do not survive a reload, and re-execute on every server render. For private scopes, only `cacheLife`'s `stale` value affects the client router window; `revalidate` and `expire` are inert until a read moves to a shared cache. The built-in presets `minutes`, `hours`, `days`, and `max` all use the same 5-minute `stale`, so switching among them does not lengthen private caches. Use three profiles: | Profile | `stale` | When | | --- | --- | --- | | `reference` | 15 minutes | Slow-changing tenant data whose every write path is a SaaS Server Action (customers, products, quotes, invoices, users, teams, roles, templates, knowledge, integrations, admin catalogs) | | `minutes` | 5 minutes | Frequent edits, or writers outside `apps/saas` that cannot invalidate the SaaS cache (tasks, agenda, files, workflows, billing, usage, inbox) | | `seconds` | 30 seconds | Chat threads, notification badges, unread inbox counts | `reference` is defined in `packages/next-config` (`cacheLife.reference`). Tenant list tags are `organizationId`-scoped builders (for example `organization-customers:{organizationId}`); numeric detail tags that can collide across tenants also carry the organization id. Never use public `"use cache"` for cookie-dependent data. ## Prefetch and request-time suspension [#prefetch-and-request-time-suspension] Prefer `await io()` from `next/cache` over `await connection()` when an island must exclude synchronous nondeterministic values (`new Date()`, `Math.random()`, …) from the static shell. `connection()` blocks prefetches; `io()` suspends like any other async boundary and stays compatible with `partialPrefetching` / `cachedNavigations`. Most feature data islands do not need either call when they only await cookie/header reads or async queries. Caveat: `requireOrganizationAccess()` / `getCurrentAppAccess()` run inside `"use cache: private"`, so cookies stay inside that cache scope. Islands that then call `new Date()` (agenda/planning when `?date=` is omitted) still need `await io()`. Keep `connection()` only when rendering must wait for a real user request (public token pages, invite accept, similar one-shot handlers). ### Island payloads [#island-payloads] Cached and prefetched overviews, hub tabs, and details must commit real content instead of a destination skeleton when the private cache already has the island. Skeletons remain correct on hard loads, `next dev`, and cold/uncached data. Copy `getProductsOverviewPayload` / `ProductsOverviewDataLoader`: 1. Add `getXPayload(orgSlug, …)` in that feature’s `*-queries.ts` (`"use cache: private"`, matching existing `cacheLife` / `cacheTag`s). Do not call `notFound()` or `redirect()` inside the cache. 2. Call `readOrganizationPayloadAccess` (or the portal/admin snapshot helpers) only from that cached function. Return `null` on miss or denied access. 3. Loader: `const payload = await getXPayload(…); if (!payload) { await require…(); notFound() }`. 4. Mark loaded UI with `data-instant-content="…"`, never the `isLoading` skeleton. 5. The happy path must not await `requireOrganizationAccess`, `requireCustomerPortalAccess`, `requireSystemAccessAdmin`, `getCurrentClaimsOrNull()`, or `io()`. Use `getAppShellAccess().authenticatedUserId` inside the payload instead of claims. Call `io()` only when the island itself needs request-time `Date`. Keep nested `get*` helpers so `updateTag` still busts the payload. Use one payload function per Suspense sibling, not one mega-payload for a whole detail page. Unbounded rows stay `IntentPrefetchLink`; after hover or a prior visit the island must be able to commit. Guest portal quote and invoice details follow the same contract without a membership gate: cache `getQuoteForPortal` / `getInvoiceForPortal` plus `getAppShellAccess`, return `null` on miss, then `notFound()`. Nested Stripe client-secret fetches stay outside that payload. ## Proxy Boundary [#proxy-boundary] The proxy enforces session routing and the locale and brand rewrites only. Keep proxy work minimal so streaming and Cache Components stay healthy. The brand step is a synchronous host lookup in a frozen `Map` plus string edits on the rewrite target, and the proxy reports it as `proxy-brand` in `Server-Timing` next to `proxy-i18n` and `proxy-session`. The `brand` root param joins every static shell and `"use cache"` key, so the build prerenders each shell once per locale and brand. Code inside `"use cache"` that renders brand-specific output must read the brand through `getBrand()` so the brand is part of the key. Shared tenant data needs no change, because organization IDs and slugs already scope it. Do not inject an organization slug header, and do not resolve organization membership there; tenant identity is URL `params.orgSlug` resolved inside Suspense islands. Locale rewrites still run on Link / Partial Prefetching requests (`next-router-prefetch`, `next-router-segment-prefetch`, `Purpose: prefetch`). Session refresh (`getClaims()` / cookie rotation) does not: refresh tokens are single-use, and a prefetch racing the click can invalidate the session. Unauthenticated redirects and admin prefix checks run on the real navigation. ### Not-found status codes [#not-found-status-codes] Unknown or non-member `orgSlug` values are rejected in the `[orgSlug]` layout via `OrganizationSlugAccessGate` / `requireOrganizationSlugAccess` in a **non-blocking** Suspense island beside `AppShell`, so cold tenant documents can paint chrome immediately. Feature permission failures stay in page Suspense islands: | Failure | Where it is decided | HTTP status | | --- | --- | --- | | Unknown or non-member `orgSlug` | `OrganizationSlugAccessGate` Suspense island in `[orgSlug]/layout.tsx` | 404 before the shell streams, otherwise 200 with the not-found UI | | Missing organization permission for an existing membership | `requireOrganizationAccess({ permission })` inside a Suspense island | 404 before the shell streams, otherwise 200 with the not-found UI | `notFound()` only produces an HTTP 404 while the response status is still open. Once `AppShell` has started streaming the status is committed, so in-island `notFound()` calls may render the not-found UI over HTTP 200. That remains an accepted Cache Components tradeoff for tenant membership and feature gates. Do not move those checks into the proxy. Page-level `notFound()` and unmatched URLs under a valid shell segment render that segment's `not-found.tsx` inside `AppShell`. Layout-level `notFound()` (unknown or non-member `orgSlug`) still uses the locale 404 and unmounts the shell. ## Instant Navigation triad [#instant-navigation-triad] Cache Components, Partial Prefetch, and View Transitions are one contract. Instant-shell references are Agenda (`agenda-hub`), Planning (`planning-hub`), and Files (`files-hub`). Do not copy public `'use cache'` from calendar demos; tenant reads stay `"use cache: private"`. ### Prefetch tiers [#prefetch-tiers] | Tier | When | What it warms | | --- | --- | --- | | Default `` | Ordinary same-origin navigation | One shared App Shell per route | | `IntentPrefetchLink` | Unbounded collections (mini-cal days, DataGrid rows, Files rail) | Shell first; upgrades to per-link content on pointer, keyboard focus, or `onTouchStart` | | `prefetch={true}` | Bounded, URL-keyed destinations we want resolved before click | URL (date/view/tab) plus the cached slice behind it; uncached data still streams | Keep `prefetch={true}` off every mini-calendar day and off unbounded DataGrid rows. The architecture-guard budget in `tests/integration/cross-package/saas/architecture-guards.test.ts` is the allowlist. Sidebar eager prefetch is the same rule: org Dashboard, Notifications, Inbox, Chats, Tasks, Agenda, Quotes, Invoices, Customers, and Products (10); portal Dashboard, Quotes, Invoices, and Assets; admin Dashboard. Planning, Files, payments, expenses, and collapsible children stay default Links so the sidebar RSC fan-out stays ≤ 16 (`apps/saas/tests/navigation-prefetch.test.ts`). Org-switcher rows use `IntentPrefetchLink`. Navigation-mode homes and breadcrumb parent hubs that match those eager destinations use `prefetch={true}`. ### Instant shells [#instant-shells] Hard navigation and Partial Prefetch should commit chrome before the data island. Share one sync shell between the page and `loading.tsx`: - **Agenda:** `AgendaHubShell` (left rail + mobile toolbar) wraps a Suspense island for `AgendaGridData`. Period title and the schedule use named View Transitions (`agenda-period-title`, `agenda-schedule`); the rail uses `default="none"`. Date/view hops are real Links (`?date=` / `?view=`) so prefetch can resolve the period. Toolbar prev/today/next and the view switcher use `prefetch={true}`; mini-cal days use `IntentPrefetchLink`. - **Planning:** `PlanningHubShell` (`data-instant-shell-marker="planning-hub"`) is the page/`loading.tsx` fallback. Cold loads paint `PlanningGridPlaceholder`; a warm private-cache hit commits `data-instant-content="planning-grid"` with no placeholder flash. Toolbar prev/today/next and the view switcher are real Links (`prefetch={true}`) like Agenda; mini-cal days use `IntentPrefetchLink`. Sidebar Planning stays off the eager prefetch set. - **Files:** `FilesHubShell` (rail + main column) is the page/`loading.tsx` fallback. Scope and folder hops use `IntentPrefetchLink`. Do not eager-prefetch every folder row. - **CRUD hubs:** Tab chrome already lives in `(tabs)/layout.tsx` outside page Suspense. Do not blanket-split toolbar vs grid. Add a nested Suspense only when a hub still flashes a full-frame skeleton. Mark instant chrome with `data-instant-shell-marker` and loaded islands with `data-instant-content`. ### View Transitions checklist [#view-transitions-checklist] - Named VT / `default="none"` on stable chrome (sidebar, agenda rail, Files rail, hub toolbars). - Crossfade or directional slide only the changing island (agenda schedule, Files browser). - List to detail: `IntentPrefetchLink` + `transitionTypes={["nav-forward"]}` and `text-morph` on titles (`SharedElementTransition`). - Detail to hub: `nav-back`. Wrap detail pages in `DirectionalPageTransition`. - No blanket page VT and no `SuspenseReveal` on every stream. ### Cache tags [#cache-tags] Pair private cache readers with `updateTag` (SaaS Actions) or `revalidateTag(tag, { expire: 0 })` (API writers). Agenda tags the org (`organization-agenda:{id}`), the fetched range, and the UTC Monday week (`organization-agenda-week:{id}:{yyyy-mm-dd}`). Mutations `updateTag` the org plus affected week(s), meaning source and destination on move when both instants are known. # Internationalization (/docs/architecture/i18n) next-intl extraction, locale routing, gettext catalogs, and translation rules. The SaaS app uses `next-intl` with extracted messages and gettext `.po` catalogs. ## Source Locale [#source-locale] English is the source language. UI code writes English messages through extraction APIs and translators fill non-English catalogs. ## APIs [#apis] Use only: - `useExtracted` in synchronous components. - `getExtracted` in async server functions. Do not use `useTranslations` or `getTranslations` in this codebase. ## Catalogs [#catalogs] Translations live under `apps/saas/translations/`. Extraction is enabled in `apps/saas/next.config.ts` via `experimental.extract: true` with `messages.sourceLocale: "en"`. Committed `.po` location comments use path-only references (no line numbers), matching next-intl's default PO codec. Empty `msgstr` values render as blank copy at runtime. Keep Dutch and German translations complete and run catalog tests after extraction. ## Locale Routing [#locale-routing] SaaS uses `localePrefix: "never"`: locale never appears in the URL. Preference is stored in the `profile-locale` cookie; the proxy rewrites onto the internal `app/[locale]/[brand]/...` segments so `next/root-params` can resolve the locale and the brand. The brand comes from the request host, and the proxy inserts it after the locale in the next-intl rewrite with string operations only. Showcase uses the same prefix mode. Locale codes (`en`, `nl`, `de`) are reserved and cannot be organization slugs. Unknown or invalid locale values resolve to the default English locale instead of returning 404. Locale switchers should use native names such as English, Nederlands and Deutsch. ### Root params and Cache Components [#root-params-and-cache-components] CentraKit follows the [next-intl Partial Prefetching e2e sample](https://github.com/amannn/next-intl/tree/main/e2e/partial-prefetching/src) and the [next/root-params blog guidance](https://next-intl.dev/blog/nextjs-root-params) for Cache Components, not the classic setup docs that still recommend `setRequestLocale` for static rendering. There is deliberately **no** pass-through `app/layout.tsx` or `app/[locale]/layout.tsx`. The document root is `app/[locale]/[brand]/layout.tsx` so `locale` and `brand` stay root params. That layout enumerates every locale and brand pair with `generateStaticParams`, so each static shell is prerendered per brand, resolves `lang` via `await getLocale()`, and mounts `NextIntlClientProvider`. It does **not** call `setRequestLocale` or validate `params.locale`; locale resolution belongs in `getRequestConfig`. `getRequestConfig` in `apps/saas/lib/i18n/request.ts` mirrors the sample shape (`import * as rootParams`, optional `locale` override, then `rootParams.locale()`). Outside a route context (Server Actions / Route Handlers), `next/root-params` is unavailable; catch the error, call `unstable_rethrow(error)` first so Next.js prerender and control-flow sentinels are preserved, then fall back to next-intl's `requestLocale` (proxy-supplied header). Do not swallow hanging fallback-param signals with a broad catch, because that converts them into header reads and blocks App Shells / Partial Prefetching. Invalid locale values still resolve to `defaultLocale` rather than `notFound()`. The brand follows the same rule: `getBrand()` in `apps/saas/lib/brand/current-brand.ts` reads `rootParams.brand()` and, after `unstable_rethrow(error)`, falls back to the request host in Server Actions and Route Handlers. Do not register `orgSlug` in `next/root-params`. Tenant slug stays URL data resolved inside Suspense islands. ### Global not-found [#global-not-found] `experimental.globalNotFound` is enabled in shared Next config. `apps/saas/app/global-not-found.tsx` handles unmatched URLs that never enter the `[locale]/[brand]` tree (for example paths the i18n proxy does not rewrite). It returns a full document via `AppDocument`, renders the default brand because there is no brand root param outside the tree, supplies the default English catalog to `NextIntlClientProvider`, and reuses the same `NotFoundContent` as `app/[locale]/[brand]/not-found.tsx`. In-app misses keep `AppShell`. Each shell segment (`[orgSlug]`, `admin`, `organizations`, `profile`, `feedback`) has its own `not-found.tsx` and a `[...rest]` catch-all so unmatched URLs stay inside the sidebar. `app/[locale]/[brand]/not-found.tsx` remains the fallback for requests that never enter a shell segment and for `notFound()` thrown from a shell layout itself (unknown org slug). ### Sitemaps [#sitemaps] The public SEO sitemap lives on the docs app (`/docs/sitemap.xml`), not on SaaS. Marketing and showcase are no longer deployed. SaaS is an authenticated product surface with `localePrefix: "never"` (locale in the `profile-locale` cookie, one URL per route), so a GrahamQuan-style multi-locale sitemap with `hreflang` alternates does not apply. ## Message IDs [#message-ids] Reuse message IDs when meaning stays the same. Create a new ID when meaning changes so translators get a clear signal. ## Translator descriptions [#translator-descriptions] Every `t({ id, message, description })` call must include a concrete `description`. That text becomes the `#.` comment in the `.po` catalog and is what AI/human translators use for context. Good descriptions name: 1. Which screen or feature the string belongs to 2. What UI role it plays (button, column header, placeholder, toast, aria-label, watermark) 3. Disambiguation for short or overloaded English words Examples: - `Clear` on an invoice date field: "Button that clears the picked invoice date; not a filter reset" - `tag` in the invoice editor: "Inserts a merge token placeholder; not the Tags feature" - `unit` on a line item: "Unit of measure suffix, for example hour or piece" - `Concept` on a draft invoice: "Draft watermark standing in for the invoice number" Avoid one-word descriptions such as `label` or `col`. ## Dutch terminology [#dutch-terminology] Prefer natural SaaS Dutch over literal calques. Keep these glossary terms stable: | English | Dutch | | --------------------- | ---------------------- | | Resources (nav hub) | Middelen | | Access | Toegang | | Roles & Permissions | Rollen & Rechten | | Customers | Klanten | | Contacts | Contactpersonen | | Tags | Tags | | Templates | Template(s) | | Knowledge | Kennis | | Customer assets | Assets | | Workflow | Workflow | | Search / Clear search | Zoeken / Zoeken wissen | | Notes (free-form) | Notities | | Location (address) | Locatie | | Size (file) | Grootte | After extraction, fill every blank `msgstr` in `nl.po` and `de.po`. Empty strings render as blank UI. German uses the formal address Sie. Dutch stays informal. Run `tests/integration/saas/i18n-catalog.test.ts` before handoff. # Monorepo (/docs/architecture/monorepo) CentraKit Turborepo app and package topology. CentraKit is a Turborepo monorepo using pnpm workspaces. Internal packages use the `@workspace/*` alias. ## Apps [#apps] | App | Framework | Purpose | | --- | --- | --- | | `apps/saas` | Next.js | Auth, dashboard, customer UI, organization UI, and admin UI. | | `apps/api` | Next.js | REST API and MCP server. | | `apps/docs` | Fumadocs on Next.js | Public documentation site generated from root `docs/`. | | `apps/marketing` | Next.js | Marketing website. | | `apps/showcase` | Next.js | Component and product UI showcase. | | `apps/cli` | Node.js | `centrakit` CLI. | ## Packages [#packages] ### Bounded contracts [#bounded-contracts] | Package | Purpose | | --- | --- | | `@workspace/foundation` | List/API primitives, validation, env contracts, opaque ID helpers. | | `@workspace/identity` | Auth, users, and RBAC contracts. | | `@workspace/tenant` | Organizations, teams, and tags. | | `@workspace/crm` | Customers and collaboration contracts. | | `@workspace/catalog` | Products, inventory, customer assets, and files. | | `@workspace/work` | Tasks and agenda. | | `@workspace/people` | Employees, HR settings, schedules, time, time off, approvals, equipment, onboarding, hiring and people insights. | | `@workspace/commerce` | Quotes, invoices, orders, shipments, promotions, purchase orders, expenses, templates, and document primitives. | | `@workspace/conversations` | AI settings, chat, channels/inbox, agent runs, knowledge, and memory. | | `@workspace/automation` | Workflows and integrations. | | `@workspace/platform` | Billing/usage, audit log, notifications, analytics, settings, and the command center contract. | | `@workspace/portal` | Customer-portal aggregate read models. | ### Wiring and providers [#wiring-and-providers] | Package | Purpose | | --- | --- | | `@workspace/core` | Request context, auth, RBAC, and audit helpers (no all-services bag). | | `@workspace/core-*` | Domain-scoped request factories (`core-tenant`, `core-crm`, `core-catalog`, `core-work`, `core-people`, `core-commerce`, `core-conversations`, `core-automation`, `core-platform`, `core-portal`). | | `@workspace/core-ai-runtime` | Server runtime for assistant chat and specialist streams, AI approval gates, and the agent chat workflows. | | `@workspace/core-workflow-runtime` | Workflow graph dispatcher, scheduled runs, lifecycle nodes, and the commerce and order event reactors. | | `@workspace/core-command-center` | Command center record search and commands, composed from several `core-*` factories. | | `@workspace/supabase-runtime` | Clients, generated DB types, shared provider primitives. | | `@workspace/supabase-*` | Bounded Supabase implementations aligned to contract packages, including `supabase-people`. | ### Feature and integration packages [#feature-and-integration-packages] | Package | Purpose | | --- | --- | | `@workspace/ai` | AI Gateway models, assistant tools, prompts, knowledge retrieval, memory, and AI suggestions. | | `@workspace/inbox` | Chat SDK channel adapters, inbound channel webhook processing, and the inbox agent tools. | | `@workspace/email` | Resend client, inbound email parsing, and the React Email templates. | | `@workspace/stripe` | Stripe client, billing and usage providers, invoice payments, and webhook verification. | | `@workspace/seo` | Brand defaults, metadata helpers, icons, and JSON-LD components. | | `@workspace/product-pdf` | Product page and product label PDFs with QR codes. | | `@workspace/quote-pdf` | Quote, invoice, and order document PDFs and their fonts. | | `@workspace/business-registry` | KVK and VIES business lookups, in `packages/integrations/business-registry`. | | `@workspace/google-calendar` | Read-only Google Calendar API client and schemas, in `packages/integrations/google-calendar`. | `packages/integrations/` is a folder of packages, not a package itself. `pnpm-workspace.yaml` lists it separately as `packages/integrations/*`. ### Shared app packages [#shared-app-packages] | Package | Purpose | | --- | --- | | `@workspace/ui` | Shared React components and Tailwind CSS 4 design tokens. | | `@workspace/api-client` | OpenAPI-generated typed API client. | | `@workspace/next-config` | Shared Next.js config factory. | | `@workspace/typescript-config` | Shared TypeScript configuration. | Lint and format presets come from Ultracite 8 (`ultracite/oxlint/*` and `ultracite/oxfmt`). The root `oxlint.config.ts` and `oxfmt.config.ts` add the house rules. See ADR 0013. ## Import Boundaries [#import-boundaries] ```text apps/* -> @workspace/core, @workspace/core-*, bounded contracts, @workspace/ui, @workspace/api-client, shared config apps/* -> never @workspace/supabase-* or another app packages/core / core-* -> may import contracts and matching @workspace/supabase-* packages/supabase-* -> may import their contracts + supabase-runtime only; never apps/core/other providers contract packages -> zod/better-result and lower contracts only; never core or providers ``` `pnpm boundaries` enforces these rules through the `tags` in each workspace `turbo.json` and the `boundaries` block in the root `turbo.json`: | Tag | Packages | May depend on | | --- | --- | --- | | `contract` | The bounded contract packages, including `people`. | `contract`, `config` | | `integration` | `email`, `stripe`, `seo`, `product-pdf`, `quote-pdf`, `api-client`, `business-registry`, `google-calendar` | `contract`, `config` | | `provider` | `supabase-runtime` and every `supabase-*` package. | `contract`, `config`, `provider` | | `core` | `core`, every `core-*` package (including `core-ai-runtime`, `core-workflow-runtime` and `core-command-center`), `ai` and `inbox`. | `core`, `contract`, `provider`, `integration`, `config` | | `ui` | `ui` | `config` and `@workspace/foundation` | | `app` | Everything under `apps/`. | `core`, `contract`, `provider`, `integration`, `ui`, `config` and `test` | The `app` tag allows `provider` because Turbo applies boundary rules to dependencies of dependencies: every app reaches `@workspace/supabase-*` through `@workspace/core-*`, so denying `provider` would fail every app. The `appImportPatterns` rule in the root `oxlint.config.ts` blocks a direct `@workspace/supabase-*` import from app code, and no app lists one in its dependencies (ADR 0002). Contract packages expose explicit subpaths (`./schemas`, `./service`, `./errors`, domain roots). They do not expose a runtime `.` barrel that pulls Zod into client graphs. Keep the contract graph acyclic. `identity` and `tenant` build on `foundation`. CRM depends on identity and tenant. Catalog and work depend on CRM and tenant. Commerce depends on CRM and work. Portal depends on catalog and commerce. People depends only on foundation. Conversations depends on identity and tenant. Automation depends on catalog, commerce, conversations and CRM. Platform depends on tenant and must not import conversations; orchestration joins those packages in `core-*`. ## Payload Baselines [#payload-baselines] Use package-owned `baseline:payload` scripts, which the CI `Build` job runs after the build, to capture SaaS and CLI size artifacts. The bundle analyzer runs only on a manual workflow run. See [CI And Release](/docs/operations/ci-and-release). # Naming (/docs/architecture/naming) Naming rules for code in the CentraKit monorepo, and the renames that brought the code in line with them. These rules apply to every app and package. Database names follow [Supabase schema](/docs/architecture/supabase-schema). ## Rules [#rules] - **One name per concept.** A concept keeps the same name in the database, the contract, the API, the MCP tools and the UI. Near-duplicate names for the same thing are drift. - **`define*` and `create*`.** A `define*` function returns a declarative value, such as a Zod schema built from arguments. A `create*` function returns a runtime instance, such as a service or a request context. - **Options types** are named `Options` without a verb, such as `RequestContextOptions`, not `CreateRequestContextOptions`. - **Acronyms are written as words:** `ApiPage`, `OpenApi`, `McpServer`, `ChatUiMessage`. Third-party names keep their own spelling, such as `OpenAPIPageData` from Fumadocs. - **Files** are kebab-case. Feature folders are singular (`customer`), and API resources and routes are plural (`/customers`). - **MCP tools** are snake case and follow the OpenAPI operation ID, so `listTasks` becomes the tool `list_tasks`. - **Dates and times** use `Temporal` from `@workspace/foundation/temporal` in contracts and services. `pnpm docs:drift` fails when a doc page still uses a retired name. It skips this page and the decision records, which keep the old names on purpose. ## Renames [#renames] | Old name | New name | | --- | --- | | `mcp-handler` | `createMcpHandler` from `@modelcontextprotocol/server` | | MCP tool `listTasks` (every tool) | `list_tasks` (snake case of the operation ID) | | `framer-motion` | `motion/react` | | `date-fns` and `@date-fns/tz` | `Temporal` from `@workspace/foundation/temporal` | | `AISearch*`, `useAISearchOpen` | `AiSearch*`, `useAiSearchOpen` | | `APIPage` | `ApiPage` | | `WebMCP` component | `WebMcp` | | `renderOpenAPIOperations` | `renderOpenApiOperations` | | `ChatUIMessage*` | `ChatUiMessage*` | | Docs `ChatUIMessage` | `DocsChatMessage` | | `TeamSwitcher` | `OrganizationSwitcher` | | `createListResponseSchema` | `defineListResponseSchema` | | `createPublicApiListResponseSchema` | `definePublicApiListResponseSchema` | | `createOpaqueIdSchema` | `defineOpaqueIdSchema` | | `createCustomerLocationNameSchema` | `defineCustomerLocationNameSchema` | | `CreateRequestContextOptions` | `RequestContextOptions` | | `CreateRequestContextFromSupabaseOptions` | `RequestContextFromSupabaseOptions` | | `CreateActingUserRequestContextOptions` | `ActingUserRequestContextOptions` | | `CreateAdminContextOptions` | `AdminContextOptions` | | `CreateApiClientOptions` | `ApiClientOptions` | | `CreateEmailServiceOptions` | `EmailServiceOptions` | | `CreateNextConfigOptions` | `NextConfigOptions` | # RBAC (/docs/architecture/rbac) PermDock permissions and roles, the token claims, and how pages, actions, routes and RLS decide access. CentraKit decides access with [PermDock](https://permdock.dev). One policy declares the permissions and roles, the database helpers and the token hook are generated from it, and the server decides in process from the verified session claims. ## Source Of Truth [#source-of-truth] The policy lives in `packages/identity/src/permdock/`: - `permissions.ts` declares every resource and action with its title and description, for example `customer.read`, `quote.send` and `platform.user.manage`. - `roles.ts` declares the built-in roles. - `policy.ts` says which role allows which permissions and on which scope. The scopes are `organization` and `customer` (inside an organization); platform roles are global. `pnpm --filter @workspace/identity permdock:generate` writes the SQL helpers (`supabase/schemas/identity/permdock/helpers.sql`) and the token hook (`supabase/schemas/identity/permdock/hook.sql`), and a new `_permdock_seeds.sql` migration whenever the seeded rows change. The helper and hook files keep their grants inline, and CentraKit's own `service_role` grants on the PermDock functions live in `supabase/schemas/identity/permdock/grants.sql`; pg-delta carries all of them into migrations. `permdock:check` fails when the files drift from the policy. ## Custom Roles [#custom-roles] Custom roles follow PermDock's custom role model. `public.roles` keeps each role's identity: its key, name, description and owning organization, and `roles_custom_key_not_builtin_check` keeps custom keys apart from the built-in ones. What a role grants lives in PermDock's tables: the seeded `permdock.role_permissions` for built-in roles and `permdock.custom_role_permissions` for custom roles, keyed by the role key. - An organization custom role belongs to one organization (`scope = 'organization'`). A platform custom role has no organization (`scope = 'system'`) and is stored with `scope = 'global'`. - The ceiling of a custom role is the set of permissions the assignable built-in roles of its scope allow: Admin, Member and Viewer in an organization, Platform Support for a platform role. Owner-only permissions such as `billing.manage` never fit in an organization custom role. - The role editor offers `permdock.assignablePermissions()`: the ceiling, narrowed to what the editor holds in that organization. A platform admin holds `platform.role.manage`, which lifts that narrowing. - Saving runs `validateCustomRole` and refuses every key outside the assignable set, then calls `public.replace_role_permissions(role_id, permissions)`. A signed-in caller goes through PermDock's generated `permdock.permdock_replace_custom_role_grants` for organization and platform roles alike: it checks that the caller holds `role.update` in the organization or `platform.role.manage` (`rls.customRoleWrites.requires`), membership, the ceiling, and that the caller may hand out every key the new and the stored definition allow. A `service_role` caller goes through `permdock_trusted_replace_custom_role_grants`, which keeps the definition checks. The function then writes the audit event. - A refusal carries PermDock's reason as the error hint (`outside-ceiling`, `not-assignable-by`, `not-member`, `manage-roles`, `declared-role`, `role-exists`); `customRoleRefusal` in `@workspace/identity/rbac/service` maps it to the role editor's messages. - Renaming, moving or deleting a row of `public.roles` moves or removes its PermDock rows through the generated `permdock_custom_role_cascade` trigger (`rls.customRoleWrites.roles`). A signed-in caller passes the same checks as the write functions; migrations, jobs and nested triggers move the rows directly. `RbacService.listRolePermissions(roleIds)` reads the keys a visible role grants, and `RbacService.replaceRolePermissions(roleId, permissions)` saves them. The legacy `public.permissions`, `public.role_permissions` and `organization_permission_overrides` tables are gone. ## Roles [#roles] | Scope | Role | Purpose | | --- | --- | --- | | global | `platform-admin` | Every `platform.*` permission, and nothing in an organization | | global | `platform-support` | Support operations with read-only billing | | `organization` | `owner` | Final authority; exactly one per organization, changed only through a transfer | | `organization` | `admin` | Members, roles, settings and integrations | | `organization` | `member` | Day-to-day operational access | | `organization` | `viewer` | Read-only | | `customer` | `customer` | Portal contacts: published quotes, invoices, orders and assets of one customer | Organizations define custom roles from the assignable built-in roles, as described above. A platform admin reaches an organization's data only as a member of it. Admin features read organization data through explicit `platform.*` branches in RLS, such as `platform.organization.read` and `platform.role.manage`. The owner role is `transferOnly`: the organizations block's `transfer_ownership` promotes the new owner and demotes the old one in one statement, and PermDock's triggers refuse any other change to the number of owners. The organizations block's deferred `bs_organization_owner` trigger refuses a commit that leaves an organization without an owner, such as its only owner leaving. ### Assigning Roles [#assigning-roles] A member's role changes through `decideRoleChange`, which applies each built-in role's `assigns`, `transferOnly` and `min` rules: an owner assigns Admin, Member and Viewer, an admin assigns Admin, Member and Viewer, Platform Admin and Platform Support assign Admin, Member and Viewer in any organization, Platform Admin also assigns Platform Admin and Platform Support, and the owner role only moves through the ownership transfer. Custom roles go through the same `decideRoleChange` and `assignableRoles()`: `permDockWithOrganizationRoles` from `@workspace/core/auth/custom-roles` derives the caller's PermDock with every custom role of the organization (`permdock.derive`), and PermDock offers a custom role when the editor may assign a built-in role there and may hand out every permission the custom role allows. Role pickers list the same set. The database repeats the check. PermDock's generated `permdock_assignment` trigger (`rls.assignments`) refuses a client write to `organization_users` that assigns, changes or removes a role the caller may not assign there, by the `assigns` graph or, for a custom role, by what the caller may hand out. Writes from security definer functions, such as accepting an invitation, are trusted by that trigger. Clients cannot write `organization_invitations` at all; the invitations block checks the role ceiling itself. Members are created, changed and removed through the organizations block (`api.create_organization`, `api.update_member_role`, `api.remove_member`), whose `bs_organization_role_guard` trigger checks `can_assign` for the old and the new role and refuses a change to the caller's own role inside those functions too; the platform admin tool changes roles on `organization_users` directly. `api.create_organization` makes the caller the owner and seeds the starter templates through the `after_organization_create` hook. PermDock's assignment trigger also guards `user_roles`: a client may write only global roles it may assign and never its own rows (`ownRole`), and better-supabase's `bs_role_scope` triggers keep organization roles in `organization_users` and system roles in `user_roles` for every writer, including the service role. Accepting an invitation requires that the inviter may still assign its role (`permdock_can_assign_for`, or `permdock_can_assign_custom_role_for` for a custom role). The admin user tool (`platform.user.manage`) lists every organization's built-in roles except Owner, plus that organization's custom roles. It never assigns or removes Owner; the row of an owner is read-only and points to Transfer ownership, which only the owner can start. ## Token Claims [#token-claims] `permdock.custom_access_token_hook` writes: - `user_role` and `roles`: the global roles from `user_roles`. - `memberships`: one entry per organization, `{ "scope": "organization", "id": "", "roles": ["owner"], "via": "staff" }`. The claim stays within PermDock's byte budget, and `memberships_truncated` is set when entries were left out. - No `tenant_id`. The organization comes from the URL, and PermDock's database helpers narrow `permitted__ids` to the `tenant_id` claim when a token carries one, so the hook config leaves out `activeFrom` and RLS admits every organization in which the user holds the permission. Nothing may write `app_metadata.active_organization`, the hook's default source for the claim. - `authz_ver`: the user's authorization version, bumped on every membership or role change. - `datetime_preferences`: the display preferences from `profiles`. Suspended users and organizations get no memberships. `supabase/tests/database/claims.test.sql` checks the shape and the size for the heaviest seeded user. ## Enforcement Layers [#enforcement-layers] ### In Process [#in-process] `RequestContext.getPermDock()` builds one PermDock per request from the verified claims, with no database read on the usual path. When the token was truncated, the memberships are read from the database (`claimsFirst` over `postgrestSources`), and custom roles are loaded only when a membership names one. Both come from the generated `permdock.subject_for(p_user)`, which the server reads through `public.permdock_subject_for` with the service role; a job or an approval acting for a stored user builds its PermDock from the same function (`createPermDockForUser`). - `requireAccess({ tenant, permission })` guards pages and layouts. The tenant is the route's `orgSlug`, resolved through the shared public slug lookup. A signed-out request goes to `/login`, and an unknown slug or a missing permission renders not found. - `withTenantAccess(permission, handler)` and `withPlatformAccess(permission, handler)` wrap Server Actions and route handlers and return a `Result` with `AccessDeniedError` on a denial. `decideAccess` answers one check. - An organization check without a row answers from organization memberships only, so a portal contact fails every organization-level check. `checkAccess` reports `not-member` instead of `denied` when the user holds no staff membership in the organization, which pages render as not found. ### Database [#database] RLS policies call the generated helpers in the init-plan form, `organization_id in (select permdock.permitted_organization_ids(''))` and `(select permdock.permdock_has(''))`, so each statement evaluates them once. A permission split by condition is named by permission key (`permitted_organization_ids_by_permission`). Trusted SQL that checks a stored user, such as a calendar feed, a notification recipient or an agent run, calls the generated `_for(p_user, ...)` forms, which `service_role` may execute. ### Relationships [#relationships] The `thread` resource (`chat_threads`) declares two PermDock relations: `owner` (the `owner_user_id` field) and `participant` (the `kind = 'user'` rows of `chat_thread_participants`). Two grants give `thread.read` on a thread to its owner and to its user participants, and `permdock:generate` writes `permdock.permitted_thread_ids(relation)`. No role holds `thread.read`, so `chat.read` keeps only role grants and stays usable in the Realtime topic policies, which PermDock refuses for a permission with relation grants (PD037). The `chat_threads` select policy admits the owner, the ids from `permitted_thread_ids('participant')`, and organization threads in `permitted_organization_ids('chat.read')`; the policies on messages, participants and feedback read through `chat_threads`, so its policy decides them all. `supabase/tests/database/chat_access.test.sql` checks the policy against that rule for every seeded user and thread. File drives and nodes, calendars, knowledge collections and workflows share the same model. Each has its own resource (`fileDrive`, `fileNode`, `agendaCalendar`, `knowledgeCollection`, `workflowDefinition`) whose grant table is a PermDock edge: a row names a user in `user_id` or a team in `team_id`, and the team group reads its members from `team_members` through the `team` resource's `member` relation. File and calendar shares use one edge per level with `match: { role }` and `includes`, so an editor is also a commenter and a viewer. File nodes walk their `parent_id` chain through `permdock.permdock_closure`, so a share on a folder reaches everything below it, up to 32 levels. Every share grant carries `requires` with the feature permission (`file.read`, `agenda.read`, `knowledge.read`, `workflow.read`), so it counts only while the member still holds that permission in the organization; the organization-wide arms (the company drive, public calendars, managers with `agenda.delete`, `knowledge.update` or `workflow.update`) are `authenticated()` grants with `requires`. `rls.rowHelpers` generates `permdock.permitted__rows(permission)`, and the policies and trusted functions ask `id::text in (select permdock.permitted_file_drive_rows('fileDrive.update'))` instead of restating the graph. A level that needs several permissions lists them all in `requires` (a calendar editor needs `agenda.read` and `agenda.update`, the customers drive `file.read` and `customer.read`), and a drive level includes its read rule, so `fileDrive.update` alone answers whether a drive is readable and editable. A file node inherits each level from its drive with `inherit(permissions.fileDrive.read, { through: ["drive"] })` (and the comment and update levels), so `permitted_file_node_rows` alone decides a node. Policies still AND the operation permission (`file.update`, `agenda.update`) for writes, because an API key passes a grant's key check with any of its `requires` keys. The `agenda_collections`, `knowledge_collections` and `workflow_definitions` select policies call the row-valued helper (`permdock.permitted_knowledge_collection_row(knowledge_collections, 'knowledgeCollection.read')`), which decides from the row's columns, so `insert ... returning` admits the row the statement creates. API keys get no scopes for permissions that only `requires` grants reach (`fileDrive`, `fileNode`, `agendaCalendar`); the key's feature scopes stand in for them. No role holds these permissions, so the feature permissions keep only role grants. `public.can_edit_file_node` answers the file inspector, and `supabase/tests/database/resource_access.test.sql` compares the policies and helpers with the previous rules for every seeded user, row and level, with shares through teams, nested folders and a trashed drive. ### UI Hints [#ui-hints] The client reads PermDock snapshots, built in `"use cache: private"` functions in `apps/saas/lib/organizations/access-snapshot.ts` from the request's PermDock (`permdock.snapshot()`), cached for `cacheLifeFor(snapshot)` and tagged with `permdock:` and the role tags, so a role or membership change drops them with `updateTag`. - The app shell loads one snapshot with every membership (`tenants: 'all'`) and renders the sidebar and command center under `PermDockProvider` from `permdock/react`. Navigation hints ask `checkAccess` against the derived organization instance. - The `[orgSlug]` layout passes `params.then(...)` into `OrganizationPermissionProvider`, which hands the organization's snapshot promise to `PermDockProvider` without awaiting it. Client components read it with `useTenantPermission(permissions.task.update)` or ``. - The provider runs with `endpoint={false}`: the policy has no closure grants, and its graph grants (`thread.read` for chat owners and participants) are server-only in the snapshot, which no hint needs because hints never ask about one thread. Every hint is answered from the snapshot. An organization-level check without a row asks PermDock with `scope: "organization"`, so only staff memberships answer it. Hints are decorative; writes go through a guarded action, route handler or RLS policy. ## Invitations [#invitations] Invitations live in `public.organization_invitations`, separate from `organization_users` because pending invitees may not have an auth user yet. Rows without an organization are platform invitations, which assign a system role in `user_roles`. The better-supabase invitations block owns the writes: `invite_member` creates an invitation and returns its token once, and the table keeps only the token's SHA-256 hash. `resend_invitation` issues a new token, `revoke_invitation` sets `revoked_at`, and `accept_invitation` (by token, from the email link) or `accept_invitation_by_id` (from the notification inbox) creates the membership for the signed-in user whose confirmed email the invitation names. Accepting checks the inviter again: an inviter who lost `member.invite` or may no longer assign the role, including a custom role, can no longer bring someone in. `update_invitation` changes an open invitation's email, role or prefill with the same checks as `invite_member` and keeps its token. `my_invitations` lists the caller's open invitations with their organization, and `decline_invitation_by_id` declines one. The accept page reads `invitation_preview`. Both carry the role's key and name and the organization's brand images from CentraKit's `public.invitation_preview_extra` hook, which no client may call. An organization invitation resolves its role only among organization roles (`roleThrough.where`), and a platform invitation only among system roles (`platformRoles.through.where`). ## Tests [#tests] - `supabase/tests/database/permdock.test.sql` holds the role matrix across the schema folders, custom roles and their ceiling, suspension, D10 and the ownership transfer. - `supabase/tests/database/organizations.test.sql` covers creating an organization, the slug rules, role changes and removals through the organizations block, platform updates and the owner invariant. - `supabase/tests/database/chat_access.test.sql` compares the chat thread and message policies with the owner, participant and organization rule for every seeded user and thread, and `packages/identity/tests/permdock-thread-relations.test.ts` checks `can(thread.read)` against the same owner and participant rows. - `supabase/tests/database/resource_access.test.sql` compares file, calendar, knowledge and workflow access with the previous `has_*_access` rules for every seeded user and row. - `supabase/tests/database/invitations.test.sql` covers organization, platform and custom role invitations, the inviter re-check, the inbox functions, the ownership transfer and an only owner's account deletion. - `packages/identity/tests/permdock-custom-roles.test.ts` covers the custom role ceiling, role assignment and the pickers, and `apps/saas/tests/permission-gates.test.tsx` covers the client snapshot. - `pnpm --filter @workspace/identity permdock:verify` runs the policy fixtures against the database. - `tests/integration/supabase/permdock-claims-live.test.ts` signs in the seeded users and checks the token shape, its size and the in-process decisions. # Service Layers (/docs/architecture/service-layers) Data contracts, Supabase implementations, request containers, and Result-based domain flows. Domain logic follows a three-layer pattern that decouples apps from infrastructure providers. ## Layers [#layers] ```text bounded contract packages -> interfaces, schemas, errors, read models packages/supabase-* -> Supabase-backed implementations packages/core + core-* -> request context and domain-scoped factories ``` ## Bounded Contract Packages [#bounded-contract-packages] The source/JIT packages `foundation`, `identity`, `tenant`, `crm`, `catalog`, `work`, `people`, `commerce`, `conversations`, `automation`, `platform`, and `portal` define TypeScript interfaces, Zod schemas, and domain error types. They may depend only on lower contract packages and infrastructure-neutral libraries. Use one shared schema per domain when practical instead of creating duplicate DTOs. Prefer purpose-built list/picker/board/summary/detail read models when callers materially differ. ## `packages/supabase-*` [#packagessupabase-] `packages/supabase-runtime` owns clients, generated database types, and shared provider primitives. The aligned bounded packages (`supabase-identity`, `supabase-tenant`, and so on) implement contract interfaces without importing other providers. Core injects cross-domain dependencies and does not expose Supabase details to apps. ## `packages/core` and `packages/core-*` [#packagescore-and-packagescore-] Create a typed `RequestContext` with `createRequestContext({ bearerToken? })`. Cookie and bearer paths create context only; they must not instantiate every domain service. Each feature or route imports a direct factory such as `createTasksService(context)` from `@workspace/core-work/tasks`. When a use case spans domains, compose the two or three explicit factories in that orchestration module. Do not hide them behind string keys, proxies, or an all-services container. The domain-scoped packages are `core-tenant`, `core-crm`, `core-catalog`, `core-work`, `core-people`, `core-commerce`, `core-conversations`, `core-automation`, `core-platform`, and `core-portal`. Three more `core-*` packages hold cross-domain runtimes rather than one domain's factories: - `@workspace/core-ai-runtime` runs assistant chat and specialist streams for the AI routes in `apps/api/app/api/v1/ai/`, including the AI approval gate and the agent chat workflows. - `@workspace/core-workflow-runtime` holds the workflow dispatcher (`dispatchWorkflowEvent`), the scheduled-run and schedule-sync job handlers, lifecycle nodes, and the commerce and order event reactors. See [Workflows](/docs/operations/workflows). - `@workspace/core-command-center` composes the tenant, CRM, platform, portal, and automation factories into command center search and commands. ## Feature and integration packages [#feature-and-integration-packages] Some packages sit beside the three layers instead of inside one of them: - `@workspace/ai` and `@workspace/inbox` carry the `core` boundary tag. `ai` owns AI Gateway models, assistant tools, prompts, knowledge retrieval, and memory. `inbox` owns the Chat SDK channel adapters and the processing of inbound channel webhooks. - `@workspace/email`, `@workspace/stripe`, `@workspace/seo`, `@workspace/product-pdf`, `@workspace/quote-pdf`, and the packages under `packages/integrations/` carry the `integration` tag. They wrap one external service or renderer, may import only contracts and config, and are composed by `core-*` packages or apps. ## Result Model [#result-model] Async service methods return `Promise>` using `better-result`. Domain exceptions should become typed error results rather than thrown exceptions. Use `Result.codec` only at action/API/AI wire boundaries. ## Provider Swapping [#provider-swapping] To replace Supabase, create a new provider package that implements the same bounded contract interfaces, then change the factories wired by the matching `@workspace/core-*` package. # Supabase Schema (/docs/architecture/supabase-schema) Declarative schema workflow, generated migrations, RLS placement, seeds, and generated types. CentraKit uses Supabase declarative schema management. ## Source Of Truth [#source-of-truth] `supabase/schemas/` is the source of truth for database structure. Do not create or write migration files directly during normal schema work. ## Workflow [#workflow] 1. Edit the desired final state in `supabase/schemas/**/*.sql`. 2. Generate a migration with `pnpm supabase:diff -f `, which runs pg-delta (`supabase db schema declarative sync --no-apply`). 3. Rows the database needs, such as built-in roles or currencies, go in a separate data migration, because pg-delta refuses schema files that insert rows. 4. Review the migration to confirm it contains only the intended incremental change. 5. Reset or apply locally as needed. Writes that must commit together, such as sending a quote with its party snapshot, published version and status history, go in one `security invoker` function that the service calls once, because each Data API request runs in its own transaction. For rollbacks, revert declarative schema files and generate a new forward migration instead of rewriting history. ## Layout And Ordering [#layout-and-ordering] `supabase/schemas/` holds `extensions.sql` and `shared/` (cross-domain types, number sequences and schema grants), then one folder per domain: `tenant`, `identity`, `crm`, `catalog`, `work`, `people`, `commerce`, `expenses`, `conversations`, `ai`, `automation`, `platform` and `portal`. Inside a domain folder, each table has one file named after the table, such as `commerce/document_parties.sql`. The file holds the table with its indexes, comments, triggers, audit trigger, RLS and policies, so everything about a table is in one place. Enums live in `/types.sql`, functions in `/functions/.sql`, and bucket policies in `/storage.sql`. `better_supabase/` is generated by better-supabase, and the helper and hook files in `identity/permdock/` by PermDock. pg-delta orders the statements by their dependencies, so `supabase/config.toml` has no `schema_paths` list and file names do not set the run order. Define each function in one file only. After any move or rename, `pnpm supabase:diff` must report no schema changes, and CI fails on drift. ## RLS [#rls] Every table must have RLS enabled. Its policies live in the table file; pg-delta creates the helper functions a policy calls before the policy. Tenant-table policies use the init-plan form `organization_id in (select permdock.permitted_organization_ids(''))`, which Postgres evaluates once per statement (a hashed SubPlan) instead of calling a helper for every candidate row. A permission whose grants are split by condition is named by its permission key through `permdock.permitted_organization_ids_by_permission('')`. Platform tables use `(select permdock.permdock_has('platform.<...>'))`, and chat rows use `thread_id in (select public.accessible_chat_thread_ids())`. The helpers are generated from the PermDock policy; see [RBAC](/docs/architecture/rbac). A few policies take the permission key from the row: the lifecycle policies in `supabase/schemas/automation/workflow_lifecycles.sql` check each subject type in the init-plan form, and the contextual artifact policies in `supabase/schemas/ai/ai_contextual_artifacts.sql` pass the row's `origin_permission` to `permdock.permitted_organization_ids_by_permission`. ## Seeds And Types [#seeds-and-types] Seed data lives under `supabase/seeds/`. After schema changes, regenerate Supabase TypeScript types for both `public` and `graphql_public` schemas so generated code stays complete. # Tenancy (/docs/architecture/tenancy) URL-based organization tenancy, slug extraction, access guards, and tenant navigation. CentraKit encodes the active organization directly in the URL. There is no `current-organization` cookie, hidden precompute segment, or in-memory tenant switch. ## URL Structure [#url-structure] Tenant-scoped routes sit under `[orgSlug]` inside the authenticated SaaS app group: ```text app/[locale]/[brand]/(app)/ layout.tsx page.tsx [orgSlug]/ layout.tsx page.tsx portal/ users/ teams/ roles/ workflows/ billing/ usage/ settings/ ``` System routes such as `/admin/*`, `/profile`, `/organizations/new`, and `/auth/*` live outside the `[orgSlug]` segment because they are not scoped to a tenant. `[locale]` and `[brand]` are hidden root segments: the proxy rewrites onto them, so neither appears in a public URL. ## Brands [#brands] One deployment serves several product brands, and each brand has one app host, such as `app.centrakit.com` or `app.mechup.com`. The brand registry in `packages/foundation/src/brands.ts` maps hosts to brands. - The proxy maps the host to a brand with a synchronous lookup in a frozen `Map` and inserts it after the locale in the rewrite. It does no fetch, database, cookie or header work for the brand. - Rendering reads the brand with `getBrand()` from `apps/saas/lib/brand/current-brand.ts`, which uses the `brand` root param. Server Actions and route handlers have no root params, so `getBrand()` falls back to the request host there, and `getRequestBrand()` reads the host directly. - Each organization belongs to one brand through `organizations.brand`. Creating an organization takes the brand from the request host, never from user input. - The CentraKit host manages every tenant, so users see and open all their organizations there. A product brand host shows only that brand's organizations. `brandServesOrganization()` in `packages/foundation/src/brands.ts` holds the rule; the org switcher, `resolvePostAuthPath` and the app shell lists filter with it, and the slug gate calls `notFound()` when it refuses. The chrome always follows the host, so a MechUp organization opened on the CentraKit host keeps the CentraKit theme. - Platform admins see every brand in `/admin/organizations`, with a brand column and filter, and can move an organization to another brand. A database trigger refuses brand changes from anyone except platform admins and the service role. - Cookies are host-only, so users sign in on each brand host separately. ## Slug Resolution Flow [#slug-resolution-flow] 1. Tenant URLs carry the organization as `[orgSlug]` under `app/[locale]/[brand]/(app)/[orgSlug]/...`. 2. `extractOrgSlug(pathname)` in `apps/saas/lib/organizations/url.ts` strips reserved root segments, validates the slug shape, and lowercases it when callers need pathname parsing (for example navigation helpers). 3. The proxy does **not** inject an org-slug request header. Org identity stays URL data so Cache Components / Partial Prefetching can upgrade param-specific shells. 4. Tenant page shells stay synchronous and do not `await params` at the page or layout root. Access gates and data loaders run inside Suspense islands and receive `orgSlug` via `params.then(({ orgSlug }) => …)`. 5. `getCurrentAppAccess(orgSlug)` / `requireOrganizationAccess({ orgSlug, permission? })` resolve membership by the explicit slug. Pass `null` only for session-only chrome (`/profile`, `/admin`, `/organizations`). Unknown slugs return `notFound()` rather than redirecting to a canonical path. ## Guards [#guards] - `requireOrganizationAccess({ orgSlug, permission? })` runs inside tenant-page Suspense islands. Callers pass the slug from route `params`; the guard checks membership and optionally enforces a permission using the loaded organization permission set. - `requireCustomerPortalAccess({ orgSlug })` allows organization members and contact-linked non-members into `/{orgSlug}/portal`. Unknown organizations 404. Portal record lists remain scoped to linked `customer_contacts` (empty for org members without a contact link). - `requireSystemAccessAdmin()` gates `/admin/*` pages. - `AppShell` mounts once in `(app)/layout.tsx`. `[orgSlug]/layout.tsx` runs `OrganizationSlugAccessGate` in Suspense so unknown/non-member slugs still `notFound()` without blocking cold chrome paint (may be HTTP 200 with the not-found UI after streaming). `/profile`, `/admin` and `/organizations` share that same shell without a slug gate. Sidebar nav hrefs still re-derive the active organization from the pathname on the client. The proxy enforces the coarse authenticated-app boundary. Page guards enforce organization membership, customer linkage, and permissions. Staff membership remains the default access kind when staff and customer access overlap, but the compact app-shell organization payload preserves both available access kinds. ## Navigation And Links [#navigation-and-links] - `apps/saas/lib/navigation.ts` defines nav sections with a `scope` and optional `permission`. - Tenant-scoped links use organization-relative hrefs like `/settings`; `resolveNavHref` prefixes them with `/{orgSlug}` at render time. - Organization switchers are `Link`-based, and the link decides between staff and portal before navigation through `organizationHomeHref`: staff go to `/{slug}` and customer-only users go to `/{slug}/portal`. The cached customer access gate stays as the fallback for typed or bookmarked URLs. - Navigation mode follows the URL: organization routes show Organization navigation, `/{orgSlug}/portal/**` shows Customer navigation and `/admin/**` shows Admin navigation. The organization switcher is the only place to change mode. An organization where the user is staff opens a submenu with Organization (`/{orgSlug}`) and Portal (`/{orgSlug}/portal`); on desktop the submenu opens on hover and clicking the organization itself opens `/{orgSlug}`, a customer-only organization links straight to its portal, and system admins get a last Admin item that opens `/admin`. Route guards remain authoritative. - Breadcrumbs strip locale and active organization slug before matching route labels, then re-prefix hrefs back onto the tenant segment. ## Creating And Deleting Organizations [#creating-and-deleting-organizations] - `createOrganizationAction` returns `{ organizationId, slug }` and redirects to `/{slug}`. - `deleteOrganizationAction` returns `{ nextOrganizationSlug }` and redirects to the next available organization or `/organizations/new`. ## Why URL Tenancy [#why-url-tenancy] - RSC caching behaves naturally because there is no private request-scoped current-organization cookie. - Links are shareable and open the right tenant directly. - The proxy stays thin: it handles locale and brand rewrites and session routing only, with no org-slug header and no database calls. - Admin and per-user pages do not inherit a slug they do not need. ## Tests [#tests] - `apps/saas/tests/rbac-url.test.ts` covers `extractOrgSlug`. - `tests/integration/saas/proxy.test.ts` covers proxy redirects, public-path behavior and the host-to-brand prefix. - `tests/integration/saas/customer-portal-guards.test.ts` covers customer-only, staff portal access, and unknown-organization portal access. - `tests/integration/saas/app-organization-access.test.ts` covers merged staff/customer access and route-specific shell context. - `apps/saas/tests/app-navigation-mode.test.ts` covers route-derived and available navigation modes. # 0000: Template (/docs/decisions/0000-template) Copy this file to record a new decision. - Status: Proposed, Accepted, Superseded by 00XX, or Deprecated - Date: YYYY-MM-DD ## Context [#context] What forces are at play? State the standard item or the problem, and what makes the default answer a poor fit here. ## Decision [#decision] What we do, stated in one or two sentences. ## Consequences [#consequences] What becomes easier, what becomes harder, and what would make us revisit this record. # 0001: Next.js route handlers for the API and MCP (/docs/decisions/0001-next-route-handlers-for-api-and-mcp) The REST API and MCP server stay as Next.js route handlers in apps/api instead of Hono, oRPC, and a separate apps/mcp. - Status: Accepted - Date: 2026-09-30 ## Context [#context] The repo standard asks for a Hono shell with an oRPC contract in `packages/contract`, and a separate `apps/mcp` built on orpc-mcp. CentraKit already ships more than 200 versioned route handlers under `apps/api/app/api/v1/`, an MCP server at `/api/mcp`, and an OpenAPI 3.2 document generated by `next-openapi-gen` from Zod schemas and route JSDoc. ## Decision [#decision] Keep Next.js route handlers, `next-openapi-gen`, and the MCP server inside `apps/api`. Scalar stays at `/api-docs`. Errors follow RFC 9457 Problem Details, so the wire format already matches the standard. ADR 0011 later moved the API onto each brand's app host and retired the Scalar page, because its `/_next` assets would collide with the SaaS app on a shared host. `/api-docs` now redirects to the REST API page in the docs, and the MCP server is served at `/mcp`. The rest of this decision stands. ## Amendment (2026-10-16) [#amendment-2026-10-16] The MCP server no longer uses `mcp-handler`. `apps/api/lib/mcp/server.ts` builds one `McpServer` per request with `createMcpHandler` from `@modelcontextprotocol/server`, and gates it with `requireBearerAuth` from the same package, so the 401 and `insufficient_scope` challenges come from the SDK. `mcp-handler` had become a thin wrapper over those two calls. Tool names are now the OpenAPI operation IDs in snake case (`list_tasks`, not `listTasks`), as the repo standard asks. This is a breaking change for MCP clients that pinned tool names. Each tool declares an `outputSchema` built from the generated REST response schema; a response that is not an object is wrapped as `{ value }`, which matches the `structuredContent` the executors already return. Tool input and output are validated by the SDK through Standard Schema, using the same Zod schemas. The SDK's JSON Schema validator (ajv on Node, CfWorker on workerd) only runs for elicitation results and `fromJsonSchema()`, and the server uses neither, so no `jsonSchemaValidator` is configured. Pick one when the server starts sending form elicitations. `withOAuthProtectedResource` from the standard is only partly adopted: the protected resource metadata route is still hand-written in `apps/api/app/.well-known/oauth-protected-resource/route.ts`, because it has to advertise the resource of the request's brand host. Host header validation from the SDK is not applied, because the API is reached through the brand host rewrites and is protected by OAuth on every call. ## Consequences [#consequences] - The committed `apps/api/public/openapi.json` stays the contract, and CI fails when it drifts. - MCP tools stay curated in `apps/api/lib/mcp` and do not mirror REST one to one. - Route handlers stay for now. Moving the API layer to Hono and an oRPC contract is a planned follow-up with its own plan, not part of the repo standard upgrade. - The Valibot migration is no longer tied to oRPC. ADR 0004 keeps Zod until the API layer migration, and either change can land first. # 0002: Bounded package topology (/docs/decisions/0002-bounded-package-topology) Domain logic stays split into bounded contract, core, and supabase packages instead of domain, contract, and services. - Status: Accepted - Date: 2026-09-30 ## Context [#context] The repo standard groups domain logic into `packages/domain`, `packages/contract`, `packages/services`, and `packages/supabase`. CentraKit uses a provider-swappable three-layer model per bounded context: - Contract packages (`foundation`, `identity`, `tenant`, `crm`, `catalog`, `work`, `commerce`, `conversations`, `automation`, `platform`, `portal`, `people`) own interfaces, schemas, and domain errors. - `packages/supabase-*` packages own the Supabase implementations. - `packages/core` and `packages/core-*` own request-scoped service wiring. Integrations with third parties live under `packages/integrations/*`, which is an extra workspace glob. ## Decision [#decision] Keep the bounded topology and the `packages/integrations/*` glob. Turbo boundary tags encode the same import rules the architecture rule describes. ## Consequences [#consequences] - Apps construct only the services they need through `@workspace/core-*` factories. - The workspace list keeps `packages/integrations/*` in addition to the standard globs. - Turbo tags are `app`, `core`, `provider`, `contract`, `ui`, `config`, `integration`, and `test` instead of the standard package names. - The `app` tag allows `provider`. Turbo applies boundary rules to dependencies of dependencies, and every app reaches `supabase-*` through `core*`, so denying `provider` would fail every app. The Oxlint `appImportPatterns` rule in `oxlint.config.ts` is what blocks a direct `@workspace/supabase-*` import from an app. # 0003: The apps/saas name and host-based routing (/docs/decisions/0003-saas-app-name-and-host-routing) The product app stays apps/saas, and every surface is served from its own subdomain instead of path prefixes. - Status: Superseded by 0011 - Date: 2026-09-30 ## Context [#context] The repo standard names the product app `apps/app` and serves surfaces through path rewrites (`/app`, `/api`, `/docs`). CentraKit serves each surface from its own host through Vercel Services host rewrites: `app.*`, `api.*`, `docs.*`, the marketing sample host, and the showcase site on the apex. The extra `apps/showcase` app is the CentraKit template site. ## Decision [#decision] Keep `apps/saas`, `apps/showcase`, and host-based routing. The docs app stays root-mounted on `docs.*`, so it uses no `basePath`. Portless names stay `app`, `api`, `docs`, `marketing`, and `showcase`. ## Consequences [#consequences] - Public URLs still derive from one `NEXT_PUBLIC_SITE_URL` through `packages/foundation/src/public-urls.ts`. - Per-page Markdown for docs lives at `/.md` and `/.mdx` on the docs host, not under `/docs`. - Moving to path-based routing would need a separate plan that covers cookies, OAuth redirect URLs, and the PWA scope. # 0004: Zod until the API layer migration (/docs/decisions/0004-zod-until-api-layer-migration) Zod 4 stays the single schema library until the API layer moves to oRPC. - Status: Accepted - Date: 2026-09-30 ## Context [#context] The repo standard uses Valibot for env, domain, contract, forms, and AI structured output. CentraKit generates its OpenAPI document with `next-openapi-gen`, which only reads Zod schemas or TypeScript types. Moving to Valibot while keeping `next-openapi-gen` would break the committed OpenAPI document and the generated API client. ## Decision [#decision] Keep Zod 4 as the only schema library. Forms use TanStack Form with Zod through Standard Schema. `no-restricted-imports` blocks other schema libraries so the codebase keeps one. ## Consequences [#consequences] - Valibot adoption moves with the later API layer plan (ADR 0001), where an oRPC contract with `@orpc/valibot` replaces `next-openapi-gen`. - The `valibot` skill is not installed until that plan starts. # 0005: Audit log naming (/docs/decisions/0005-audit-log-naming) The audit log keeps the audit_logs table family and capture_audit_log_row() instead of audit_events and audit_row_change(). - Status: Accepted - Date: 2026-09-30 ## Context [#context] The repo standard names the audit table `audit_events` and the row trigger function `audit_row_change()`. CentraKit already ships `audit_logs`, `audit_log_restricted_details`, `audit_log_export_jobs`, and `audit_log_table_registry`, with `capture_audit_log_row()` attached through the registry. Production data exists in these tables. ## Decision [#decision] Keep the current names. Each domain table still gets the trigger, its ignored and redacted column lists in the registry, and a pgTAP case. ## Consequences [#consequences] - Renaming would need a data migration and a coordinated release for no functional gain. - `docs/architecture/audit-logs.mdx` stays the reference for how to register a new table. # 0006: Env file layout (/docs/decisions/0006-env-file-layout) pnpm env:pull writes development values to .env.local so .env.development.local can hold local Supabase overrides. - Status: Accepted - Date: 2026-09-30 ## Context [#context] The repo standard has `pnpm env:pull` write `.env.development.local`, `.env.preview.local`, and `.env.production.local`. CentraKit uses `.env.development.local` for hand-written overrides that point the apps at local Supabase and Portless hosts. Pulling into that file would erase those overrides on every pull. ## Decision [#decision] `pnpm env:pull` writes the development environment to `.env.local`, and also writes `.env.preview.local` and `.env.production.local`. Local overrides stay in `.env.development.local`, which Next.js loads after `.env.local` in development. Apps load the root files through `loadMonorepoEnv` in `packages/next-config`. ## Consequences [#consequences] - Root scripts load env through `loadMonorepoEnv` instead of `dotenv -e` wrappers. - `.env.example` lists every key, and the overrides file is documented in `AGENTS.md`. # 0007: Deferred better-supabase and PermDock (/docs/decisions/0007-deferred-better-supabase-and-permdock) The hand-written Supabase packages and the custom RBAC stay until dedicated migration plans. - Status: Superseded by 0009 - Date: 2026-09-30 ## Context [#context] The repo standard uses better-supabase for typed repositories and PermDock for roles and permissions. better-supabase is at version 0.1.0, and PermDock is not published on npm yet. CentraKit already has typed `supabase-*` packages that return `Result` values and run as the caller, and an RBAC catalog in `packages/identity/src/rbac` mirrored by SQL seeds and init-plan RLS policies. ## Decision [#decision] Keep the hand-written `supabase-*` packages and the custom RBAC. The `db:gen` script, `better-supabase doctor`, `packages/policy`, `permdock rls generate`, and `permdock doctor` wait for their own plans. ## Consequences [#consequences] - `pnpm supabase:types` generates `database.types.ts` in the meantime. - Permission changes still update `packages/identity/src/rbac/permissions.ts`, the SQL seeds, and the RLS policies together. # 0008: CLI on citty, clack and c12 (/docs/decisions/0008-cli-unjs-stack) The centrakit CLI moves from commander and hand-written helpers to citty, @clack/prompts, c12 and Node 24 built-ins, and the guard tests scan files with fs.glob and oxc-parser. - Status: Accepted - Date: 2026-10-02 ## Context [#context] The CLI was built on commander with a custom runner, hand-written runtime flag parsing, a JSON-only config file and a `spawn`-based browser opener. It never prompted for missing values, so `register` took the password as a positional argument and every organization command needed a UUID typed by hand. The guard tests in `tests/integration` and `packages/ui/tests` each had their own recursive directory walker and matched imports with regular expressions, which also matched text inside comments and strings. ## Decision [#decision] - Commands and flags use citty. Each command is a `defineCommand` default export loaded lazily from `src/root.ts`, so `--version` and help stay fast without a separate fast path. - Interactive flows use `@clack/prompts`, wrapped in `src/ui.ts`. Prompts only appear when stdin and stdout are terminals, `--json` is off and the session is not non-interactive. Otherwise a missing value is an error. - Config files load through c12, which uses jiti for TypeScript configs. The package exports `defineConfig` from `centrakit/config`. - Colors use `node:util` `styleText`, the browser opener uses `open`, and command suggestions use `fastest-levenshtein`. - The CLI targets Node 24. The build script runs with plain `node` through type stripping, so `tsx` is no longer needed. - Guard tests list files with the `listFiles` and `listFilesSync` helpers from `@workspace/test-fixtures/source-files`, built on `fs.glob`, and read imports with `importSpecifiers`, built on `oxc-parser`. ## Consequences [#consequences] - commander is removed from the catalog. `std-env` was considered for CI detection but left out, because it reads the environment once at import and the tests change it per case. The CLI reads `CI` through its t3 env schema at call time instead. - citty does not support repeated flags, so `--social` is read from the raw arguments with `collectRepeatedFlag` in `src/args.ts`. - citty only passes a subcommand the arguments after its name, so `runCli` moves leading global flags behind the command path before parsing. - `register` takes the password from a hidden prompt or `--password-stdin` and no longer accepts it as an argument. - New guard tests should use the shared helpers instead of adding another walker or import regex. ## Amendment (2026-10-16) [#amendment-2026-10-16] - Distribution: the CLI ships as a tarball attached to each GitHub release by `release-cli.yml`, not on npm. The `centrakit` name is unclaimed on npm, the CLI has no users outside the team yet, and trusted publishing needs an npm organization and a publish job behind a protected environment. Publishing to npm with trusted publishing is the follow-up once there is an external user, and the change goes through a `.github` pull request. Until then `apps/cli/package.json` is `"private": true`, so a stray `npm publish` cannot claim the name; Changesets still versions and tags it through `privatePackages`. `release-cli.yml` keeps `id-token: write` because the shared setup action exchanges an OIDC token for the Turborepo remote cache; it is not a leftover from a publish step. - Errors: `CliError` carries an RFC 9457 `problem`. API failures keep the server's problem, and errors raised by the CLI use `type: "about:blank"` with a `code` of `USAGE_ERROR`, `CANCELLED` or `CLI_ERROR`. With `--json` the whole problem is printed to stdout. - Exit codes: 0 for success, 1 for a failure, 2 for a usage error such as an unknown command, an invalid flag value or a missing argument, and 130 for a cancelled prompt. - Secrets are never flag values. `--token` is removed in favor of `CENTRAKIT_TOKEN` or `--token-stdin`, and `--base-url` is renamed to `--api-url`. - The session file is created with mode `0600` in a `0700` directory, and the config directory honors `XDG_CONFIG_HOME`. - `engines.node` is `24.x`, matching the root, and every command's `--help` output is a snapshot in `tests/integration/cli/help-snapshots.test.ts`. # 0009: Adopt better-supabase and PermDock from npm (/docs/decisions/0009-adopt-better-supabase-and-permdock) CentraKit moves its Supabase data access onto better-supabase and its roles and permissions onto PermDock, consuming both as published npm packages. - Status: Accepted - Date: 2026-10-05 - Supersedes: 0007 ## Context [#context] ADR 0007 deferred both libraries until they were published. `better-supabase` 0.5.1 and `permdock` 0.1.0 are now on npm. CentraKit hand-writes its data access in the `supabase-*` packages (about 1,560 `.from` and 210 `.rpc` call sites with manual row and error mapping) and keeps its permission keys in four hand-maintained copies across TypeScript and SQL, with 921 RLS policies typing those keys by hand. ## Decision [#decision] Adopt both libraries as an outside user would: the published npm versions, pinned exactly in the pnpm catalog, never a workspace link or git URL to the library repos. better-supabase owns transport, sessions, typed repositories and `doctor`. PermDock owns permissions, roles, the JWT claims, the access token hook and the RLS helpers. When CentraKit needs something a library lacks, the fix is a pull request on that library, released and then bumped here. `plans/050-adopt-better-supabase-and-permdock.md` holds the phases. ## Consequences [#consequences] - `better-supabase gen` generates the database types, and `better-supabase doctor` reports schema findings in CI. - Repository rows use camelCase, while raw supabase-js calls, RPC arguments and SQL keep database names until every package is ported. - Permission changes start in the PermDock policy, and the SQL seeds, hook and RLS helpers are generated from it. - Both libraries are young and break in minor releases, so every bump reads the changelog and runs the full gate. # 0010: Billing on the Stripe Sync Engine (/docs/decisions/0010-stripe-sync-engine-for-billing) Subscriptions, invoices, payment methods and the billing contact are read from the Stripe Sync Engine's tables instead of a hand-written Stripe mirror. - Status: Accepted - Date: 2026-10-06 ## Context [#context] CentraKit kept its own copy of Stripe's billing objects: `organization_billing_customers`, `organization_subscriptions` with a history table, `organization_payment_methods`, `organization_invoices` and `organization_tax_ids`, refreshed by a webhook processor and a reconcile job (about 2,500 lines). The better-supabase `billing` and `entitlements` blocks read Stripe's objects from the [Stripe Sync Engine](https://github.com/supabase/stripe-sync-engine) and never call Stripe to find out what a tenant pays for. Keeping the mirror would mean asking the blocks to work against an app-specific copy, which their design argues against. ## Decision [#decision] Run the Stripe Sync Engine 0.48.5 (`@supabase/stripe-sync-engine`, pinned in the catalog) as a library inside the API app. The Stripe webhook consumer lets the engine store each platform event in the `stripe` schema and then hands the event to the better-supabase `billing` block, which links the customer to its organization. The engine owns the `stripe` schema and installs it with its own migrations (`pnpm stripe:sync:migrate`); CentraKit's declarative schema never defines it, and every CentraKit function that reads it checks that the table exists. CentraKit keeps its plan catalog, credit packages, price publishing and invoice recipients, and derives each organization's plan from the subscription item's price (`billing.organization_plans`). ## Consequences [#consequences] - The mirror tables, their policies and audit triggers, the reconcile logic and most of the Stripe provider are gone; Stripe is the only record for the billing contact and tax ids. The subscription history for admin analytics stays, appended from the Sync Engine state after each subscription event. - The Sync Engine is infrastructure every environment needs: a migration step and a backfill per Supabase project, and `POSTGRES_URL` in the SaaS and API apps. `docs/operations/stripe-billing.mdx` lists the hosted steps. - Updating billing details now requires Stripe; with placeholder keys the pages read the demo rows only. - Engine 0.48.5 mis-detects an existing `invoice_status` type in another schema and stores timestamps as `integer`; the migration script works around the first, and dates after 2038 cannot be stored. ## Amendment (2026-10-16) [#amendment-2026-10-16] - **Alternative considered: Stripe Data Pipeline.** Data Pipeline copies Stripe data into a warehouse on a schedule. It is built for analytics, refreshes hours behind Stripe, and does not write into Postgres where RLS and the better-supabase blocks read, so it cannot replace the Sync Engine for entitlements. - **Webhooks are the sync path.** The billing service still calls `refreshFromStripe`, which runs `syncCustomer`, after plan changes, cancellations, billing detail updates and admin invoice transitions. That doubles the Stripe calls for each change, because the webhook consumer stores the same events moments later. The follow-up removes the inline sync from mutations, returns the state from the Stripe response, and keeps `syncFromStripe` as the explicit refresh. - **No PaymentIntent during render.** The customer portal's `getPaymentClientSecretForPortal` creates a new PaymentIntent while the page renders when the open intent no longer matches the balance. A page view should never create Stripe objects. The follow-up moves Elements to deferred intent creation (`mode: "payment"` with amount and currency), so the intent is created in a Server Action when the customer pays. - Both follow-ups change live payment flows, so they land with tests against Stripe test mode rather than inside the repo standard upgrade. # 0011: Multi-brand path routing (/docs/decisions/0011-multi-brand-path-routing) Every brand is served from one app host with the API, MCP server and docs as path rewrites, and the SaaS app renders each brand through a hidden brand root param. - Status: Accepted - Date: 2026-10-12 - Supersedes: 0003 ## Context [#context] ADR 0003 served every surface from its own host: `app.*`, `api.*`, `docs.*`, the `sample.*` marketing site and the showcase on the apex, all derived from one `NEXT_PUBLIC_SITE_URL` in `packages/foundation/src/public-urls.ts`. One deployment now serves several product brands (CentraKit, MechUp and StableDock), each on its own domain. With host routing every brand would need its own API, docs and marketing hosts, plus OAuth redirect URLs and Auth allow list entries for each, and a single site URL cannot describe more than one brand. The public website of each brand moves to Lovable, so the apex and `www` hosts no longer belong to this deployment. ## Decision [#decision] Serve each brand from one `app.` host and route by path. The root `vercel.json` sends `/api/*` and the public `/portal/calendars/:token` and `/portal/files/:token` routes to the `api` service, `/mcp` to its `/api/mcp` handler, `/.well-known/oauth-protected-resource` to `api`, and `/docs/*`, `/llms.txt` and `/llms-full.txt` to the `docs` service, which has `basePath: "/docs"`. Everything else goes to `saas`. Only `saas`, `api` and `docs` deploy. `apps/marketing` and `apps/showcase` stay in the repo but leave Vercel, and the apex and `www` domains point at Lovable through DNS. The `api.*`, `docs.*` and `sample.*` hosts are cut over without redirects. The brand registry in `packages/foundation/src/brands.ts` lists every brand with its hosts per environment. `production` and `development` are picked from `VERCEL_ENV`, and anything else is `local`. `NEXT_PUBLIC_SITE_URL` only overrides the default brand's origin in the local environment, for E2E builds on a port. | Brand | Production | Development (`develop`) | Local | | ---------- | -------------------- | -------------------------- | ---------------------- | | CentraKit | `app.centrakit.com` | `app.centrakit.dev` | `app.localhost` | | MechUp | `app.mechup.com` | `mechup.centrakit.dev` | `mechup.localhost` | | StableDock | `app.stabledock.app` | `stabledock.centrakit.dev` | `stabledock.localhost` | | Caresly | `app.caresly.app` | `caresly.centrakit.dev` | `caresly.localhost` | | PropDock | none yet | `propdock.centrakit.dev` | `propdock.localhost` | | MarryDuck | none yet | `marryduck.centrakit.dev` | `marryduck.localhost` | | SignalDock | none yet | `signaldock.centrakit.dev` | `signaldock.localhost` | | Spacelio | none yet | `spacelio.centrakit.dev` | `spacelio.localhost` | | LegalDock | none yet | `legaldock.centrakit.dev` | `legaldock.localhost` | The SaaS route tree is `app/[locale]/[brand]/...`. The proxy maps the host to a brand with a synchronous lookup in a frozen `Map` and inserts the brand after the locale in the next-intl rewrite, so `[brand]` never shows in a public URL. It adds no fetch, database, cookie or header work. Rendering reads the `brand` root param, so every static shell is prerendered per brand with the right name, logo, fonts and theme. Server Actions and route handlers have no root params and read the brand from the request host instead. Each organization belongs to one brand through `organizations.brand`, set from the request host when the organization is created. The CentraKit host is where the tenants of every brand are managed, so it lists and opens all of a user's organizations. A product brand host lists and opens only that brand's organizations, and another brand's slug renders not found there. Platform admins see every brand in `/admin`, with a brand column and filter, and a trigger lets only platform admins and the service role change an organization's brand. Organization links decide between staff and portal before navigation, so customer-only users get `//portal` links. Per-brand theming is tokens only: `[data-brand=""]` overrides for neutrals, primary, ring, charts, radius, menu accent, `--font-sans` and a new `--font-heading`, generated from a shadcn preset. Every brand's preset is still `null`, so all brands render the CentraKit tokens and placeholder marks. `BrandMark` in `@workspace/ui` takes a brand id, and `AppBrandMark` in `apps/saas` reads the root param. The MCP resource moves from `/api/mcp` on the API host to `/mcp`, and the protected resource metadata is built per host. The accepted token audiences are the `/mcp` URL of every brand host in the environment, Supabase's default `authenticated` audience and `MCP_RESOURCE_AUDIENCE`. ## Consequences [#consequences] - Prerendered shells multiply by the number of brands, from about 470 to about 1,410 with three brands. - Cookies stay host-only, so a session on one brand host does not sign the user in on another. - MCP clients must switch to `https://app./mcp`. Tokens issued for the old `https://api.centrakit.com/api/mcp` audience are rejected unless `MCP_RESOURCE_AUDIENCE` still carries that value, so this is a breaking change for MCP clients. - REST clients and the CLI move to `https://app./api`; the CLI defaults to the CentraKit app host and takes `--host` for another brand. - Problem `type` URIs move to `https://app.centrakit.com/docs/developers/errors#` on every brand host. - Stripe, the Google OAuth client and the Supabase project stay shared. Third-party screens such as the Google consent screen, Stripe Checkout and receipts, and the Supabase OAuth consent page (on the Supabase `site_url`, which is the CentraKit host) still say CentraKit. Emails take the brand's name as the sender name but keep the one `EMAIL_FROM` address. - `typedRoutes` stays on in `apps/saas`. - `apps/showcase` is the CentraKit template site, not a brand host, so it is exempt from the rule against hardcoded product names. Its copy may name CentraKit directly. `apps/marketing` is not exempt. - Adding a brand means a registry entry, its Vercel domains, Supabase redirect URLs on `main` and `develop`, a Portless alias, and its preset, fonts and assets. - Revisit this record if a brand needs its own deployment, Supabase project or Stripe account, or a session shared across brand hosts. # 0012: The Data API for module access from request paths (/docs/decisions/0012-data-api-for-saas-module-access) Request paths reach better-supabase modules and Auth through the Data API and the Auth admin API; direct Postgres is reserved for the libraries that only speak SQL. - Status: Accepted - Date: 2026-10-13 ## Context [#context] Several admin and organization pages opened a direct Postgres connection from the request path: the users list read last sign-ins from `auth.sessions`, suspend, ban and delete wrote `auth.users` and `auth.sessions`, the audit recorder called `better_supabase.audit_event`, the admin dashboard read job queue health, and the billing pages called the billing module and the Sync Engine read models. Each of these needed `POSTGRES_URL`, so a missing or wrong value broke pages that otherwise ran on the Data API like the rest of the app, and the request held a second connection pool next to PostgREST. ## Decision [#decision] Request paths use the same three routes as every other service call: - better-supabase modules that a request path calls get `api` entry points in `better-supabase.config.ts` (`sessions`, `audit`, `jobs` and `billing`, next to the modules that already had them). The server calls them with `rpcTransport(serviceClient, { schema: "api" })`, and the entry points are granted to `service_role` only. - Auth changes go through the Auth admin API (`auth.admin.updateUserById` for bans, `deleteAccount` from `better-supabase/server` without `sql`). Ending another user's sessions goes through `public.end_user_sessions`, a service-role-only function. - CentraKit's own service-role functions in `public` (`organization_subscription`, `organization_stripe_invoices`, `capture_organization_subscription_history`, `plan_subscription_ids`) are called with `db.$rpc` on a service-role `Db`. Direct Postgres stays for the libraries that only speak SQL: the Stripe Sync Engine, job claiming and the drain, the webhook inbox, idempotency, agent checkpoints, usage reporting to Stripe and the API's caller Postgres modes. Both servers check for `POSTGRES_URL` in `instrumentation.ts` and refuse to start without it. ### Secret-key contexts (amended 2026-10-16) [#secret-key-contexts-amended-2026-10-16] `createAdminContext()` from `@workspace/core/request-context` is the one way for apps and feature code to get a context on the secret key. It replaced `createAdminRequestContext`, and no other code reads `SUPABASE_SECRET_KEY`, except the API's MCP usage signer. `getSharedSupabaseAdminClient()` from `@workspace/supabase-runtime` is the lower-level primitive under it. Only `packages/core*` and the `packages/supabase-*` providers call it directly, for wiring that needs a client rather than a whole context: rate limiting, billing, email delivery, payment webhooks, agent chat, comments and acting-user contexts. Apps never import it, which the boundary tags already enforce. The repo standard keeps secret-key contexts off request paths. CentraKit allows them on a request path in these cases, each behind its own gate: - Platform admin pages and actions, after `withPlatformAccess` or `requireAccess` with a platform grant. - Public token flows (shared chat threads, public knowledge pages, quote and terms PDFs), after the token has been verified. - Tenant reads and actions that touch rows the caller cannot reach under RLS (inbox routing, workflow runs, template installs, calendar integration tokens, analytics rollups, portal PDFs), after `withTenantAccess`, `requireAccess` or the portal guard has checked the grant. - Chat handlers that record usage and approvals for the authenticated caller. - Usage metering for any metered action, such as the business registry lookup, after the action's own grant check. - The cached organization lookup by slug or id in `apps/saas/lib/organizations/access.ts`. It runs before the grant check because it feeds that check, and it returns only the organization row the gate needs. Reads that need no privilege use a publishable-key context instead, for example public product pages that call a security-definer RPC. Cron routes, job handlers, webhooks and workflow steps use it as system work. Moving a case to a security-definer RPC is preferred whenever the call site changes for another reason. ## Consequences [#consequences] - The admin users, dashboard and billing pages and every audited action work over PostgREST; only the Sync Engine path in billing and the API's background work open a Postgres connection. - A new module that a request path calls needs an `api` option and, when it should not be callable by clients, a `functions` list limited to service-role entry points. `grants.test.sql` checks that the new entry points are closed to `anon` and `authenticated`. - The job queue health source (`rpcQueueMonitor`) reads stats and dead letters and retries them, but cannot enqueue or claim; workers keep `createJobs` over `ctx.postgresAdmin`. - A missing `POSTGRES_URL` now fails at server start instead of on the first job, webhook or billing sync. # 0013: Ultracite for Oxlint and Oxfmt (/docs/decisions/0013-ultracite-for-oxlint-and-oxfmt) Lint and format presets come from Ultracite 8. The hand-maintained ox-config package is gone. - Status: Accepted - Date: 2026-10-13 ## Context [#context] `packages/ox-config` held the Oxlint presets, the Oxfmt config, and a vendored copy of the anti-slop plugin. Every workspace extended that package. The presets drifted from the rules we wanted at full strictness, and anti-slop had to be copied in by hand. Ultracite 8 publishes maintained Oxlint and Oxfmt presets, including type-aware linting and the JS plugins this repo already used: `@shadcn/lint`, anti-slop, `oxlint-plugin-react-doctor`, and `eslint-plugin-tsdoc`. `eslint-plugin-github` is left out because it loads `typescript-eslint`, which rejects TypeScript 7. ## Decision [#decision] - Ultracite 8 is the lint and format preset. The root `oxlint.config.ts` extends `ultracite/oxlint/core`, `vitest`, `shadcn`, and `anti-slop`, plus the `react-doctor` and `tsdoc` plugins. `options.typeAware`, `denyWarnings`, and `reportUnusedDisableDirectives: "error"` stay on. `options.typeCheck` stays off. - Next.js apps also extend `react`, `next`, and `next/js-plugins`. React workspaces extend `react`. Every other workspace extends the root config only. Each workspace repeats `ignorePatterns` and `settings`, because Oxlint does not inherit those through `extends`. - House rules that the presets do not cover stay in the root config: restricted imports, `node/no-process-env`, `turbo/no-undeclared-env-vars`, `no-underscore-dangle`, `import/no-unassigned-import`, and Testing Library. `no-console` is set on library workspaces, because a `packages/**` glob does not match when Oxlint runs inside the workspace. - OpenAPI route tags stay in `jsdoc/check-tag-names` `definedTags` and in the root `tsdoc.json`. The tsdoc plugin otherwise narrows that rule to TSDoc tags only. The override has to live on each Next.js app config as well as the root, because `next/js-plugins` is extended after the root config. - `packages/ox-config` is deleted, including the vendored anti-slop copy. ## Consequences [#consequences] - `pnpm check` and `pnpm fix` run `ultracite check` and `ultracite fix`. `pnpm lint` still typechecks each workspace through Turbo, then lints root tooling with nested config disabled. - A rule that fires is fixed in the code. A disable comment is only for a case the code cannot change, and it names the rule and the reason. - New workspaces depend on `ultracite` from the catalog and extend the root config. They do not grow a local preset package. # 0014: Declarative Schemas 2.0 and a one-time reset (/docs/decisions/0014-declarative-schemas-2-reset) The database is declared one table per file in domain folders, generated by pg-delta, and redesigned in a single breaking reset. - Status: Accepted - Date: 2026-10-15 ## Context [#context] Supabase Declarative Schemas 2.0 generates migrations with pg-delta, which orders statements by dependency and can fail on schema drift (`--strict-coverage`). Our schema files still carried a numbering scheme from the time when file order set the run order, and policies, triggers and audit registrations for one table were spread over several layer files. Earlier migrations had also accumulated data-model debt: bigint keys on a few tables, text status columns with check constraints, mixed `cancelled` and `canceled` spellings, plaintext bearer tokens, money in several scales, jsonb lists that hid relations, and about 19 seller and buyer columns copied onto each document table. The product has no production data that must be kept, so a breaking change with one reset costs less than a sequence of compatible migrations. ## Decision [#decision] - `supabase/schemas/` has one folder per domain with one file per table, named after the table. A table file holds the table, its indexes, comments, triggers, `bs_audit` trigger, RLS and policies. Enums go in `/types.sql`, functions in `/functions/.sql`. No file is numbered, and `config.toml` has no `schema_paths`. - `pnpm supabase:diff` runs pg-delta with `--strict-coverage`. CI runs it and the unscoped sync that covers event triggers, and fails on drift. - The migrations are squashed into one set (`pnpm supabase:squash`): extensions, `init`, event triggers, module data, the named data migrations and the PermDock seeds. Hosted develop and production are reset once with `docs/operations/database-reset.mdx`. Repairing the migration history is rejected, because the old schema does not match the new `init`. - Customers, customer assets and teams use `uuid` keys. The `{customer_id}` invoice number token is removed. - Status and kind columns are enums with the `canceled` spelling. Tables written by better-supabase module functions keep text columns. - Bearer tokens are stored only as sha256 hashes and shown once. - Timestamps end in `_at` and dates in `_on`. - Seller, buyer and ship-to snapshots live in `document_parties`, with a `document_party_role` enum and a check that each row belongs to exactly one quote, quote version, invoice or order. Sending a quote or invoice writes the snapshot, the status and the history in one `security invoker` function. - Tax identifiers are `chamber_of_commerce_number`, `vat_number` and `iban` in the database. Public contract fields keep their names (`kvk`, `btw`, `vatId`, `sellerKvk`, `buyerBtw`), and the services map between them, so API clients do not break for a column rename. The webhook and file contracts are the exception: they follow the renamed date columns. - jsonb lists that model relations become child tables with `position`: `quote_accepted_optional_lines` and `organization_social_links`. Kept on purpose: - `organization_invitations.prefill` stays jsonb. The better-supabase invitations block owns it in its RPCs and client, and a `jsonb-schemas` check validates its shape. - The lifecycle workflow keys (`quotes.lifecycle_workflow_key`, `invoices.lifecycle_workflow_key`, `templates.lifecycle_workflow_key` and the organization defaults) stay validated text with no foreign key. A key matches several versioned `workflow_definitions` rows, so it has no unique target. - `chat_messages.dispatch_id`, `quote_version_lines.source_line_id` and `product_scan_requests.workflow_run_id` stay correlation ids without a foreign key. The rows they point at can be deleted or cleaned up by retention while the referencing row must stay. ## Consequences [#consequences] - An agent adds a table by adding one file in its domain folder, then runs `pnpm supabase:diff -f `. - A later squash needs another reset of every hosted database, so migrations are incremental from here on. - Contract names and column names differ for tax identifiers. The mapping lives in each Supabase service and is covered by its unit tests. - Follow-ups not done in this reset: the settings columns on `organizations` versus `organization_settings`, and the status history tables that overlap with workflow lifecycle events. # 0015: Direct-to-develop workflow (/docs/decisions/0015-direct-to-develop-workflow) Maintainers and agents commit straight to develop instead of opening one branch and pull request per change. - Status: Accepted - Date: 2026-10-16 ## Context [#context] The shared repo standard puts every agent run on its own `/` branch with one pull request into the working branch. CentraKit has a small team, only `develop` and `main` deploy on Vercel, and feature branches get no preview. A pull request per change adds review overhead without a preview to review, while the Lefthook pre-commit and pre-push hooks already run `pnpm verify:affected` on every commit. The standard allows a direct-to-develop variant when a repo records it in `AGENTS.md` and in an ADR. ## Decision [#decision] Commit and push straight to `develop` with small conventional commits and explicit `git add `. Never skip hooks. `main` moves only through the `develop` to `main` pull request, which runs the full CI and the Changesets release. Changes to `.github/` workflows and actions are the exception. They go on a branch with a pull request into `develop`, so the new workflows run on `pull_request` before they reach the branch that deploys. ## Consequences [#consequences] - Changes reach the `develop` preview without waiting for a review. - CI on `develop` is the first full gate, so a push that passes the affected hooks can still fail the integration, Supabase or build suites there. The integration shards run without a database; the `Live database suites` job in `verify.yml` runs the live RLS and repository suites against a native local stack, and `database.yml` runs pgTAP when `supabase/**` changes. Fix forward on `develop`. - `main` needs branch protection with the required checks, because it is the only branch that gates a release. - Revisit this record when the team grows or when feature branches get previews. # 0016: Turbopack build cache off in CI (/docs/decisions/0016-turbopack-build-cache-off-in-ci) GitHub Actions builds skip the Turbopack build cache, because a cold SaaS build with it runs the 7 GB runner out of memory. - Status: Accepted - Date: 2026-10-16 ## Context [#context] The repo standard restores `.next/cache` in every job that runs `next build`, so the Turbopack build cache (`turbopackFileSystemCacheForBuild`, on by default since Next.js 16.3) survives between runs. The repo is private, so CI runs on standard runners with 2 vCPU, about 7.9 GB of RAM, a 4 GB swap file and about 10 GB of free disk. Larger runners are out of scope. We turned the cache on in CI in pull request 96, still building Next apps one at a time (`--concurrency=1`). Three cold `saas` builds failed with "The runner has received a shutdown signal". A background step that logged memory every 15 seconds showed RAM at 7.8 of 7.9 GB and swap at 3 GB within three minutes of the build starting, followed by the shutdown. Disk stayed at about 62 of 73 GB used. The same cold build without the cache finishes on this runner in about 15 minutes. A larger swap file does not fit: the free disk also has to hold the build outputs and the cache itself, and a 16 GB swap file filled the disk before the build started. ## Decision [#decision] `turbopackFileSystemCacheForBuild` stays on for local and Vercel builds and is off when `GITHUB_ACTIONS` is `true`. `GITHUB_ACTIONS` is in `globalPassThroughEnv`, so CI still shares the Turbo Remote Cache with local and Vercel builds. CI has no `.next/cache` restore or save step, because nothing writes that cache there. The Build job keeps a background step that logs memory and disk usage. ## Consequences [#consequences] - Even without the cache, a cold build peaks at about 7.7 GB of RAM and 2.5 GB of swap, so the Build job has little headroom left. A memory failure there will show in the memory log before it becomes a mystery shutdown. - A Turbo cache miss on `saas` costs a full cold build in CI (about 15 minutes). The Turbo Remote Cache still skips the build entirely when the app's inputs are unchanged. - Revisit this record when the Build job moves to a runner with at least 16 GB of RAM, or when a Next.js release lowers the memory the build cache needs. Then turn the cache on, restore and save `apps/*/.next/cache` in the Build job (save only on `develop`), and measure again with the memory log. # 0017: Toolchain exceptions to the repo standard (/docs/decisions/0017-toolchain-exceptions) The classic TypeScript alias, the default checker count, and the standard toolchain items we hold back until upstream catches up. - Status: Accepted - Date: 2026-10-16 ## Context [#context] The repo standard asks for TypeScript 7 everywhere, `tsc --noEmit` at its defaults, Turborepo's `global` configuration block, `isolatedDeclarations` in the library preset, `ensureStatic = "navigation"` on the docs and marketing layouts, and the newest Ultracite release that clears the release-age gate. A few of these do not work in this repo yet, and one tool still needs the classic compiler API. ## Decision [#decision] - **Classic TypeScript alias.** `@hey-api/openapi-ts` and `react-doctor` call the TypeScript 6 JavaScript compiler API, which TypeScript 7 does not ship. The catalog aliases it as `typescript-classic: npm:@typescript/typescript6@`. Only `@workspace/api-client` depends on it, and its `register-typescript-classic.mjs` hook resolves `typescript` to the alias for `openapi-ts` and for `pnpm doctor:react`. Repo code never imports it. - **Default checker count.** Typecheck scripts run `tsc --noEmit` without `--checkers`. On the SaaS app with `--incremental false`, one checker took 10.2 s, the default 4.7 s, four checkers 4.55 s and eight checkers 3.5 s. The default is already four, and eight costs memory on the 2 vCPU CI runners for about a second locally. - **Top-level Turbo keys.** `turbo.json` keeps `globalEnv`, `globalPassThroughEnv`, `globalDependencies` and the other top-level keys instead of the `global` block, and leaves the `globalConfiguration` future flag off. `eslint-plugin-turbo` 2.11.8 reads only `globalEnv`, so with the `global` block every `process.env.NODE_ENV` read fails `turbo/no-undeclared-env-vars`. Switch when the plugin reads `global.env`. - **No `isolatedDeclarations`.** Every workspace package exports TypeScript source with `noEmit`, and nothing emits declarations, so the option would only add explicit return types to every export without changing a build. Add it to `library.json` when a package starts publishing built output. - **Docs layout at `ensureStatic = "shell"`.** The docs nav reads the brand from the request host behind a `` boundary, so the complete route cannot be static. The App Shell can, and `"shell"` keeps it that way. Marketing has no request-time data and uses `"navigation"`. - **Generated Zod schemas stay `.ts`.** The standard emits large generated metadata as `.js` plus a `.d.ts`. `packages/api-client/src/generated/zod.gen.ts` (about 20,000 lines) is runtime schema code rather than an inert literal, and a matching `.d.ts` would have to come from `tsc --declaration`, which the source-only, `noEmit` package model above does not run. A full check of `@workspace/api-client` takes 0.65 s and of `apps/api` 2.2 s. Switch when the api-client check passes 3 s or a `typecheck:perf` gate exists. - **Supabase start in `database.yml` is a foreground step.** The standard starts it in the background while dependencies install. The CLI and the signing key script come from the workspace install, so overlapping them needs a second Supabase CLI pin outside the catalog. The start takes about 28 s of a 7-minute job, most of which is the 4.4-minute pgTAP run. Switch if the start grows past a minute or the catalog pin can be shared. - **Ultracite stays on 8.0.0.** Ultracite 8.0.1 adds `anti-slop/require-readable-spacing` and `anti-slop/no-array-filter-map`. The spacing autofix inserts enough blank lines to push 23 components past `react-doctor/no-giant-component` (300 lines). Splitting those components is its own change; take 8.0.1 with it. Dependabot ignores `ultracite` until then, so remove that entry from `.github/dependabot.yml` in the same change. ## Consequences [#consequences] - `pnpm openapi:generate` and `pnpm doctor:react` keep working without a TypeScript 6 install anywhere else. Drop the alias once both tools run on the TypeScript 7 API. - Each held-back item has a named trigger above. When it fires, make the change and mark the item done here. - `docs:drift` fails on the retired `typescript6` script names and on `--checkers 4` in docs and rules. # 0018: Data conventions for money, rates and file deletion (/docs/decisions/0018-data-conventions) Invoice and quote amounts stay numeric for EN 16931, VAT rates stay percentages, and Files keep a soft delete with restore. - Status: Accepted - Date: 2026-10-16 ## Context [#context] The repo standard stores money as integer minor units (`*_cents bigint`), rates as integer basis points, and adds `deleted_at` only with an ADR. CentraKit's documents follow EN 16931 and Peppol BIS, which carry line quantities and unit prices with up to four decimals and round totals to two. About 200 columns use `numeric(14, 4)` or `numeric(14, 2)`, and VAT rates are `numeric(5, 2)` percentages such as `21.00`. Files have a trash with restore and a purge job. ## Decision [#decision] - Document money on quotes, invoices, purchase orders and templates stays `numeric`: `numeric(14, 4)` for quantities, unit prices and discounts, and `numeric(14, 2)` for document totals. The contract carries them as numbers, and rounding to two decimals happens once, at the document total. - Payment balances that Stripe settles (`amount_due_cents`, `amount_paid_cents`, invoice charges, payment intents) stay integer cents, because Stripe works in minor units. - VAT and other tax rates stay `numeric(5, 2)` percentages, since EN 16931 (BT-152) expresses the VAT rate as a percentage. Platform fees use basis points (`application_fee_bps`). - `file_nodes.deleted_at` is the one soft delete. It backs the Files trash, a check constraint keeps `deleted_at`, `deletion_batch_id` and `purge_at` in step, and partial indexes filter on `deleted_at is null`. Other tables archive with `archived_at` or delete. ## Consequences [#consequences] - E-invoice exports map columns to UBL without converting units. - Code that crosses from a document total to a Stripe amount converts explicitly to cents at that boundary. - A new money column picks one of the two shapes above and names it accordingly (`*_cents` for integer minor units). Revisit if Peppol or EN 16931 drop decimal line amounts, or if a payment provider accepts decimal amounts. # 0019: Supabase-backed jobs, rate limits, flags and storage (/docs/decisions/0019-supabase-backed-platform-needs) Jobs, rate limits, feature flags and file storage run on the better-supabase modules and Supabase Storage instead of Vercel Queues, Firewall rules, Vercel Flags and Blob. - Status: Accepted - Date: 2026-10-16 ## Context [#context] The repo standard maps durable fan-out to Vercel Queues, rate limits to Vercel Firewall, feature flags to Vercel Flags and assets not bound to RLS to Vercel Blob. CentraKit adopted better-supabase (ADR 0009), whose modules already provide typed jobs on pgmq (`module_07_jobs`), per-tenant rate limits (`module_17_rate_limit`) and flags (`module_32_flags`), all inside the same database, RLS and audit trail as the data they guard. Almost every file in the product belongs to a tenant and is read through RLS. ## Decision [#decision] - Background jobs use the better-supabase jobs module on Supabase Queues (pgmq), drained by the API's job route and pg_cron. Durable multi-step runs still use the Workflow SDK. - Per-tenant and per-key rate limits use the better-supabase rate limit module through `packages/core/src/rate-limit.ts`. Vercel Firewall stays the edge layer for abusive traffic and bots. - Feature flags that depend on the organization, plan or role use the better-supabase flags module, so RLS and SQL can read them. Vercel Flags is not adopted. - Files live in Supabase Storage with object paths in `*_path` columns. Vercel Blob is not used. ## Consequences [#consequences] - One database holds the job, limit and flag state with its tenant, so tests and pgTAP cover them like any table. - Queue throughput and flag reads cost database capacity. Revisit when a job type needs more throughput than pgmq gives on the current compute size, or when a flag must change pages that are otherwise static. # 0020: German as a third app locale (/docs/decisions/0020-german-locale) The SaaS app ships English, Dutch and German, with formal German copy. - Status: Accepted - Date: 2026-10-16 ## Context [#context] The repo standard ships `en` as the source locale and `nl` as the one translation. CentraKit's brands sell in Germany, Austria and Switzerland as well as the Netherlands and Belgium. ## Decision [#decision] - The app locales are `en`, `nl` and `de`, and every `.po` catalog stays complete for all three. - German copy uses the formal Sie. Dutch stays informal (`je` and `jouw`). - Customer emails pick the locale from the recipient's country, and keep Switzerland and Luxembourg on English. The API, MCP, CLI and logs stay English. ## Consequences [#consequences] - Every copy change needs a German translation before it lands, which `tests/integration/saas/i18n-catalog.test.ts` enforces. - Adding a fourth locale follows the same steps and needs no new record. # 0021: Shaped loading files for cold navigation (/docs/decisions/0021-loading-files-for-cold-navigation) `apps/saas` keeps a `loading.tsx` per route group that reuses the feature skeleton, for cold hard navigations. - Status: Accepted - Date: 2026-10-16 ## Context [#context] The standard prefers page-level Suspense boundaries over `loading.tsx` and keeps `loading.tsx` only as the fallback for cold hard navigations. `nextjs-app-architecture` asks for shaped loading states. CentraKit's page shells are synchronous and wrap async feature components in ``, but a cold navigation into a dynamic tenant route still waits for the layout without a fallback. ## Decision [#decision] Each tenant route segment in `apps/saas` keeps a `loading.tsx` that renders the same skeleton as its page's Suspense fallback. It is never a generic spinner, and new routes add one with the page. ## Consequences [#consequences] - Cold navigations show the final layout at once, and warm navigations still stream through the page boundaries. - A skeleton change touches both the page fallback and `loading.tsx`, which is why they share one component. Revisit when Partial Prefetching covers cold navigations to dynamic segments. # 0022: verify:affected on pre-commit (/docs/decisions/0022-pre-commit-verify-gate) The pre-commit hook runs `pnpm verify:affected` after the staged-file fix, not only the pre-push hook. - Status: Accepted - Date: 2026-10-16 ## Context [#context] The standard's pre-commit hook only formats and fixes staged files, and the type-aware lint, typecheck and tests run once per push. CentraKit commits straight to `develop` (ADR 0015), so there is no pull request between a commit and the shared branch, and agents commit in small batches that are pushed later. ## Decision [#decision] Lefthook runs the Ultracite fix on staged files and then `pnpm verify:affected` on pre-commit, and the same gate again on pre-push. Turbo's cache makes the push re-run near instant. ## Consequences [#consequences] - A commit that lands can always be pushed, and a broken commit never sits in local history waiting for the push. - Commits take longer, between a few seconds with a warm cache and a few minutes after a dependency change. Revisit if the team moves to one pull request per change. # 0023: Vitest through Turbo tasks (/docs/decisions/0023-vitest-through-turbo) Each workspace runs its own Vitest config through Turbo `test` and `test:unit` tasks instead of one root Vitest run with projects. - Status: Accepted - Date: 2026-10-16 ## Context [#context] The standard runs Vitest from a root `vitest.config.ts` whose `test.projects` reference each workspace. CentraKit has more than forty workspaces with their own environments (Node, DOM, workflow, Supabase), and CI shards the integration suite. Turbo already caches and filters by affected workspace. ## Decision [#decision] - `pnpm test` and `pnpm test:unit` run `turbo run test` and `turbo run test:unit`. Each workspace's `vitest.config.ts` is the project that runs. - The root `vitest.config.ts` stays for editor integrations and for running one file from the root. - Each workspace sets `fsModuleCachePath: "node_modules/.vitest-cache"`, so parallel workspaces never share Vitest's module cache directory. ## Consequences [#consequences] - `--affected` and Turbo's cache skip unchanged workspaces locally and in CI. - Cross-workspace coverage needs a merge step. Revisit when Vitest projects support per-project caching that matches Turbo's. # 0024: Typecheck in its own CI job (/docs/decisions/0024-separate-typecheck-job) CI runs typecheck as its own job with `--concurrency=1` instead of inside the single static job. - Status: Accepted - Date: 2026-10-16 ## Context [#context] The standard runs lint, typecheck, Knip and the drift checks as one Turbo call inside one static job. On the 2 vCPU runners, `tsgo` typechecking the SaaS app next to other workspaces ran out of memory, and running it next to lint doubled the wall time of the static job. ## Decision [#decision] The `verify` workflow has a `typecheck` job that runs `pnpm typecheck:tooling` and then `turbo run typecheck --concurrency=1`, in parallel with the `static`, `test` and `integration` jobs. ## Consequences [#consequences] - Typecheck no longer fails from memory pressure, and the static job stays short. - The extra job spends one more checkout and install. Revisit when larger runners are available or `tsgo` memory use drops enough to run four workspaces at once. # 0025: Package-owned env modules (/docs/decisions/0025-package-owned-env-modules) Packages that read configuration own a validated `env.ts`, in addition to one `env.ts` per app. - Status: Accepted - Date: 2026-10-16 ## Context [#context] The standard gives each app one `env.ts` as the only file that reads `process.env`. CentraKit's request-scoped wiring lives in packages (`core`, `core-ai-runtime`, `supabase-runtime`, `stripe`, `email`, `next-config`, `foundation`), which run inside several apps and the CLI. Threading every key through each app's `env.ts` would duplicate the schema in four places. ## Decision [#decision] - A package that needs configuration exports a lazily validated env module from its own `src/env.ts`, built with t3-env, and is the only place in that package that reads `process.env`. - Apps keep their own `env.ts` for app-only keys, and never re-declare a key a package owns. - `turbo.json` lists each key in the env of every task that builds an app using the package. ## Consequences [#consequences] - A key is declared once, next to the code that reads it, and every app that uses the package validates it the same way. - A missing key fails when the package first reads it rather than when the app starts, so packages validate at first use and tests reset the cache. # 0026: Workflows started outside apps/api (/docs/decisions/0026-workflows-started-from-runtime-packages) Workflow runs start from the shared runtime packages, which run inside both `apps/saas` and `apps/api`. - Status: Accepted - Date: 2026-10-16 ## Context [#context] The standard says only `apps/api` starts workflows. CentraKit's assistant chat streams from the SaaS app, and its `WorkflowAgent` runs and the automation dispatcher live in `packages/core-ai-runtime` and `packages/core-workflow-runtime`. Routing every chat turn through the API would add a network hop to each streamed response. ## Decision [#decision] - `start()` from `workflow/api` is called only in `packages/core-ai-runtime` and `packages/core-workflow-runtime`, plus the job handlers in `apps/api/lib/jobs`. - Both apps host the workflow routes, and each sets its own `WORKFLOW_QUEUE_NAMESPACE`. ## Consequences [#consequences] - Chat stays one hop from the browser, and automations and chat share one runtime. - Workflow deployments span two apps, so a workflow change ships with both. Revisit when the API moves to the oRPC follow-up and can host chat streaming without the extra hop. # 0027: React Compiler memoization policy (/docs/decisions/0027-react-compiler-memoization) The React Compiler memoizes components; hand-written memoization stays only where React Doctor or a library needs it. - Status: Accepted - Date: 2026-10-16 ## Context [#context] The standard allows no hand-written `useMemo`, `useCallback` or `memo`. The React Compiler is on in the SaaS app, but TanStack Table column definitions must keep a stable identity, and extracted copy computed from `t()` calls is easier to read in one `useMemo`. ## Decision [#decision] - Column definitions for TanStack Table stay wrapped in `useMemo`, as `architecture-ui-patterns.mdc` says. - Other hand-written memoization is removed when React Doctor (`pnpm doctor`) flags it as dead weight, not in a bulk sweep. - New code does not add `useMemo`, `useCallback` or `memo` unless a library requires a stable reference. ## Consequences [#consequences] - The compiler handles most memoization, and the remaining hooks each have a reason. - Some legacy hooks stay until Doctor reports them. Revisit when TanStack Table no longer needs stable column identity. # 0028: Path-scoped architecture rule (/docs/decisions/0028-path-scoped-architecture-rule) The architecture rule attaches to TypeScript files, and the AGENTS.md invariants are the always-on summary. - Status: Accepted - Date: 2026-10-17 ## Context [#context] The repo standard makes three rules always-on: `architecture`, `writing` and `git-workflow`, and path-scopes everything else. CentraKit's `architecture.mdc` is about 10.7 KB, close to the size of `AGENTS.md` itself, while `writing` and `git-workflow` are about 1 KB each. Most of it only matters when editing TypeScript: the SaaS feature-slice layout, the naming table and the cache-key rules. `AGENTS.md` already lists the thirteen architecture invariants, which every session loads. ## Decision [#decision] `architecture.mdc` attaches to `apps/**` and `packages/**` TypeScript files instead of loading always. The numbered invariants in `AGENTS.md` are the always-on summary, and the rule holds the detail. `learned-product.mdc` is always-on next to `writing` and `git-workflow`, because it carries the white-label and locale facts that apply to copy, SQL and docs as much as to code. ## Consequences [#consequences] - A session that only touches docs, SQL or workflows loads about 10 KB less context and still sees the invariants. - A change to an invariant updates both `AGENTS.md` and `architecture.mdc` in the same commit. - Revisit if sessions that start outside TypeScript files keep breaking an architecture rule that the invariants do not cover. Move that rule into the invariants first, before making the whole file always-on. # 0029: ISO 8601 strings in contracts (/docs/decisions/0029-iso-strings-in-contracts) Contract schemas carry timestamps and dates as ISO 8601 strings and parse them to Temporal where code compares or computes. - Status: Accepted - Date: 2026-10-17 ## Context [#context] The repo standard sends timestamps as ISO 8601 strings and has the domain schema transform them to `Temporal.Instant`, so services work with Temporal values. CentraKit's contract schemas are Zod schemas shared by the API, the MCP server, the generated OpenAPI document and the SaaS forms. A transform would change their output type from `string` to `Temporal.Instant`, which `next-openapi-gen` cannot describe, Server Components cannot pass to Client Components, and `structuredContent` cannot carry. ## Decision [#decision] Contract schemas keep timestamps as `z.iso.datetime({ offset: true })` strings and calendar days as `YYYY-MM-DD` strings. Code that compares, sorts or computes parses them at that point with the helpers in `@workspace/foundation/temporal` (`parseInstant`, `parsePlainDate`, `addIsoDays`) and compares with `Temporal.Instant.compare` or `Temporal.PlainDate.compare`. `Date` stays out of contract and service code, as the standard says, except where a third-party API demands one. ## Consequences [#consequences] - The same schema validates the wire, the OpenAPI document and the forms, with no second set of wire schemas. - Every comparison parses again. That costs little at the list sizes the app renders. - Revisit with the Valibot migration in ADR 0004, which would allow separate wire and domain schemas. # 0030: Stripe Connect on Accounts v2 with direct charges (/docs/decisions/0030-stripe-connect-accounts-v2-direct-charges) Tenant invoice payments run as direct charges on Accounts v2 connected accounts, with Stripe as requirements and losses collector and onboarding embedded in the app. - Status: Proposed - Date: 2026-10-21 ## Context [#context] Organizations send invoices to their own customers, for example a garage invoicing a car owner. The customer should pay online, see the garage on the payment page and on the bank statement, and the money should land with the garage. The organization owner should have to set up as little as possible. ScaleDock earns a platform fee per payment next to the subscription and credits it already bills through Stripe Billing (ADR 0010). Today CentraKit creates legacy Express connected accounts, onboards them through hosted Account Links and charges invoices as destination charges with `transfer_data.destination` and `application_fee_amount`, without `on_behalf_of`. Express accounts carry `losses_collector: application` and `fees_collector: application_express`. That makes ScaleDock the merchant of record: - The customer sees ScaleDock, not the garage, in the iDEAL | Wero flow and on the bank statement, while the invoice comes from the garage. iDEAL requires the name the customer sees to be the business they pay. - Refunds, disputes, negative balances and Stripe's fees hit the platform. - Stripe marks the v1 `type` parameter as deprecated and no longer recommends Accounts v1 for new Connect integrations. The minimum an organization owner must provide is set by law, not by Stripe. Under the EU anti-money-laundering rules (in the Netherlands Wwft article 3, for Stripe's Irish entities the Irish transposition of the same directive) the payment provider must identify and verify the business, its ultimate beneficial owners and the authority and identity of its representative. For a Dutch account with `card_payments`, Stripe asks for the MCC and website; for a company also its registered name, address, phone and KvK number, the representative's name, date of birth, home address, email and phone, and owners and directors for a BV or VOF; for a sole proprietor onboarded as an individual their own details, with a KvK number once iDEAL is requested. Every account accepts the Stripe terms and adds a bank account for payouts. Every PSP asks for the same set, so "only an IBAN" is not possible. Stripe holds the funds in both the current and the new construction, but with direct charges the organization is the merchant of record and Stripe's contracting party, which is the position the PSD2 check below needs to confirm. ## Decision [#decision] 1. **Accounts v2, one account per organization.** Each organization gets one `v2/core/accounts` account. The `customer` configuration carries its plan subscription, credits and overage with ScaleDock (ADR 0010), and the `merchant` configuration is added when it turns on online payments. The organization pays ScaleDock from its own bank account or card (SEPA Direct Debit or card), never from its Stripe balance; the `customer` configuration only replaces the separate v1 Customer object. 2. **Responsibilities.** `defaults.responsibilities.losses_collector` and `fees_collector` are `stripe`. Stripe collects and verifies the requirements, carries negative balances and charges its processing and dispute fees to the organization. The application fee is ScaleDock's margin only, and the platform pays no per-account or per-payout Connect fees. The platform fee starts as a fixed 1 EUR per payment, excluding VAT, through the existing `invoice_payment_fee` setting. 3. **Dashboard.** `dashboard: none`. Organizations never use a Stripe Dashboard. CentraKit renders Connect embedded components instead: `account_onboarding`, `notification_banner` and `account_management`, which Stripe requires when it is losses collector, plus `payment_details` (refunds and disputes for one payment, opened from CentraKit's own payments list) and `payouts`. Refunds started in the `payment_details` component do not refund the application fee, so CentraKit applies its refund policy for the fee itself. `dashboard: express` with Stripe as losses collector is a public preview, so it is not used. 4. **Direct charges.** The invoice payment is a PaymentIntent created on the connected account (`Stripe-Account` header) with `application_fee_amount`. The organization is the merchant of record, its name appears in the payment flow and on the statement, and refunds and disputes run against its balance. 5. **Payment page.** The customer portal invoice page keeps the Payment Element, initialized with the connected account. A Stripe Checkout link on the connected account is an option for a plain payment link outside the portal, not the default. 6. **Prefill.** CentraKit creates the account with everything it already knows (registered name, address, phone, KvK and VAT number, website, MCC from the brand vertical, contact email) and lets the embedded onboarding ask only for the rest. The legal form comes from the onboarding until CentraKit stores one. Personal data such as the representative's date of birth and home address is entered in Stripe's component and never stored by CentraKit. 7. **No migration.** No organization has a subscription or a connected account in production yet, so the v1 Customer and Express code paths are replaced, not kept side by side. Test data in the sandbox is discarded. As a guard for any v1 rows that do exist, the data migration `20261022190000_retire_legacy_connect_accounts.sql` removes connected accounts that never received a v2 snapshot (no card payments status and no customer configuration) and cancels their open online payments, so the organization activates online payments again on Accounts v2. Destination charges from v1 keep their payment rows. Their payment, refund and dispute events still arrive on the platform endpoint, which looks the PaymentIntent up among stored payments whose account is no longer a payment account and applies the event through the same handler as the Connect endpoint, so a payment that was still processing at deploy settles the invoice and later refunds and disputes are recorded. ## Consequences [#consequences] - The organization owner completes onboarding inside CentraKit in a few minutes: confirm the prefilled business details, enter their date of birth and home address, add owners and directors for a BV or VOF, add an IBAN, accept the terms. Because Stripe collects the requirements, they sign in to Stripe with email and a one-time code in a popup when a component needs it (onboarding, account management, payouts and the notification banner). Stripe asks for an ID document or selfie only when automatic verification fails. - ScaleDock carries no negative balance or dispute liability for invoice payments and no longer sees the payment volume in its own balance; reporting reads per connected account. - Stripe does not invoice application fees. ScaleDock issues its own VAT invoice for the platform fee. - CentraKit must build what the Stripe Dashboard would otherwise provide: the requirement banner, account details, payment list with refunds and disputes, and payouts. - Webhooks need a snapshot endpoint with the Connected accounts scope for direct-charge, refund, dispute and payout events, and a thin event destination with the Your account scope for v2 account events; both have their own signing secret. - iDEAL requires the organization's website to show its KvK number and at least two contact details. Every organization gets a public business page in CentraKit that shows them, and its URL is the account's business URL when the organization has no website of its own. - PRODUCT.md, the invoice and Stripe operations docs and the platform settings docs stop describing ScaleDock as merchant of record. - The VAT role and the PSD2 position get a legal check before production. ## Alternatives considered [#alternatives-considered] - **Keep destination charges, add `on_behalf_of`.** The garage name reaches the statement, but the platform still carries refunds, disputes and Stripe fees, and onboarding needs the same `card_payments` requirements. No gain for the organization. - **Platform as requirements collector** (`losses_collector: application`, which also forces `fees_collector: application`, with `dashboard: none`). Allows onboarding without a Stripe sign-in, but ScaleDock becomes liable for negative balances and fraud, processes identity data itself and must keep up with every KYC requirement change. - **Mollie Connect for Platforms.** The same legal KYC set, onboarding happens in Mollie's own app outside CentraKit, and ID upload is asked up front. Similar or more effort for the organization and less control for the product. - **Adyen for Platforms.** Enterprise contracts and minimums do not fit the current stage. # Architecture decision records (/docs/decisions/README) Why CentraKit differs from the shared repo standard, and which migrations are deferred. Architecture decision records (ADRs) capture choices that a future contributor could otherwise mistake for drift. Each record states the context, the decision, and the consequences. ## When to add a record [#when-to-add-a-record] Add a record when you: - Keep a deliberate deviation from the shared repo standard. - Replace a library in the one-library-per-concern list. - Add a vendor that is not a first-party Vercel product, a Vercel Marketplace integration, or a Vercel Connect connector. - Defer a standard item to a later plan. Copy `0000-template.md`, take the next free number, and add the file to `meta.json`. ## Records [#records] | Number | Title | Status | | ------ | --------------------------------------------- | ------------------ | | 0001 | Next.js route handlers for the API and MCP | Accepted | | 0002 | Bounded package topology | Accepted | | 0003 | The `apps/saas` name and host-based routing | Superseded by 0011 | | 0004 | Zod until the API layer migration | Accepted | | 0005 | Audit log naming | Accepted | | 0006 | Env file layout | Accepted | | 0007 | Deferred better-supabase and PermDock | Superseded by 0009 | | 0008 | CLI on citty, clack and c12 | Accepted | | 0009 | Adopt better-supabase and PermDock from npm | Accepted | | 0010 | Billing on the Stripe Sync Engine | Accepted | | 0011 | Multi-brand path routing | Accepted | | 0012 | The Data API for module access | Accepted | | 0013 | Ultracite for Oxlint and Oxfmt | Accepted | | 0014 | Declarative Schemas 2.0 and a one-time reset | Accepted | | 0015 | Direct-to-develop workflow | Accepted | | 0016 | Turbopack build cache off in CI | Accepted | | 0017 | Toolchain exceptions to the repo standard | Accepted | | 0018 | Data conventions for money, rates and files | Accepted | | 0019 | Supabase-backed jobs, limits, flags, storage | Accepted | | 0020 | German as a third app locale | Accepted | | 0021 | Shaped loading files for cold navigation | Accepted | | 0022 | `verify:affected` on pre-commit | Accepted | | 0023 | Vitest through Turbo tasks | Accepted | | 0024 | Typecheck in its own CI job | Accepted | | 0025 | Package-owned env modules | Accepted | | 0026 | Workflows started outside `apps/api` | Accepted | | 0027 | React Compiler memoization policy | Accepted | | 0028 | Path-scoped architecture rule | Accepted | | 0029 | ISO 8601 strings in contracts | Accepted | | 0030 | Stripe Connect on Accounts v2, direct charges | Proposed | # Docs for AI agents (/docs/developers/ai-agents) How AI agents and LLM tools read the CentraKit docs through llms.txt, Markdown page URLs, the docs MCP server and Ask AI, and how they act on CentraKit data through the product MCP server. Every page of these docs is available as Markdown, and the site has an index, a full-text export and an MCP server for agents. Use this page to point an assistant, a coding agent or a retrieval pipeline at the docs, and to choose between reading docs and acting on CentraKit data. The docs live under `/docs` on each brand's app host. The examples use `app.centrakit.com`; every other brand host, such as `app.mechup.com`, serves the same paths. ## Choose an entry point [#choose-an-entry-point] | You want to | Use | | --- | --- | | Give an agent a map of the docs | `https://app.centrakit.com/llms.txt` | | Load all docs into one context | `https://app.centrakit.com/llms-full.txt` | | Fetch one page as Markdown | The page URL with `.md`, or `Accept: text/markdown` | | Let an agent search and read docs on demand | The docs MCP server at `https://app.centrakit.com/docs/mcp` | | Ask a question in the browser | Ask AI on any docs page | | Read or change customers, tasks, quotes and orders | The product MCP server at `https://app.centrakit.com/mcp` | ## llms.txt and llms-full.txt [#llmstxt-and-llms-fulltxt] [`/llms.txt`](/docs/llms.txt) is a plain-text index of every page with its title, URL and description. It is also served at `/docs/llms.txt`, and its page links carry the `/docs` prefix. It is small enough to put in a system prompt, and agents use it to pick the pages to fetch. [`/llms-full.txt`](/docs/llms-full.txt) is the Markdown of every page in one file. It is large, so prefer the index plus single pages unless you are building a retrieval index or the model has a large context window. Both cover the guides, the OpenAPI operation pages and the changelogs. ## Markdown pages [#markdown-pages] Every page has a Markdown version. Add `.md` or `.mdx` to the page path: ```bash curl https://app.centrakit.com/docs/developers/mcp.md curl https://app.centrakit.com/docs/developers/mcp/index.mdx ``` Or ask for Markdown on the normal URL with the `Accept` header: ```bash curl -H "Accept: text/markdown" https://app.centrakit.com/docs/developers/mcp ``` When a request prefers `text/markdown`, the docs site returns the Markdown instead of HTML and sets `Vary: Accept`, so caches keep both versions apart. Responses use `Content-Type: text/markdown; charset=utf-8`. Each Markdown page starts with `# (<url>)`, where the URL carries the `/docs` prefix, and the page description, followed by the page body. OpenAPI operation pages contain only that page's operations and the component schemas they reference, not the whole specification. ## Page actions [#page-actions] Every docs page has two buttons under its title: - **Copy Markdown** copies the page's Markdown to the clipboard, ready to paste into a chat. - **Open** shows a menu with **Open in GitHub** (the page source), **View as Markdown**, **Open in Scira AI**, **Open in ChatGPT**, **Open in Claude** and **Open in Cursor**. The AI options open the tool with a prompt that points it at the page's Markdown URL. ## Docs MCP server [#docs-mcp-server] The docs MCP server lets an agent search and read the docs as tools instead of fetching URLs. It is public, read-only and needs no authentication. | Environment | Endpoint | | ----------- | ------------------------------------ | | Production | `https://app.centrakit.com/docs/mcp` | | Local | `https://docs.localhost/docs/mcp` | It has three tools: | Tool | Input | Returns | | --- | --- | --- | | `list_pages` | none | The page index, with page URLs such as `/developers/mcp`. | | `search` | `query`, optional `locale` | Matching sections with their page URLs. | | `get_page` | `url`, a page pathname such as `/developers/mcp`. The `/docs/developers/mcp` form and full URLs also work. | The page as Markdown, or `page not found` for an unknown URL. | Add it to a client that supports remote MCP servers: ```json { "mcpServers": { "centrakit-docs": { "url": "https://app.centrakit.com/docs/mcp" } } } ``` For one-click installs in Cursor, VS Code and Claude Code, see the connect table on [MCP server](/docs/developers/mcp). ## Ask AI [#ask-ai] The floating **Ask AI** button on every docs page opens a chat panel. The assistant searches the docs with a `search` tool, answers from the results, and cites the pages it used as links. When the docs do not cover a question, it says so and suggests a better search. Ask AI uses the platform's default chat model, which administrators set in [AI settings](/docs/admin/ai-settings). It reads only the public docs and cannot see your organization data. ## Product MCP server [#product-mcp-server] To let an agent act on your CentraKit data, connect the product MCP server at `https://app.centrakit.com/mcp`, or `/mcp` on your brand's app host. It needs OAuth, runs with your permissions, and exposes 74 tools for tasks, customers, products, quotes, commerce, agenda, files, workflows, webhooks and the audit log. Mutations can be held by approval policies. See [MCP server](/docs/developers/mcp). A typical setup connects both servers: the docs server so the agent knows how CentraKit works, and the product server so it can do the work. ## For contributors [#for-contributors] The repository has instructions for coding agents that work on CentraKit itself: - `AGENTS.md` at the repository root describes the layout, commands, architecture invariants, conventions and hard rules. `CLAUDE.md` points to it. - `docs/agents/` holds notes on mistakes agents have repeated, one page per topic: the toolchain, git and release, the Supabase schema, tenancy and routing, translations, testing and writing docs. Agents load the page for the topic they are working on. - `.agents/rules/` and `.agents/skills/` hold rules and skills that attach to specific files or tasks. Docs pages are written from the shipped code. Before you change a page in this site, read [Writing docs](/docs/agents/writing-docs). ## Related pages [#related-pages] - [MCP server](/docs/developers/mcp) - [REST API](/docs/developers/api) - [OpenAPI](/docs/developers/openapi) - [Writing docs](/docs/agents/writing-docs) # REST API (/docs/developers/api) CentraKit REST API base URLs, authentication, organization scoping, pagination, and a map of every route under /api/v1. The CentraKit API app serves a versioned REST API under `/api/v1` and the product MCP server at `/mcp`. Both are paths on each brand's app host. The SaaS app, the CLI and MCP clients all call the same routes, with the same permission checks and the same error format. ## Overview [#overview] | Environment | Base URL | | ------------------- | ------------------------------- | | Production | `https://app.centrakit.com/api` | | Preview (`develop`) | `https://app.centrakit.dev/api` | | Local (Portless) | `https://app.localhost/api` | Every brand host serves the same API, for example `https://app.mechup.com/api`. Use the host of the brand your organization belongs to. The API moved from `https://api.centrakit.com/api`, which no longer exists. Every path in the OpenAPI spec starts with `/v1`, so a full URL looks like `https://app.centrakit.com/api/v1/organizations`. Keep `/api` in any base URL you configure, because the route handlers live under that prefix. Locally, run `pnpm dev:portless` to serve `https://app.localhost`, which forwards `/api` to the API app on `https://api.localhost`. `pnpm dev:api:portless` runs only the API, on `https://api.localhost/api`. ```bash curl https://app.centrakit.com/api/v1/health ``` ```json { "ok": true, "service": "api", "version": "v1" } ``` ## Authentication [#authentication] Routes accept either a Supabase browser session cookie or a bearer access token: ```http Authorization: Bearer <access-token> ``` When a request carries a bearer token, the API verifies its issuer, audience, signature and expiry before it runs the route. Without a bearer token, the API reads the Supabase session cookie. Use bearer tokens for scripts, CI, the CLI and MCP hosts. See [Authentication](/docs/developers/authentication) for how to obtain a token. With the CLI: ```bash CENTRAKIT_TOKEN=<access-token> centrakit orgs list ``` ## Organization scoping [#organization-scoping] Tenant data lives under `/api/v1/organizations/{organizationId}/...`, where `organizationId` is the organization UUID (not its slug). There is no current-organization header or cookie: every organization-scoped request names its organization in the path. To find the IDs you can use, call `GET /api/v1/organizations`. `PATCH /api/v1/users/me/active-organization` stores a preferred default organization on your profile for clients that need one, but it never replaces the organization ID in the path or any permission check. Each operation under `/organizations/{organizationId}` checks one organization permission before it reads or writes data. Reads require the matching `view` permission, and mutations require the operation's create, update, delete or lifecycle permission, for example `webhook.update` for rotating a webhook secret. The check runs the same way for cookie and bearer requests: - An unauthenticated request gets `401`. - An authenticated user without the permission gets `403` with code `ORGANIZATION_PERMISSION_DENIED`. Row Level Security in the database still applies on top of this check. See [RBAC](/docs/architecture/rbac) and [Tenancy](/docs/architecture/tenancy) for the permission model. ## Pagination [#pagination] Most list endpoints use offset pagination with these query parameters: | Parameter | Default | Limits | Meaning | | --- | --- | --- | --- | | `offset` | `0` | 0 or more | Number of items to skip. | | `limit` | `50` | 1 to 100 | Page size. | | `q` | none | 200 characters | Free-text search, on endpoints that support it. | A list response wraps its items in `data` and reports the range in `pagination`: ```json { "data": [{ "id": "..." }], "pagination": { "offset": 0, "limit": 50, "total": 1240, "hasMore": true, "totalIsEstimate": true } } ``` - `hasMore` is the reliable signal for whether another page exists. Request the next page with `offset` plus `limit`. - `total` is exact unless `totalIsEstimate` is `true`. Large collections such as tasks, quotes, invoices, customer assets, file drives, organizations and members use an estimated count by default, so treat `total` as approximate when the flag is set. A few endpoints use their own limits. Collaboration reference search defaults to 20 results (maximum 50), command center search to 24 (maximum 40), inbox threads to 30 (maximum 100) and the notification inbox to 10 (maximum 20). Chat message lists count `offset` from the newest message window. The OpenAPI reference lists the parameters for each operation. ## Idempotent retries [#idempotent-retries] Send an `Idempotency-Key` header (at most 255 characters, such as a UUID) with a `POST`, `PUT`, `PATCH` or `DELETE` to make a retry safe. The API stores the first response for 24 hours per signed-in user and key: | Retry | Response | | --- | --- | | Same key and same method, path and body | The stored response, with `idempotency-replayed: true` | | Same key while the first request is still running | `409` with code `IDEMPOTENCY_KEY_IN_USE` and `retry-after` | | Same key with another body | `422` with code `IDEMPOTENCY_KEY_REUSED` | A request that fails with a `5xx` status releases its key, so the client can retry it with the same key. Requests without the header run as before. ## Organization resource map [#organization-resource-map] Paths in these tables are relative to `/api/v1/organizations/{organizationId}`. Operation names, request bodies and response schemas are in the generated reference described on [OpenAPI](/docs/developers/openapi). ### Organization, users and teams [#organization-users-and-teams] | Path | Methods | | --------------------------------------- | ------------------ | | (the organization itself) | GET, PATCH, DELETE | | `/members` | GET, POST (invite) | | `/members/{userId}` | GET, PATCH (role) | | `/invitations` | GET | | `/invitations/{invitationId}` | GET, PATCH, DELETE | | `/teams` | GET, POST | | `/teams/{teamId}` | GET, PATCH, DELETE | | `/teams/{teamId}/members` | PUT | | `/branding/logo` | POST, DELETE | | `/branding/brand-images` | POST | | `/branding/brand-images/{brandImageId}` | DELETE | ### Customers and assets [#customers-and-assets] | Path | Methods | | --- | --- | | `/customers` | GET, POST | | `/customers/{customerId}` | GET, PATCH, DELETE | | `/customers/{customerId}/contacts` | POST | | `/customers/{customerId}/contacts/{contactId}` | GET, PATCH, DELETE | | `/customers/{customerId}/contacts/{contactId}/primary` | POST | | `/customers/{customerId}/locations` | POST | | `/customers/{customerId}/locations/{locationId}` | GET, PATCH, DELETE | | `/customers/{customerId}/locations/{locationId}/primary` | POST | | `/customers/{customerId}/notes` | POST | | `/customers/{customerId}/notes/{noteId}` | PATCH, DELETE | | `/customer-assets` | GET, POST | | `/customer-assets/{customerAssetId}` | GET, PATCH, DELETE | | `/customer-assets/{customerAssetId}/notes` | GET, POST | | `/customer-assets/{customerAssetId}/notes/{noteId}` | PATCH, DELETE | ### Catalog and content [#catalog-and-content] | Path | Methods | | ------------------------------------------ | ------------------ | | `/products` | GET, POST | | `/products/{productId}` | GET, PATCH, DELETE | | `/products/{productId}/suppliers` | POST | | `/products/{productId}/suppliers/{linkId}` | PATCH, DELETE | | `/product-categories` | GET, POST | | `/product-categories/{categoryId}` | GET, PATCH, DELETE | | `/product-categories/{categoryId}/move` | POST | | `/suppliers` | GET, POST | | `/suppliers/{supplierId}` | GET, PATCH, DELETE | | `/brands` | GET, POST | | `/brands/{brandId}` | GET, PATCH, DELETE | | `/tags` | GET, POST | | `/tags/{tagId}` | GET, PATCH, DELETE | | `/templates` | GET, POST | | `/templates/{templateId}` | GET, PATCH, DELETE | Templates cover quote, invoice and task templates. ### Inventory [#inventory] | Path | Methods | | ------------------------------------------ | ----------------------- | | `/inventory/levels` | GET | | `/inventory/locations` | GET, POST | | `/inventory/stock-records` | GET, POST | | `/inventory/stock-records/{stockRecordId}` | PATCH, DELETE (archive) | | `/inventory/stock-adjustments` | POST | | `/inventory/movements` | GET | | `/inventory/allocations` | GET | ### Quotes [#quotes] | Path | Methods | | ------------------------------------- | ------------------ | | `/quotes` | GET, POST | | `/quotes/{quoteId}` | GET, PATCH, DELETE | | `/quotes/{quoteId}/send` | POST | | `/quotes/{quoteId}/accept` | POST | | `/quotes/{quoteId}/reject` | POST | | `/quotes/{quoteId}/duplicate` | POST | | `/quotes/{quoteId}/assets` | PUT | | `/quotes/{quoteId}/tasks` | PUT | | `/quotes/{quoteId}/lifecycle` | GET | | `/quotes/{quoteId}/lifecycle/advance` | POST | | `/quotes/{quoteId}/notes` | GET, POST | | `/quotes/{quoteId}/notes/{noteId}` | PATCH, DELETE | See [Quotes](/docs/product/quotes). ### Invoices [#invoices] | Path | Methods | | ----------------------------------------- | ------------------ | | `/invoices` | GET, POST | | `/invoices/from-quote` | POST | | `/invoices/{invoiceId}` | GET, PATCH, DELETE | | `/invoices/{invoiceId}/send` | POST | | `/invoices/{invoiceId}/payments` | POST | | `/invoices/{invoiceId}/mark-paid` | POST | | `/invoices/{invoiceId}/write-off` | POST | | `/invoices/{invoiceId}/cancel` | POST | | `/invoices/{invoiceId}/duplicate` | POST | | `/invoices/{invoiceId}/pause-reminders` | POST | | `/invoices/{invoiceId}/resume-reminders` | POST | | `/invoices/{invoiceId}/lifecycle` | GET | | `/invoices/{invoiceId}/lifecycle/advance` | POST | | `/invoices/{invoiceId}/notes` | GET, POST | | `/invoices/{invoiceId}/notes/{noteId}` | PATCH, DELETE | See [Invoices](/docs/product/invoices). ### Orders, shipments, purchasing and promotions [#orders-shipments-purchasing-and-promotions] | Path | Methods | | -------------------------------------------- | --------- | | `/orders` | GET, POST | | `/orders/{orderId}` | GET | | `/orders/{orderId}/confirm` | POST | | `/orders/{orderId}/cancel` | POST | | `/orders/{orderId}/lifecycle` | GET | | `/orders/{orderId}/lifecycle/advance` | POST | | `/shipments` | GET | | `/shipments/{shipmentId}` | GET | | `/shipments/{shipmentId}/ship` | POST | | `/purchase-orders` | GET, POST | | `/purchase-orders/{purchaseOrderId}` | GET | | `/purchase-orders/{purchaseOrderId}/receive` | POST | | `/promotions/validate` | POST | Creating, confirming and cancelling orders, shipping a shipment, and creating or receiving a purchase order run the organization's approval policy. When a policy requires approval, the route answers `409` with code `APPROVAL_REQUIRED` and an `approvalId`. See [Errors](/docs/developers/errors), [Orders](/docs/product/orders) and [Approvals](/docs/product/approvals). ### Tasks and agenda [#tasks-and-agenda] | Path | Methods | | ---------------------------------------- | ------------------ | | `/tasks` | GET, POST | | `/tasks/reorder` | POST | | `/tasks/{taskId}` | GET, PATCH, DELETE | | `/tasks/{taskId}/status` | PATCH | | `/tasks/{taskId}/assignees` | PUT | | `/tasks/{taskId}/comments` | GET, POST | | `/tasks/{taskId}/comments/{commentId}` | PATCH, DELETE | | `/tasks/{taskId}/time-entries` | GET, POST | | `/tasks/{taskId}/time-entries/{entryId}` | DELETE | | `/tasks/{taskId}/lifecycle` | GET | | `/tasks/{taskId}/lifecycle/advance` | POST | | `/agenda/items` | GET, POST | | `/agenda/items/{agendaItemId}` | GET, PATCH, DELETE | | `/agenda/items/{agendaItemId}/move` | POST | See [Tasks](/docs/product/tasks) and [Agenda](/docs/product/agenda). ### People [#people] | Path | Methods | | ----------------------------------- | ---------- | | `/employees` | GET, POST | | `/employees/{employeeId}` | GET, PATCH | | `/time-entries` | GET, POST | | `/time-off-requests` | GET, POST | | `/expenses` | GET, POST | | `/approvals` | GET, POST | | `/hiring/jobs` | GET | | `/hiring/jobs/{jobId}/applications` | GET | `GET /approvals` lists the time-off requests, timesheets and expense claims waiting for the caller's decision, including delegated ones. `POST /approvals` approves or rejects up to 100 of them at once, and each decision succeeds or fails on its own. ### Files [#files] | Path | Methods | | ----------------------------------------- | ------- | | `/files/drives` | GET | | `/files/nodes/{nodeId}` | GET | | `/files/nodes/{nodeId}/children` | GET | | `/files/nodes/{nodeId}/download` | GET | | `/files/copy` | POST | | `/files/uploads` | POST | | `/files/uploads/{reservationId}/finalize` | POST | | `/files/uploads/{reservationId}/cancel` | POST | | `/files/zip-exports` | POST | | `/files/zip-exports/{exportId}` | GET | Uploads are reserved first, then finalized or cancelled. See [Files](/docs/product/files). ### Inbox, chat, knowledge and AI [#inbox-chat-knowledge-and-ai] | Path | Methods | | ----------------------------------------------- | ----------------- | | `/inbox/threads` | GET | | `/inbox/threads/{threadId}/messages` | GET, POST (reply) | | `/inbox/threads/{threadId}/suggestions` | POST | | `/inbox/attachments` | POST | | `/inbox/attachments/{attachmentId}` | GET | | `/inbox/deliveries/{deliveryId}` | GET | | `/knowledge/retrieve` | POST | | `/collaboration/references` | GET | | `/ai/threads` | GET, POST | | `/ai/threads/{threadId}/messages` | GET, POST | | `/ai/models` | GET | | `/ai/activity` | GET | | `/ai/agent-runs/{runId}` | GET | | `/ai/agent-runs/{runId}/events` | GET | | `/ai/agent-runs/{runId}/handoff` | POST | | `/ai/agent-runs/{runId}/approvals/{approvalId}` | POST | | `/ai/contextual-actions/{actionId}` | GET | | `/ai/contextual-artifacts` | GET | | `/ai/contextual-artifacts/{artifactId}` | GET, PATCH | | `/channels/{connectionId}/webhook` | GET, POST | Only `/ai/threads/{threadId}/messages` from the `/ai` group is in the OpenAPI spec. The other `/ai` routes serve the SaaS assistant and agent-run viewer and can change without notice. `/channels/{connectionId}/webhook` receives provider callbacks for an inbox channel connection (WhatsApp) and queues them for processing; it is not for API clients. See [Inbox](/docs/product/inbox), [Chat](/docs/product/chat) and [Knowledge](/docs/admin/knowledge). ### Notifications [#notifications] | Path | Methods | | --------------------------------- | ----------------- | | `/notifications` | GET | | `/notifications/inbox` | GET | | `/notifications/{notificationId}` | PATCH | | `/notifications/mark-all-read` | POST | | `/notification-preferences` | GET, PUT | | `/notification-subscriptions` | GET, POST, DELETE | ### Workflows and webhooks [#workflows-and-webhooks] | Path | Methods | | ------------------------------------------------------ | ------------------ | | `/workflows/definitions` | GET | | `/workflows/definitions/{definitionId}` | GET | | `/workflows/definitions/{definitionId}/publish` | POST | | `/workflows/definitions/{definitionId}/runs` | POST (start a run) | | `/workflows/runs` | GET | | `/workflows/runs/{runId}` | GET | | `/workflows/runs/{runId}/approval` | POST | | `/workflows/runs/{runId}/cancel` | POST | | `/workflows/runs/{runId}/restart` | POST | | `/webhooks/destinations` | GET, POST | | `/webhooks/destinations/{destinationId}` | GET, PATCH, DELETE | | `/webhooks/destinations/{destinationId}/rotate-secret` | POST | | `/webhooks/endpoints` | GET, POST | | `/webhooks/endpoints/{endpointId}` | GET, PATCH, DELETE | | `/webhooks/endpoints/{endpointId}/rotate-secret` | POST | | `/webhooks/endpoints/{endpointId}/rotate-token` | POST | | `/webhooks/deliveries` | GET | | `/webhooks/deliveries/{deliveryId}` | GET | See [Webhooks](/docs/developers/webhooks) and [Workflows](/docs/product/workflows). ### Audit log [#audit-log] | Path | Methods | | --------------------------- | ------- | | `/audit-events` | GET | | `/audit-exports` | POST | | `/audit-exports/{exportId}` | GET | See [Audit log](/docs/product/audit-log). ## Top-level routes [#top-level-routes] These paths are relative to `/api/v1`. ### Account and session [#account-and-session] | Path | Methods | Purpose | | --- | --- | --- | | `/health` | GET | Versioned health check. No authentication. | | `/auth/login` | POST | Email and password sign-in. Creates a cookie session and returns the user. | | `/auth/signup` | POST | Create an account with email and password. | | `/auth/logout` | POST | Clear the current session. | | `/auth/oauth/start` | GET | Create the authorization URL for an OAuth sign-in provider. | | `/auth/password/reset` | POST | Send a password reset email. | | `/auth/password/update` | POST | Set a new password for the signed-in user. | | `/users/me` | GET | The authenticated user. | | `/users/me/active-organization` | PATCH | Store the preferred default organization. | | `/organizations` | GET, POST | List organizations you can access, or create one. | | `/command-center/search` | GET | Search records for the command center. | | `/agenda/google-calendar/oauth/start` | GET | Start the Google Calendar integration authorization. | ### Assistant [#assistant] The `/ai/chat` routes (`/ai/chat`, `/ai/chat/threads`, `/ai/chat/{threadId}` with `participants`, `share`, `stop` and `stream`, `/ai/chat/attachments`, `/ai/chat/follow-ups` and `/ai/chat/starter-suggestions`), `/ai/specialists` and `/ai/transcribe` power the in-app assistant. `/workflows/graph-operations` powers AI edits in the workflow editor. They are session transports for the SaaS app, are not in the OpenAPI spec, and are not a stable integration surface. ### Public token routes [#public-token-routes] These routes take no session. The token or the published record in the path is the credential. | Path | Methods | Purpose | | --- | --- | --- | | `/public/calendars/{token}` | GET | iCalendar feed for a shared agenda calendar. | | `/public/time-off/{token}` | GET | iCalendar feed of approved team leave. | | `/public/chats/{token}` | GET | Read-only shared chat. | | `/public/files/{token}` | GET | Redirect to a signed download URL for a shared file. | | `/public/onboarding/{token}` | GET, POST | New-hire pre-boarding page and details form. | | `/public/onboarding/{token}/documents` | POST | Upload a pre-boarding document. | | `/public/portal/{orgSlug}/quotes/{quoteId}/request-changes` | POST | A customer requests changes to a sent quote. | | `/public/webhooks/{token}` | POST | Receive an incoming organization webhook. | The pre-boarding, quote change request and incoming webhook routes are rate limited and answer `429` with a `Retry-After` header when a caller sends too many requests. The counters are kept in the database, so the limit holds across every API instance. See [Customer portal](/docs/product/customer-portal) and [Webhooks](/docs/developers/webhooks). ### Provider webhooks [#provider-webhooks] | Path | Methods | Caller | | --- | --- | --- | | `/billing/stripe/webhook` | POST | Stripe platform billing events, verified with the `stripe-signature` header. | | `/billing/stripe/connect/webhook` | POST | Stripe events of connected accounts (payments, refunds, disputes, payouts), verified with the `stripe-signature` header. | | `/billing/stripe/accounts/webhook` | POST | Stripe thin v2 account events, verified with the `stripe-signature` header. | | `/email/resend/webhook` | POST | Resend, verified with the `svix-*` signature headers. | | `/email/supabase/auth` | POST | The Supabase Auth send-email hook. | `/organizations/{organizationId}/channels/{connectionId}/webhook` (listed above) is the per-connection inbox channel callback. Each provider webhook is verified, stored once per message id in the webhook inbox and answered with `202` (`200` for a repeated delivery); the inbox worker processes it afterwards, with retries. See [Stripe billing](/docs/operations/stripe-billing). ### Scheduled jobs [#scheduled-jobs] Vercel Cron calls `GET /internal/jobs/drain` every minute with `Authorization: Bearer <CRON_SECRET>`; any other caller gets `401`. The route queues due schedules and works the background job queues. See [Deployment](/docs/getting-started/deployment) for the jobs and their schedules. `/admin/subscriptions/{orgSlug}/invoices/{invoiceId}/pdf` serves subscription invoice PDFs to platform administrators with `platform.billing.read`. ## Errors [#errors] Every error is an RFC 9457 problem details document served as `application/problem+json`, with a stable `code` and a `messageKey`. See [Errors](/docs/developers/errors). ## Generated client [#generated-client] The OpenAPI spec is committed at `apps/api/public/openapi.json`. Inside the monorepo, `@workspace/api-client` wraps a Hey API client generated from that spec, and the CLI and the MCP server both use it. See [OpenAPI](/docs/developers/openapi). ## Related pages [#related-pages] - [Authentication](/docs/developers/authentication) - [Errors](/docs/developers/errors) - [Webhooks](/docs/developers/webhooks) - [MCP server](/docs/developers/mcp) - [CLI](/docs/developers/cli) - [OpenAPI](/docs/developers/openapi) - [Service layers](/docs/architecture/service-layers) # Authentication (/docs/developers/authentication) How the CentraKit API authenticates browser sessions and bearer tokens, and how clients get tokens through Supabase OAuth 2.1, PKCE and dynamic client registration. The CentraKit API trusts one identity provider: Supabase Auth. A request is authenticated either by a Supabase session cookie (browsers) or by a Supabase access token sent as a bearer token (scripts, the CLI and MCP clients). Both paths end in the same request context, so permissions, Row Level Security, audit logging and errors behave the same way. ## Session cookies [#session-cookies] The SaaS app signs users in with Supabase and stores the session in the `sb-auth-token` cookie. Supabase splits large sessions into chunked cookies such as `sb-auth-token.0`. When a request to the API has no `Authorization: Bearer` header, the API reads this cookie session. The API is the `/api` path on each brand's app host, so the browser sends the cookie the SaaS app set on that host. Cookies are host-only: a session on `app.centrakit.com` does not reach `app.mechup.com`. Cookies suit same-site browser flows. Use bearer tokens for everything else. ## Bearer tokens [#bearer-tokens] Send a Supabase access token (a JWT) in the `Authorization` header: ```http GET /api/v1/users/me HTTP/1.1 Host: app.centrakit.com Authorization: Bearer <access-token> ``` Before the route runs, the API verifies: - the signature, against the Supabase JWKS at `https://<project-ref>.supabase.co/auth/v1/.well-known/jwks.json`; - the issuer, which must be `https://<project-ref>.supabase.co/auth/v1`; - the expiry; - the audience, which must be Supabase's default `authenticated`, the MCP resource URL of a brand host such as `https://app.centrakit.com/mcp`, or the `MCP_RESOURCE_AUDIENCE` value a deployment configures. As soon as the request has a `Bearer` authorization scheme, the cookie is ignored. An invalid or expired token gets `401` with an `AUTH_INVALID_TOKEN` problem and a `WWW-Authenticate` header that points to the protected resource metadata. The API does not fall back to the cookie session. ### Scopes on bearer tokens [#scopes-on-bearer-tokens] Supabase access tokens usually carry no CentraKit scopes. When a token does carry `mcp:read` or `mcp:write` in its `scope` claim, the scopes limit what the token may do. `mcp:read` covers the permissions of the MCP read tools and `mcp:write` those of the MCP mutation tools, on the REST API as on the MCP server. A few operations name their own scope where one permission backs a read and a write: reading an audit export (`GET .../audit-exports/{exportId}`) takes `mcp:read`, and creating one takes `mcp:write`. A route that the token's scopes do not reach answers `403` with `WWW-Authenticate: Bearer error="insufficient_scope", scope="..."`, naming the scope that would reach it, and a PermDock problem (`type` `https://permdock.com/problems/denied`). Routes outside the organization permission model, such as the assistant chat and `PATCH /v1/users/me/active-organization`, each declare the scope that reaches them: reads take `mcp:read` and changes take `mcp:write`. Scopes only narrow what a token can do: the user's organization permissions still decide every request; see [MCP server](/docs/developers/mcp). ### OAuth clients act only on what was delegated [#oauth-clients-act-only-on-what-was-delegated] A token that Supabase's OAuth server issued to a client carries that client's `client_id`. The API treats the client as an agent acting for the user: each request is decided on the user's organization permissions, limited to what the user delegated to that client. | Client | What it may do for the user | | --- | --- | | The CentraKit CLI (the client ID in the API's `CENTRAKIT_OAUTH_CLIENT_ID`) | Everything the user may do | | A client whose token carries `mcp:read` or `mcp:write` | What the MCP tools covered by those scopes may do | | Any other client | Nothing | A token from any other client that carries only identity scopes (`openid`, `email`, `profile`) is refused on every organization route with the `403` `insufficient_scope` challenge above. A missing organization permission returns the `403` `ORGANIZATION_PERMISSION_DENIED` problem. Session tokens from a password or Google sign-in carry no `client_id` and are not limited. ## Getting a token [#getting-a-token] ### CLI login with PKCE [#cli-login-with-pkce] `centrakit login` runs an OAuth 2.1 authorization code flow with PKCE against Supabase: 1. The CLI creates a PKCE verifier and challenge and a random `state`. 2. It opens `https://<project-ref>.supabase.co/auth/v1/oauth/authorize` in your browser with the challenge and a loopback redirect URI, `http://127.0.0.1:8976/callback` by default. 3. You complete the Supabase authorization step for your CentraKit account in the browser. 4. Supabase redirects to the loopback server. The CLI checks `state` and exchanges the code and verifier at `/auth/v1/oauth/token`. 5. The CLI calls `GET /api/v1/users/me` with the new token and stores the access and refresh tokens in `session.json` (file mode `600`). It refreshes the access token when it expires. Browser login needs an OAuth client ID (`CENTRAKIT_OAUTH_CLIENT_ID`) and the Supabase URL (`CENTRAKIT_SUPABASE_URL` or `NEXT_PUBLIC_SUPABASE_URL`). The API must know the same client ID in its own `CENTRAKIT_OAUTH_CLIENT_ID`; otherwise it treats the CLI as an unknown client and refuses its requests. See [CLI](/docs/developers/cli) for every setting. ### OAuth 2.1 for your own client [#oauth-21-for-your-own-client] The Supabase project runs an OAuth 2.1 authorization server with dynamic client registration enabled. A client that wants to call the API on behalf of a user: 1. Fetches the protected resource metadata (below) and reads `authorization_servers`. 2. Fetches that server's OAuth authorization server metadata to find the authorization, token and registration endpoints. 3. Registers itself through dynamic client registration, or uses a client ID it already has. 4. Runs the authorization code flow with PKCE, sending the user to the authorization endpoint and receiving the code on its redirect URI. 5. Sends the resulting access token as a bearer token and uses the refresh token to renew it. MCP clients do all of this automatically when you add the server. ### Headless and CI use [#headless-and-ci-use] For scripts and CI, pass an access token you obtained earlier: ```bash CENTRAKIT_TOKEN=<access-token> centrakit orgs list curl -H "Authorization: Bearer $CENTRAKIT_TOKEN" https://app.centrakit.com/api/v1/organizations ``` Access tokens expire. Long-running jobs need to refresh them with the refresh token from the same flow. Personal API keys can be created on the Authentication tab of your profile (`pdk_` keys with read-only or read and write access, in all your organizations or in one, with an optional expiry). The REST API under `/api/v1` accepts a key as a bearer token or in the `x-api-key` header: ```bash curl -H "Authorization: Bearer pdk_..." https://app.centrakit.com/api/v1/organizations/<organization-id>/workflows/definitions ``` A request with a key runs as you, limited to the key's permissions: the database applies the same row-level security as for your session, and a read-only key cannot change data. An unknown, expired or revoked key answers `401` with a `WWW-Authenticate: Bearer` challenge and a problem response. Keys work only on the REST API; the MCP server accepts OAuth tokens. A key limited to one organization answers `403` for every other organization you belong to. Organization API keys are not available yet. ## Protected resource metadata [#protected-resource-metadata] The API publishes RFC 9728 protected resource metadata on every brand host. The `resource` is the `/mcp` URL of the host that served the document. | Environment | URL | | --- | --- | | Production | `https://app.centrakit.com/.well-known/oauth-protected-resource` | | Local | `https://app.localhost/.well-known/oauth-protected-resource` | ```json { "resource": "https://app.centrakit.com/mcp", "authorization_servers": ["https://<project-ref>.supabase.co/auth/v1"] } ``` The document adds `scopes_supported` only when the deployment sets `MCP_REQUIRED_SCOPES`. The endpoint answers CORS preflight requests so browser-based clients can read it. ## Error handling [#error-handling] Authentication failures are problem details with an `AUTH_*` code: | Code | Status | When | | --- | --- | --- | | `AUTH_UNAUTHENTICATED` | 401 | No session and no bearer token on a route that needs one. | | `AUTH_INVALID_TOKEN` | 401 | The bearer token failed verification or expired. | | `AUTH_INVALID_CREDENTIALS` | 401 | Wrong email or password on `/auth/login`. | | `AUTH_INSUFFICIENT_SCOPE` | 403 | The token lacks a required `mcp:*` scope. | | `AUTH_CONFIRMATION_REQUIRED` | 403 | The account email is not confirmed yet. | | `AUTH_VALIDATION_ERROR` | 400 | The request body is invalid. | | `AUTH_PROVIDER_ERROR` | 502 | Supabase Auth failed. | | `AUTH_RESOURCE_SERVER_MISCONFIGURED` | 500 | The deployment's audience or issuer configuration is wrong. | A valid user without the organization permission for a route gets `403` with `ORGANIZATION_PERMISSION_DENIED`. See [Errors](/docs/developers/errors). ## Related pages [#related-pages] - [Authentication architecture](/docs/architecture/authentication) - [RBAC](/docs/architecture/rbac) - [REST API](/docs/developers/api) - [MCP server](/docs/developers/mcp) - [CLI](/docs/developers/cli) - [Environment variables](/docs/operations/environment-variables) # CLI (/docs/developers/cli) Every centrakit CLI command and global flag, how it signs in and resolves the API base URL, its configuration files and environment variables, and how releases are packaged. The `centrakit` CLI calls the CentraKit REST API from a terminal, a script or CI. It signs in with a browser PKCE flow or takes a bearer token, and it can call any OpenAPI operation by its ID. The source lives in `apps/cli`. ## Install [#install] The CLI needs Node.js 24 or later. Each published GitHub release has the packaged CLI tarball attached by the CLI release workflow. Install it globally with npm: ```bash npm install -g ./centrakit-<version>.tgz centrakit --version ``` Inside the monorepo, build and run it with: ```bash pnpm --filter centrakit build pnpm --filter centrakit cli -- --help ``` ## Global flags [#global-flags] Global flags go before or after the command name. | Flag | Effect | | --- | --- | | `--api-url <url>` | Use this API base URL. Include `/api`, for example `https://app.localhost/api`. | | `--host <host>` | Use the API on this app host, for example `app.mechup.com` for MechUp. The CLI calls `https://<host>/api`. A value with a scheme, such as `http://localhost:3000`, keeps it. | | `--json` | Print output as indented JSON, and print errors as a Problem Details document on stdout. Also turns off prompts. | | `--token-stdin` | Read a bearer token from standard input instead of the stored session. | | `--debug` | Print stack traces for errors. Also set by `CENTRAKIT_CLI_DEBUG=1`. | | `--no-color` | Disable ANSI colors. Also set by `NO_COLOR`. Colors are also off when output is not a terminal. | | `--non-interactive` | Never prompt or open a browser. Also set by `CI=true` or `CENTRAKIT_CLI_NON_INTERACTIVE=1`. Pass `--no-non-interactive` or `--non-interactive=false` to override the environment. | | `-V`, `--version` | Print the CLI version. | | `-h`, `--help` | Print help for the CLI or a command, for example `centrakit orgs create --help`. | An unknown command fails with a suggestion, for example ``Unknown command `org`. Did you mean `orgs`?``. This also works for subcommands, such as `centrakit orgs lst`. ## Interactive use [#interactive-use] When the CLI runs in a terminal, it asks for values you leave out instead of failing: - `register` and `forgot-password` ask for the email, and `register` also asks for the first name, last name and password. - Commands that take an `<organizationId>` show a picker of your organizations. - `logout` and `orgs delete` ask for confirmation. Pass `--yes` (or `-y`) to skip it. - `login` shows a spinner while it waits for the browser. Prompts never appear when standard input or output is not a terminal, when `--json` is set, or in non-interactive mode. In those cases a missing value fails with a message such as `Missing the EMAIL argument.` and exit status 2, and confirmations are skipped. Pressing Ctrl+C at a prompt prints `Cancelled.` and exits with status 130. ## Commands [#commands] | Command | What it does | | --- | --- | | `login` | Sign in with the browser and store a local session. | | `logout` | Delete the stored session. | | `register <email>` | Create an account. | | `forgot-password <email>` | Send a password reset email. | | `whoami` | Show the authenticated user. | | `health` | Show the API health status. | | `api <operationId>` | Call any OpenAPI operation directly. | | `orgs list` | List organizations you can access. | | `orgs get <organizationId>` | Show one organization. | | `orgs create` | Create an organization. | | `orgs update <organizationId>` | Update organization settings. | | `orgs delete <organizationId>` | Delete an organization. | | `orgs switch <organizationId>` | Set your preferred active organization. | | `orgs branding logo set <organizationId>` | Upload the organization logo. | | `orgs branding logo remove <organizationId>` | Remove the organization logo. | | `orgs branding images add <organizationId>` | Upload a brand image. | | `orgs branding images remove <organizationId>` | Remove a brand image. | | `users me` | Show the authenticated user. Same output as `whoami`. | Every `<organizationId>` must be the organization UUID. The CLI rejects anything else before calling the API. In a terminal you can leave it out and pick the organization from a list. ### login [#login] ```bash centrakit login ``` Opens the Supabase OAuth authorization page in your browser, waits for the redirect on a loopback server, exchanges the code with PKCE, and stores the session. The CLI always prints the authorization URL, so you can open it yourself when the browser does not start. The command fails when: - `--non-interactive`, `CI=true` or `CENTRAKIT_CLI_NON_INTERACTIVE=1` is set; - `--token-stdin` or `CENTRAKIT_TOKEN` is set; - `CENTRAKIT_OAUTH_CLIENT_ID` or the Supabase URL is missing; - the callback port is already in use. See [Authentication](/docs/developers/authentication) for the flow. ### logout [#logout] ```bash centrakit logout ``` Asks for confirmation, then deletes `session.json`. Pass `--yes` to skip the question. Tokens already issued stay valid until they expire. ### register and forgot-password [#register-and-forgot-password] ```bash centrakit register centrakit register ada@example.com --first-name Ada --last-name Lovelace printf '%s' "$PASSWORD" | centrakit register ada@example.com --first-name Ada --last-name Lovelace --password-stdin centrakit forgot-password ada@example.com centrakit forgot-password ada@example.com --redirect-path /reset-password ``` `register` never takes the password as an argument, so it does not end up in your shell history. In a terminal it asks for the password without echoing it. In scripts, pipe it in with `--password-stdin`; one trailing newline is removed. `--redirect-path` is the app path the recovery link opens. It defaults to `/reset-password`. ### whoami, users me and health [#whoami-users-me-and-health] ```bash centrakit whoami centrakit users me --json centrakit health ``` ### api [#api] ```bash centrakit api <operationId> [--path <json>] [--query <json>] [--body <json>] ``` Calls any operation from the OpenAPI spec by its operation ID and always prints JSON. Each option takes a JSON object: ```bash centrakit api listCustomers \ --path '{"organizationId":"1f0c2d3e-4b5a-4c6d-8e7f-9a0b1c2d3e4f"}' \ --query '{"limit":20,"q":"acme"}' centrakit api setTaskStatus \ --path '{"organizationId":"1f0c2d3e-4b5a-4c6d-8e7f-9a0b1c2d3e4f","taskId":"7a8b9c0d-1e2f-4a3b-8c4d-5e6f7a8b9c0d"}' \ --body '{"status":"done","position":0}' ``` Find operation IDs on [OpenAPI](/docs/developers/openapi) or in [REST API](/docs/developers/api). ### orgs list, get, delete and switch [#orgs-list-get-delete-and-switch] ```bash centrakit orgs list centrakit orgs get 1f0c2d3e-4b5a-4c6d-8e7f-9a0b1c2d3e4f --json centrakit orgs switch 1f0c2d3e-4b5a-4c6d-8e7f-9a0b1c2d3e4f centrakit orgs delete 1f0c2d3e-4b5a-4c6d-8e7f-9a0b1c2d3e4f ``` `orgs delete` asks for confirmation unless you pass `--yes`. `orgs switch` stores a preferred active organization on your profile, which clients can use as a default. It does not change which organization later CLI commands use, because organization routes always take the organization ID explicitly. ### orgs create and update [#orgs-create-and-update] ```bash centrakit orgs create \ --name "Green Acre Service" \ --slug green-acre \ --website https://greenacre.example \ --address-city Utrecht \ --address-country NL \ --social "LinkedIn|https://linkedin.com/company/greenacre" \ --primary-color "#1F7A4D" ``` | Option | Required | Value | | --- | --- | --- | | `--name <name>` | Yes | Organization name. | | `--slug <slug>` | Yes | URL slug. | | `--website <url>` | No | Website URL. | | `--address-line1 <value>` | No | Address line 1. | | `--address-line2 <value>` | No | Address line 2. | | `--address-city <value>` | No | City. | | `--address-state <value>` | No | State or province. | | `--address-postal-code <value>` | No | Postal code. | | `--address-country <value>` | No | Country. | | `--social <label\|url>` | No | A social link as `label\|url`. Repeat the flag for more links. | | `--primary-color <hex>` | No | Primary brand color. | | `--secondary-color <hex>` | No | Secondary brand color. | `orgs update <organizationId>` takes the same options, and `--name` and `--slug` are required there too. The API requires the full settings object, so any option you leave out is cleared, as are the phone, IBAN, VAT and Chamber of Commerce fields, which these flags cannot set. To keep existing values, read them with `orgs get`, then send the complete object with `centrakit api updateOrganization --path '{"organizationId":"..."}' --body '<json>'`. ### orgs branding [#orgs-branding] ```bash centrakit orgs branding logo set 1f0c2d3e-4b5a-4c6d-8e7f-9a0b1c2d3e4f --file ./logo.png centrakit orgs branding logo remove 1f0c2d3e-4b5a-4c6d-8e7f-9a0b1c2d3e4f centrakit orgs branding images add 1f0c2d3e-4b5a-4c6d-8e7f-9a0b1c2d3e4f --file ./hero.webp centrakit orgs branding images remove 1f0c2d3e-4b5a-4c6d-8e7f-9a0b1c2d3e4f --image-id <brandImageId> ``` `--file` must be a `.png`, `.jpg`, `.jpeg` or `.webp` file; the CLI picks the content type from the extension. `images remove` requires `--image-id`. ## Authentication [#authentication] The CLI picks a token in this order: 1. `--token-stdin`, which reads the token from standard input. 2. `CENTRAKIT_TOKEN`. 3. The stored session from `centrakit login`. When its access token has expired, the CLI refreshes it with the refresh token and saves the new session. If the refresh fails, the request is sent without a token. ```bash CENTRAKIT_TOKEN=<access-token> centrakit orgs list printf "%s" "$ACCESS_TOKEN" | centrakit --token-stdin orgs list --json ``` ## API base URL [#api-base-url] The CLI resolves the base URL in this order: 1. `--api-url <url>`. 2. `--host <host>`, which becomes `https://<host>/api`. 3. `CENTRAKIT_API_URL`. 4. `apiUrl` from the config files, see [Configuration](#configuration). 5. `https://app.localhost/api` when `NODE_ENV` is `development`, otherwise `https://app.centrakit.com/api`. Every brand serves the API at `/api` on its own app host, so `--host` is the short way to reach another brand: ```bash centrakit --host app.mechup.com orgs list centrakit --host app.stabledock.app health ``` Always include `/api` in a base URL you pass with `--api-url`, `CENTRAKIT_API_URL` or `apiUrl`, because the CLI appends paths such as `/v1/health`. ## Configuration [#configuration] The config directory is `CENTRAKIT_CONFIG_DIR` when set, otherwise `$XDG_CONFIG_HOME/centrakit`, or `~/.config/centrakit` when `XDG_CONFIG_HOME` is not set. It holds: | File | Contents | | --- | --- | | `session.json` | Access token, refresh token, expiry and user from `centrakit login`. Written with file mode `600`. | | `config.json` | Optional defaults. Only `apiUrl` is supported. | Settings can also come from rc and config files. The CLI merges them in this order, highest first: 1. `centrakit.config.ts` (or `.mts`, `.js`, `.mjs`, `.json`) in the working directory. 2. `.centrakitrc` in the working directory. 3. `.centrakitrc` in `$XDG_CONFIG_HOME`, or in your home directory when `XDG_CONFIG_HOME` is not set. 4. `config.json` in the config directory. Every source accepts the same keys, and only `apiUrl` is supported. An unknown key or an invalid URL fails with `Invalid CLI config`. ```json { "apiUrl": "https://app.localhost/api" } ``` An rc file uses `key=value` lines: ```ini apiUrl=https://app.localhost/api ``` When the project installs `centrakit` as a dependency, a TypeScript config file can import `defineConfig` for type checking: ```ts import { defineConfig } from "centrakit/config"; export default defineConfig({ apiUrl: "https://app.localhost/api", }); ``` ### Environment variables [#environment-variables] | Variable | Purpose | | --- | --- | | `CENTRAKIT_TOKEN` | Bearer token to use instead of the stored session. | | `CENTRAKIT_API_URL` | API base URL. | | `CENTRAKIT_CONFIG_DIR` | Config directory. | | `CENTRAKIT_OAUTH_CLIENT_ID` | OAuth client ID for `login`. Required for browser login. The API needs the same ID under the same name. | | `CENTRAKIT_SUPABASE_URL` | Supabase project URL for `login`. Falls back to `NEXT_PUBLIC_SUPABASE_URL`. | | `CENTRAKIT_OAUTH_SCOPES` | Requested scopes. Defaults to `openid email profile`. | | `CENTRAKIT_OAUTH_REDIRECT_HOST` | Loopback callback host. Defaults to `127.0.0.1`. | | `CENTRAKIT_OAUTH_REDIRECT_PORT` | Loopback callback port. Defaults to `8976`. | | `CENTRAKIT_CLI_NON_INTERACTIVE` | `1` enables non-interactive mode. | | `CENTRAKIT_CLI_DEBUG` | `1` prints stack traces for errors. | | `CI` | `true` enables non-interactive mode. | | `NO_COLOR` | Any non-empty value disables colors. | The OAuth client must allow the redirect URI `http://<host>:<port>/callback`, which is `http://127.0.0.1:8976/callback` by default. ## Output and exit codes [#output-and-exit-codes] Commands print plain text for strings and indented JSON for objects. `--json` always prints JSON. `orgs list` prints the organization array without the pagination wrapper. When an API call fails, the CLI prints the problem `detail` (or `title`) and the code to stderr, for example `The organization could not be found. (ORGANIZATION_NOT_FOUND)`, and exits with status 1. With `--json`, it prints the whole problem to stdout instead. Errors raised by the CLI itself use the same shape with `type` set to `about:blank` and a `code` of `USAGE_ERROR`, `CANCELLED` or `CLI_ERROR`. | Exit status | Meaning | | --- | --- | | 0 | Success, help or `--version`. | | 1 | The command failed, including API errors. | | 2 | Usage error: an unknown command, an invalid flag value or a missing argument. `centrakit api` also exits with 2 for an unknown operation ID or a `--path` or `--query` value that is not an object of scalar values. | | 130 | A prompt was cancelled. | See [Errors](/docs/developers/errors). ## Releases [#releases] CLI releases go through Changesets. When a GitHub release is published, the Release CLI Assets workflow generates the OpenAPI artifacts, lints, typechecks and builds the CLI, packs it with `pnpm pack`, checks that the tarball contains `dist/cli.js`, and uploads the tarball to the release. A maintainer can rerun it manually for an existing release tag. See [CI and release](/docs/operations/ci-and-release). ## Related pages [#related-pages] - [Authentication](/docs/developers/authentication) - [REST API](/docs/developers/api) - [MCP server](/docs/developers/mcp) - [OpenAPI](/docs/developers/openapi) # Errors (/docs/developers/errors) The RFC 9457 problem details format that every CentraKit API and MCP error uses, how codes and statuses are derived, and how to handle APPROVAL_REQUIRED. Every CentraKit API error is an [RFC 9457](https://www.rfc-editor.org/rfc/rfc9457) problem details document served with `Content-Type: application/problem+json` and `Cache-Control: private, no-store`. MCP tool errors carry the same document, so clients can branch on one set of codes. ## Shape [#shape] ```json { "type": "https://app.centrakit.com/docs/developers/errors#CUSTOMER_NOT_FOUND", "title": "Customer not found", "status": 404, "detail": "The customer could not be found.", "code": "CUSTOMER_NOT_FOUND", "messageKey": "customer.error.notFound" } ``` | Member | Meaning | | --- | --- | | `type` | URI that identifies the problem. Always `https://app.centrakit.com/docs/developers/errors#<code>`, on every brand host. Older responses used `https://docs.centrakit.com/developers/errors#<code>`; compare `code` instead of `type`. | | `title` | Short summary derived from the code, such as `Customer not found`. It is the same for every occurrence. | | `status` | HTTP status code, repeated in the body. | | `detail` | Human-readable explanation of this occurrence. Optional, and not stable. | | `instance` | URI reference for this occurrence. Optional. | | `code` | Stable machine code. Branch on this, not on `detail` or `title`. | | `messageKey` | Translation key a client can use to show localized copy. | | `errors` | Optional list of `{ "detail": "...", "pointer": "..." }` validation issues. | Some problems add extension members next to these, such as `approvalId`. The standard members always win over an extension with the same name. ## Codes and statuses [#codes-and-statuses] Domain services return tagged errors such as `CustomerNotFoundError`. The API turns the tag into the code by dropping `Error` and converting to upper snake case, so `CustomerNotFoundError` becomes `CUSTOMER_NOT_FOUND` with the message key `customer.error.notFound`. Unless a route sets the status explicitly, the status follows from the tag: | Tag contains | Status | Example code | | --- | --- | --- | | `Validation` | 400 | `CUSTOMER_VALIDATION` | | `Unauthenticated` | 401 | `CRON_UNAUTHENTICATED` | | `Forbidden` or `PermissionDenied` | 403 | `ORGANIZATION_PERMISSION_DENIED` | | `NotFound` or `TokenInvalid` | 404 | `ORGANIZATION_NOT_FOUND` | | `Conflict`, `Expired`, `StateError` or `ApprovalRequired` | 409 | `APPROVAL_REQUIRED` | | `AlreadyCredited`, `InUse`, `Locked`, `NotReady`, `LastOfType`, `RecordHeld`, `InsufficientStock` or `StaleContext` | 409 | `CUSTOMER_IN_USE` | | `Missing`, `TooSoon`, `InvalidPayload` or `InvalidDateRange` | 422 | `QUOTE_VALID_UNTIL_TOO_SOON` | | `Unavailable` | 503 | `CRON_UNAVAILABLE` | | Anything else | 500 | `CUSTOMER_UNEXPECTED` | Routes set other statuses where a fixed mapping does not fit: | Code | Status | Where | | --- | --- | --- | | `AUTH_*` | varies | Authentication. See [Authentication](/docs/developers/authentication). | | `RATE_LIMITED` | 429 | Incoming webhooks, with a `retry-after` header. | | `PAYLOAD_TOO_LARGE` | 413 | Incoming webhook bodies over 256 KB. | | `NOT_FOUND`, `UNAUTHORIZED`, `DISABLED`, `FAILED` | 404, 401, 404, 500 | Incoming webhook receiver results. | | `API_UPSTREAM` | 502 or the upstream status | An MCP tool got a non-problem error from the REST API. | | `SUPPORT_SESSION_ENDED` | 403 | A browser request carried the cookie of an admin support session that has ended or expired. | | `SUPPORT_SESSION_READ_ONLY` | 403 | An admin in a read-only support session asked the assistant for a chat turn or a specialist run. | Pre-boarding links and the portal quote change request also answer `429` when a caller sends too many requests. `IDEMPOTENCY_KEY_IN_USE` (409) and `IDEMPOTENCY_KEY_REUSED` (422) answer a repeated `Idempotency-Key`. See [Idempotent retries](/docs/developers/api#idempotent-retries). ### AI chat transport codes [#ai-chat-transport-codes] The assistant transports under `/api/v1/ai/chat` use the same problem format with lowercase codes, for example `rate_limited` (429, with `Retry-After`), `billing_limit_reached`, `model_unavailable`, `unsupported_attachment` and `stale_message_delta` (409). These routes serve the SaaS app and are not part of the public API. ### Agent run approvals [#agent-run-approvals] `POST /api/v1/organizations/{organizationId}/ai/agent-runs/{runId}/approvals/{approvalId}` adds `continued` and `decided` booleans to its problems, so the client knows whether the run moved on. ## APPROVAL_REQUIRED [#approval_required] An organization can require approval before certain actions run. When a mutation waits on an approval policy, it answers `409` with code `APPROVAL_REQUIRED` and an `approvalId`: ```json { "type": "https://app.centrakit.com/docs/developers/errors#APPROVAL_REQUIRED", "title": "Approval required", "status": 409, "detail": "This order needs approval before it can be confirmed.", "code": "APPROVAL_REQUIRED", "messageKey": "order.error.approvalRequired", "approvalId": "0f1d2c3b-4a5e-4f60-8a7b-9c0d1e2f3a4b" } ``` Over REST, the order, shipment and purchase order routes run the policy themselves: creating, confirming and cancelling an order, shipping a shipment, and creating or receiving a purchase order. Over MCP, every mutation tool that maps to an action in the approval catalog runs the policy before it calls the REST route. Those problems also include: | Member | Meaning | | --- | --- | | `requirement` | `confirm` when the user has to confirm it in Notifications, or `authorize` when someone else has to approve it. | | `approverPermission` | The permission an approver needs, or `null`. | To handle it, tell the user who has to act, wait until the request is decided, and send the same request with the same input again. See [Approvals](/docs/product/approvals). An approval covers one call. Once the approved request ran, or after it was rejected, the same request with the same input is refused until the original request window ends: | Where | Status | Code | | --- | --- | --- | | MCP tools | 403 | `APPROVAL_REFUSED`, with `reason` and `approvalId` | | Quote send and invoice write-off | 409 | `QUOTE_CONFLICT` or `INVOICE_CONFLICT` | | Order, shipment and purchase order | 409 | `ORDER_CONFLICT` or `APPROVAL_CONFLICT` | `reason` is `approval-rejected` or `approval-consumed`. Change the input to ask for a new approval. ## Reading errors in TypeScript [#reading-errors-in-typescript] Inside the monorepo, `@workspace/foundation/api/problem-details` reads and validates problem documents: ```ts import { readProblemDetails } from "@workspace/foundation/api/problem-details"; const response = await fetch(url, { headers }); if (!response.ok) { const problem = await readProblemDetails(response); if (problem?.code === "APPROVAL_REQUIRED") { console.info("Waiting for approval", problem.approvalId); } } ``` The generated `@workspace/api-client` SDK returns the problem as `result.error`. Use `getApiProblem(result.error)` or `getApiErrorMessageKey(result.error)` from `@workspace/api-client/client` to read it. Any other client can parse the JSON body and branch on `code`. ## MCP [#mcp] A failed MCP tool call returns `isError: true`. Its `structuredContent` is the problem document, and its text content is the same document as JSON. For `APPROVAL_REQUIRED`, the `detail` already tells the agent to call the tool again with the same input once the request is approved. ## CLI [#cli] The CLI prints the problem `detail` (or `title`) followed by the code, for example `The organization could not be found. (ORGANIZATION_NOT_FOUND)`, and exits with status 1. ## Related pages [#related-pages] - [REST API](/docs/developers/api) - [Authentication](/docs/developers/authentication) - [MCP server](/docs/developers/mcp) - [Approvals](/docs/product/approvals) # MCP server (/docs/developers/mcp) Connect AI clients to the CentraKit product MCP server and the docs MCP server, with OAuth setup, scopes, approvals and the full list of 75 product tools. CentraKit runs two MCP servers on the app host of each brand, such as `app.centrakit.com` or `app.mechup.com`: - The product server at `https://app.centrakit.com/mcp` acts on your organization data with your permissions. It needs OAuth. - The docs server at `https://app.centrakit.com/docs/mcp` reads these docs. It is public and read-only. The product server moved from `https://api.centrakit.com/api/mcp`, which no longer exists. Update the URL in existing client configurations and sign in again. ## Connect [#connect] | Server | Cursor | VS Code | Claude Code | | --- | --- | --- | --- | | Product API | [Add to Cursor](cursor://anysphere.cursor-deeplink/mcp/install?name=centrakit&config=eyJ1cmwiOiJodHRwczovL2FwcC5jZW50cmFraXQuY29tL21jcCJ9) | [Add to VS Code](vscode:mcp/install?%7B%22name%22%3A%22centrakit%22%2C%22type%22%3A%22http%22%2C%22url%22%3A%22https%3A//app.centrakit.com/mcp%22%7D) | `claude mcp add --transport http centrakit https://app.centrakit.com/mcp` | | Docs | [Add to Cursor](cursor://anysphere.cursor-deeplink/mcp/install?name=centrakit-docs&config=eyJ1cmwiOiJodHRwczovL2FwcC5jZW50cmFraXQuY29tL2RvY3MvbWNwIn0=) | [Add to VS Code](vscode:mcp/install?%7B%22name%22%3A%22centrakit-docs%22%2C%22type%22%3A%22http%22%2C%22url%22%3A%22https%3A//app.centrakit.com/docs/mcp%22%7D) | `claude mcp add --transport http centrakit-docs https://app.centrakit.com/docs/mcp` | The product server starts an OAuth sign-in on first use. The docs server needs no authentication. For another brand, replace `app.centrakit.com` with that brand's app host. ## Docs MCP server [#docs-mcp-server] The docs server at `https://app.centrakit.com/docs/mcp` (locally `https://docs.localhost/docs/mcp`) is built on `fumadocs-core/mcp` and has three read-only tools: | Tool | Input | Returns | | --- | --- | --- | | `list_pages` | none | The page index, the same content as `/llms.txt`. | | `search` | `query`, optional `locale` | Matching docs sections with their page URLs. | | `get_page` | `url`, a page pathname such as `/developers/mcp`. The `/docs/developers/mcp` form and full URLs also work. | The page as Markdown. | A typical agent calls `list_pages` or `search` first, then `get_page` for the pages it needs. The same content is available without MCP; see [Docs for AI agents](/docs/developers/ai-agents). ## Transport [#transport] The product server speaks the stateless MCP `2026-07-28` protocol over Streamable HTTP, and serves clients that use the 2025-era Streamable HTTP protocol through the same endpoint. | Environment | Endpoint | | ------------------- | ------------------------------- | | Production | `https://app.centrakit.com/mcp` | | Preview (`develop`) | `https://app.centrakit.dev/mcp` | | Local | `https://app.localhost/mcp` | Every brand host serves the same endpoint, for example `https://app.mechup.com/mcp`. The `/mcp` path is a rewrite to the API app's `/api/mcp` handler. The current protocol has no transport sessions and no `Mcp-Session-Id`. Legacy HTTP+SSE endpoints such as `/api/sse` and `/api/message` do not exist. Clients can call `server/discover` for the supported protocol versions, server identity (`centrakit-api`) and capabilities. `server/discover` responses carry a one-hour public cache hint. `tools/list` answers per caller (see [Scopes and permissions](#scopes-and-permissions)), so its results are private and carry no cache lifetime. Tool results are never cached. ## Authentication [#authentication] Every request to the product server needs a bearer token. 1. An unauthenticated request gets `401` with a `WWW-Authenticate` header that points to `/.well-known/oauth-protected-resource` on the same host, such as `https://app.centrakit.com/.well-known/oauth-protected-resource`. The same metadata is also served at `/.well-known/oauth-protected-resource/mcp`, the path RFC 9728 clients derive from the resource URL. 2. That metadata names the Supabase OAuth 2.1 authorization server, `https://<project-ref>.supabase.co/auth/v1`. 3. Dynamic client registration is enabled, so the MCP client registers itself, runs the authorization code flow with PKCE in your browser, and receives an access token. The browser opens the consent page at `/oauth/consent` on the app host, which asks you to sign in if needed and then shows the client name and the requested scopes. Select Allow to continue or Deny to cancel. You can revoke a client later under Profile, Connected apps. 4. The client sends the token on every request. The server verifies the token's issuer, signature, expiry and audience. Supabase OAuth tokens usually keep the default `authenticated` audience, and the server accepts it as well as the `/mcp` resource URL of every brand host, such as `https://app.centrakit.com/mcp`, and any `MCP_RESOURCE_AUDIENCE` the deployment configures. A token issued for the old `https://api.centrakit.com/api/mcp` resource is rejected unless the deployment still lists that value in `MCP_RESOURCE_AUDIENCE`. See [Authentication](/docs/developers/authentication). ### Scopes and permissions [#scopes-and-permissions] Each tool requires one OAuth scope, which must be present in the token's `scope` claim: | Tool kind | Required scope | | --------- | -------------- | | Read | `mcp:read` | | Mutation | `mcp:write` | Every mutation tool requires `mcp:write`; there are no exceptions. `getOrganizationAuditExport` reads with `mcp:read` although it shares the `auditLog.export` permission with `createOrganizationAuditExport`. A call without the scope gets `403` with `WWW-Authenticate: Bearer error="insufficient_scope", scope="mcp:write"`, so the client can ask the authorization server for the missing scope and retry. Each tool is also bound to the permission its REST route checks, such as `task.read` for `list_tasks` or `task.create` for `create_task`. `get_api_health_v1`, `get_current_user` and `list_organizations` use `profile.read`, which every signed-in user holds. `tools/list` returns only the tools your token's scopes and your role in at least one organization allow, so a viewer does not see mutation tools. The list is a hint: the REST route still decides every call with your organization permissions, so a token can never do more than its user. MCP clients act for you only on the permissions of these tools. A token with only identity scopes (`openid`, `email`, `profile`) carries no delegation, so it lists no tools and every call is refused. ### Tokens from the CLI and CI [#tokens-from-the-cli-and-ci] For local agent work, `centrakit login` runs the same PKCE flow and stores a refreshable session. For CI and hosts that inject their own credentials, pass a bearer token through `CENTRAKIT_TOKEN` or pipe it to `centrakit --token-stdin`. See [CLI](/docs/developers/cli). ## Configure a client [#configure-a-client] ### Direct connection [#direct-connection] Hosts that support Streamable HTTP and OAuth connect to the URL directly: ```json { "mcpServers": { "centrakit": { "url": "https://app.centrakit.com/mcp" } } } ``` ### `mcp-remote` [#mcp-remote] Hosts that only launch local commands can bridge through `mcp-remote`, which also handles the OAuth flow: ```json { "mcpServers": { "centrakit": { "command": "npx", "args": ["-y", "mcp-remote", "https://app.centrakit.com/mcp"] } } } ``` ### Client config files [#client-config-files] | Client | Config file | Notes | | --- | --- | --- | | Cursor | `~/.cursor/mcp.json` | Direct URL. Fall back to `mcp-remote` if OAuth fails. | | Claude Desktop | `~/Library/Application Support/Claude/claude_desktop_config.json` | Use `mcp-remote`. | | Windsurf | `~/.codeium/windsurf/mcp_config.json` | Direct URL or `mcp-remote`. | ## Tool input [#tool-input] Each tool maps to one REST operation and takes a nested input: ```json { "pathParams": { "organizationId": "1f0c2d3e-4b5a-4c6d-8e7f-9a0b1c2d3e4f" }, "query": { "limit": 50, "offset": 0, "q": "pump" }, "body": {} } ``` - `pathParams` holds the path parameters, such as `organizationId`, `taskId` or `quoteId`. Organization-scoped tools always need the organization UUID. Call `list_organizations` first to find the organizations the user can access and their IDs. - `query` is optional and holds list filters and pagination. - `body` holds the request body for mutations. Inputs are validated against the generated operation schemas before the call, and the REST route validates them again. The server then calls the matching REST operation with the same bearer token, so validation, permissions, audit logging and errors match the REST API. Results include a text fallback and `structuredContent` when the API returns JSON. Each tool also publishes `_meta` entries `com.centrakit/operation-kind` (`read` or `mutate`) and `com.centrakit/required-oauth-scope`, plus annotations: `readOnlyHint` for reads, and `destructiveHint`, `openWorldHint` and `idempotentHint` for mutations, derived from the action catalog. When the server sees that the tools you may use changed during a session, for example after a role change, it sends `notifications/tools/list_changed`. ## Tools [#tools] The catalog is a deliberate allowlist of 75 REST operations: 42 read tools and 33 mutation tools. Tool names are the OpenAPI operation IDs in snake case, so `listTasks` becomes `list_tasks`. Each tool declares an `outputSchema` built from the REST response schema, and successful calls return that shape as `structuredContent`. | Group | Read tools | Mutation tools | | --- | --- | --- | | Account | `get_api_health_v1`, `get_current_user`, `list_organizations` | | | Tasks | `list_tasks`, `get_task` | `create_task`, `update_task`, `set_task_status` | | Customers and assets | `list_customers`, `get_customer`, `listCustomerAssets`, `getCustomerAsset` | `create_customer`, `update_customer` | | Products | `list_products`, `get_product` | `create_product`, `update_product` | | Quotes | `list_quotes`, `get_quote` | `create_quote`, `update_quote` | | Tags | `list_tags`, `get_tag` | `create_tag`, `update_tag` | | Orders | `list_orders`, `get_order` | `create_order`, `confirm_order`, `cancel_order` | | Shipments | `list_shipments`, `get_shipment` | `ship_shipment` | | Purchase orders | `listPurchaseOrders`, `getPurchaseOrder` | `createPurchaseOrder`, `receivePurchaseOrder` | | Inventory and promotions | `listInventoryLevels`, `validatePromotion` | | | Agenda | `list_agenda_items`, `get_agenda_item` | `createAgendaItem`, `updateAgendaItem`, `deleteAgendaItem`, `move_agenda_item` | | Files | `list_file_drives`, `listFileNodeChildren`, `get_file_node`, `createFileDownload` | | | Workflows | `listWorkflowDefinitions`, `getWorkflowDefinition`, `listWorkflowRuns`, `get_workflow_run` | `startWorkflowRun` | | Knowledge and inbox | `retrieveKnowledge` | `sendInboxThreadReply` | | Webhook deliveries | `listWebhookDeliveries`, `getWebhookDelivery` | | | Webhook destinations | `listWebhookDestinations`, `getWebhookDestination` | `createWebhookDestination`, `updateWebhookDestination`, `deleteWebhookDestination`, `rotateWebhookDestinationSecret` | | Webhook endpoints | `listWebhookEndpoints`, `getWebhookEndpoint` | `createWebhookEndpoint`, `updateWebhookEndpoint`, `deleteWebhookEndpoint`, `rotateWebhookEndpointSecret`, `rotateWebhookEndpointToken` | | Audit log | `listOrganizationAuditEvents`, `getOrganizationAuditExport` | `createOrganizationAuditExport` | `createFileDownload` returns a signed download URL and `validatePromotion` checks a promotion code without changing anything, so both count as read tools. Operations that are not in this table are only available through the REST API. ## Approvals [#approvals] Mutation tools that map to an action in the approval catalog run the organization's approval policy before they call the API. Order, shipment and purchase order mutations run the policy inside the REST route instead, so one call raises one request. When a policy holds the call, the tool fails with code `APPROVAL_REQUIRED`, an `approvalId`, a `requirement` (`confirm` or `authorize`) and the `approverPermission` when one applies. Tell the user who has to act, and call the same tool with the same input once the request is approved. The approval covers exactly that call: the same tool, the same input, the same user and the same MCP client. An approved call runs once. Calling the same tool with the same input again fails with code `APPROVAL_REFUSED` and `reason` `approval-consumed`, and a call whose request was rejected fails with `reason` `approval-rejected`. Change the input to ask again, or wait until the original request window ends. See [Approvals](/docs/product/approvals) and [Errors](/docs/developers/errors). ## Errors [#errors] A failed tool call returns `isError: true`. Its `structuredContent` is the REST problem details document, and the text content is the same document as JSON, so the `code` values match the API. ## Usage [#usage] Each organization-scoped tool call counts once on the organization's MCP usage meter. The REST request the tool makes is not counted again as an API request. ## MCP Apps and widgets [#mcp-apps-and-widgets] Not yet available: MCP Apps resources, prompts and interactive widgets. The product server exposes tools only. ## Troubleshooting [#troubleshooting] | Symptom | Likely cause | | --- | --- | | `401 Unauthorized` on connect | No bearer token, an expired token, or a token whose audience the server does not accept. | | `403` with `error="insufficient_scope"` | The token lacks `mcp:read` or `mcp:write`. | | `tools/list` is empty | The token has no `mcp:read` or `mcp:write` scope, or your role grants none of the tools. | | Tool error `ORGANIZATION_PERMISSION_DENIED` | Your user lacks the organization permission for that operation. | | Tool error `APPROVAL_REQUIRED` | An approval policy holds the mutation. Retry after it is approved. | | Tool error `APPROVAL_REFUSED` | The approval for this exact call was rejected or already used. Change the input to ask again. | | OAuth discovery fails | `/.well-known/oauth-protected-resource` is unreachable or the authorization server is misconfigured. | | Local connection fails | The SaaS and API apps are not running. Start both with `pnpm dev:portless`. | | The old URL no longer connects | The client still uses `https://api.centrakit.com/api/mcp`. Change it to `https://app.centrakit.com/mcp`. | ## Related pages [#related-pages] - [Docs for AI agents](/docs/developers/ai-agents) - [Authentication](/docs/developers/authentication) - [REST API](/docs/developers/api) - [Errors](/docs/developers/errors) - [Webhooks](/docs/developers/webhooks) - [AI settings](/docs/admin/ai-settings) # OpenAPI (/docs/developers/openapi) Where the CentraKit OpenAPI 3.2 specification lives, how it is generated from route JSDoc and Zod schemas, and how the TypeScript client, reference pages and drift check are built from it. The CentraKit REST API is described by one OpenAPI 3.2 document. It is generated from the route handlers in `apps/api`, committed to git, and used to build the TypeScript client, the MCP tool catalog, the CLI `api` command and the API reference pages in these docs. ## Where to find it [#where-to-find-it] | Format | Location | | --- | --- | | Raw spec | Served at `/openapi.json` on the app host and committed as `apps/api/public/openapi.json`. | | Docs reference pages | The OpenAPI section of these docs, one page per operation. `/api-docs` on the app host redirects to [REST API](/docs/developers/api). | The spec currently has 175 paths and 259 operations in 36 tags. Paths start with `/v1`, and the servers are `https://app.centrakit.com/api` and `https://app.localhost/api`, so full URLs look like `https://app.centrakit.com/api/v1/health`. ## Security schemes [#security-schemes] | Scheme | Meaning | | --- | --- | | `SessionCookie` | The Supabase session cookie set by the SaaS app or the login route. | | `BearerAuth` | `Authorization: Bearer <access-token>`. | Most operations accept either. See [Authentication](/docs/developers/authentication). ## How the spec is generated [#how-the-spec-is-generated] The pipeline has two steps, both run by one command: ```bash pnpm openapi:generate ``` 1. `pnpm --filter api openapi:generate` runs `openapi-gen generate` from `next-openapi-gen` with `apps/api/openapi-gen.config.mts`. It scans `apps/api/app/api` and writes `apps/api/public/openapi.json`. 2. `pnpm --filter @workspace/api-client generate` runs Hey API (`@hey-api/openapi-ts`) over that file and writes the client to `packages/api-client/src/generated`. ### Route JSDoc [#route-jsdoc] Only route handlers with an `@openapi` tag in their JSDoc are included. Request and response shapes come from Zod schemas, which the generator resolves by name from `apps/api/app/api` and the shared contract packages listed in the config's `schemaDir`. A typical handler: ```ts /** * List tags. * @description Return tags for an organization, optionally filtered by kind. * @operationId listTags * @tag Tags * @auth SessionCookie,bearer * @pathParams tagsParamsSchema * @queryParams tagsOpenApiListQuerySchema * @response 200:tagsListResponseSchema * @responseDescription Returns paginated tags. * @responseSet resource * @openapi */ export const GET = withApiRoute(/* ... */); ``` | Tag | Purpose | | --- | --- | | `@operationId` | Stable operation name, used by the client, the CLI and MCP. | | `@tag` | Group in the reference. Tags are declared in the config. | | `@description` | Longer operation description. | | `@auth` | Accepted security schemes. | | `@pathParams` | Zod schema for path parameters. | | `@queryParams` | Zod schema for query parameters. | | `@body` or `@requestBody` | Zod schema for the request body. `@requestBody ... required` marks it required. | | `@bodyDescription` | Request body description. | | `@contentType` | Non-JSON request content type, such as `multipart/form-data` uploads. | | `@response` | Success status and Zod schema, such as `201:organizationTagSchema`. | | `@responseDescription` | Success response description. | | `@responseSet` | Which shared error responses to add. | | `@openapi` | Includes the handler in the spec. | ### Error responses [#error-responses] Error responses use problem details, `application/problem+json` with the `problemDetailsSchema` component. `@responseSet` picks the statuses that are added to an operation: | Set | Statuses | | --------------- | ---------------------------- | | `public` | 400, 500. The default. | | `auth` | 400, 401, 403, 500 | | `organizations` | 400, 401, 403, 404, 409, 500 | | `resource` | 400, 401, 403, 404, 409, 500 | Each shared response includes an example whose `type` points to a code on [Errors](/docs/developers/errors). The generator runs with diagnostics set to fail on errors, so a broken schema reference stops generation. ## TypeScript client [#typescript-client] `@workspace/api-client` is the generated client. Hey API runs with three plugins: - `@hey-api/typescript` for request and response types; - `zod` for request and response schemas; - `@hey-api/sdk` for one function per operation, returning `{ data, error }` fields and validating requests with the Zod schemas. | Export | Contents | | --- | --- | | `@workspace/api-client` | `createApiClient`, `requestApiOperation`, `apiSdk`, types, Zod schemas and the operation catalog. | | `@workspace/api-client/generated/sdk` | The generated operation functions. | | `@workspace/api-client/generated/zod` | The generated Zod schemas. | | `@workspace/api-client/operations` | `getOperationById` and `getOperationCatalog`. | | `@workspace/api-client/mcp` | The MCP tool definitions and catalog, and `executeMcpApiOperation`, which the MCP server uses to call operations. | ```ts import { apiSdk, createApiClient } from "@workspace/api-client"; const client = createApiClient({ baseUrl: "https://app.centrakit.com/api", accessToken, }); const { data, error } = await apiSdk.listTasks({ client, path: { organizationId }, query: { limit: 50 }, }); ``` `requestApiOperation` calls an operation by its runtime operation ID. The CLI `api` command uses it. It is untyped on purpose, because Hey API does not emit an operation ID map that could type a dynamic call. The client package is internal to the monorepo and is not published to npm. External integrations can generate their own client from `openapi.json` with any OpenAPI 3.2 generator. ### Generation details [#generation-details] Hey API 0.99 needs the classic TypeScript compiler API, so the generate script loads `scripts/register-typescript-classic.mjs`, which redirects `typescript` imports to the `typescript-classic` package during generation only. The committed output is type-checked by the repository's TypeScript 7 compiler with strict settings, including `exactOptionalPropertyTypes`. The generated fetch runtime passes explicit `undefined` for a few optional fields. `scripts/postprocess-generated-runtime.mjs` replaces those known assignments in the generated `client` and `core` runtime files with conditional properties. The transform is idempotent, has its own tests, and fails generation when the upstream code no longer matches. The SDK functions, types and Zod schemas are not changed. ## Reference pages in these docs [#reference-pages-in-these-docs] The docs app reads `apps/api/public/openapi.json` with `fumadocs-openapi` and generates one page per operation under the OpenAPI section. Each page also has a Markdown version that contains only that page's operations and the component schemas they reference, which keeps it small enough for AI agents. See [Docs for AI agents](/docs/developers/ai-agents). ## Keeping the spec in sync [#keeping-the-spec-in-sync] After you change route JSDoc, a route's Zod schema or a shared contract schema: 1. Run `pnpm openapi:generate`. 2. Check that the named components you touched are not emitted as `{}` in `openapi.json`. An empty component means the generator could not resolve the schema, usually because its file is outside `schemaDir`. 3. Update the MCP tool catalog if the operation is exposed there. See [MCP server](/docs/developers/mcp). 4. Commit `apps/api/public/openapi.json` and `packages/api-client/src/generated` together with the route change. CI runs an OpenAPI drift check in the verify workflow: ```bash pnpm openapi:generate git diff --exit-code -- apps/api/public/openapi.json packages/api-client/src/generated ``` The check fails when the committed artifacts do not match a fresh generation. Run the same commands locally before you push. See [CI and release](/docs/operations/ci-and-release). ## Known gaps [#known-gaps] - Some routes are not in the spec: the assistant and AI chat routes, the channel provider webhooks and the scheduled job routes. See [REST API](/docs/developers/api). - Several list operations declare `limit` and `offset` as strings instead of integers, so the generated client types them as strings: `listTags`, `listCustomerAssets`, `listNotifications`, `listTemplates`, the webhook delivery, destination and endpoint lists, the workflow definition and run lists, and the inventory allocation, movement and stock record lists. `listOrganizationAuditEvents` has the same issue for `offset`. ## Related pages [#related-pages] - [REST API](/docs/developers/api) - [Errors](/docs/developers/errors) - [CLI](/docs/developers/cli) - [MCP server](/docs/developers/mcp) - [Docs for AI agents](/docs/developers/ai-agents) # Webhooks (/docs/developers/webhooks) Send signed CentraKit events to your HTTPS endpoints and receive external webhooks that start workflows, including signatures, secret and token rotation, delivery retries, and the REST and MCP operations. CentraKit has two kinds of webhooks per organization: - Outgoing destinations receive signed `POST` requests when events happen in CentraKit. Events are queued for delivery within about a minute of the change. - Incoming endpoints give an external system a URL to call. Each call publishes a `webhook.received` event that starts matching workflows. Manage both in the SaaS app under Integrations, on the Webhooks integration. It lists Incoming endpoints and Outgoing destinations, with New incoming webhook and New outgoing webhook actions that open `/{orgSlug}/integrations/webhooks/incoming/create` and `/{orgSlug}/integrations/webhooks/outgoing/create`. Everything is also available through the REST API and the MCP server. ## Key concepts [#key-concepts] | Concept | Description | | --- | --- | | Destination | An outgoing target: name, HTTPS URL, subscribed event kinds and an enabled flag. It has a `whsec_` signing secret. | | Endpoint | An incoming receiver: name, verification, optional bound workflow definition and an enabled flag. It has a URL token and a secret. | | Delivery | One queued outgoing request to a destination, with its attempts and the last observed response. | | Event kind | The event name a destination subscribes to, such as `invoice.paid` or `task.completed`. | ## Outgoing destinations [#outgoing-destinations] ### Create a destination [#create-a-destination] ```bash curl -X POST \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ https://app.centrakit.com/api/v1/organizations/$ORG_ID/webhooks/destinations \ -d '{ "name": "ERP sync", "url": "https://erp.example.com/hooks/centrakit", "eventKinds": ["invoice.paid", "order.shipped"] }' ``` The URL must use HTTPS. The `201` response includes the destination and its `secret`. Store the secret now: list and get responses never return it, and only an explicit rotation returns a new one. `eventKinds` defaults to an empty list. A destination with no event kinds receives no event deliveries; it only gets test sends and workflow webhook actions, and the overview shows it as Workflow only. An event kind matches exactly, `*` matches every event, and a prefix pattern such as `invoice.*` matches every invoice event. ### Event kinds [#event-kinds] The destination form offers these event kinds, which are the same domain events workflows can trigger on: | Area | Event kinds | | --- | --- | | Quotes | `quote.sent`, `quote.viewed`, `quote.accepted`, `quote.rejected`, `quote.changes-requested`, `quote.expired`, `quote.withdrawn`, `quote.deposit-paid`, `quote.invoiced` | | Invoices | `invoice.sent`, `invoice.viewed`, `invoice.overdue`, `invoice.reminder-sent`, `invoice.partially-paid`, `invoice.paid`, `invoice.uncollectible`, `invoice.cancelled`, `invoice.payment-failed`, `invoice.disputed`, `invoice.refunded`, `invoice.dispute-lost`, `invoice.reminders-paused`, `invoice.reminders-resumed` | | Orders | `order.created`, `order.confirmed`, `order.paid`, `order.backordered`, `order.shipped`, `order.ready-for-pickup`, `order.delivered`, `order.picked-up`, `order.fulfilled`, `order.invoiced`, `order.completed`, `order.returned`, `order.cancelled` | | Shipments | `shipment.shipped`, `shipment.out-for-delivery`, `shipment.ready-for-pickup`, `shipment.delivered`, `shipment.exception` | | Purchase orders | `purchase-order.sent`, `purchase-order.in-transit`, `purchase-order.partially-received`, `purchase-order.received`, `purchase-order.overdue`, `purchase-order.cancelled` | | Inventory | `inventory.low-stock`, `inventory.out-of-stock`, `inventory.back-in-stock`, `inventory.level-changed` | | Promotions | `promotion.expiring`, `promotion.limit-reached` | | Tasks | `task.created`, `task.assigned`, `task.status-changed`, `task.stage-changed`, `task.completed`, `task.due-soon`, `task.overdue` | | Agenda | `agenda.item.created`, `agenda.item.updated`, `agenda.item.deleted` | | Customers | `customer.created`, `customer.lookup.requested` | | Other | `knowledge.file.ingest-requested`, `product.lookup.requested`, `inbox.message.received`, `billing.usage-threshold`, `workflow.run.failed`, `webhook.received` | The API accepts any event kind string up to 160 characters, but only the kinds above are published. Two more kinds appear in delivery logs without a subscription: `webhook.test` for the Send test action on a destination, and `workflow.action` for a workflow step that sends a webhook to a destination. ### Request format [#request-format] Deliveries follow [Standard Webhooks](https://www.standardwebhooks.com). Each one is a `POST` to the destination URL: ```http POST /hooks/centrakit HTTP/1.1 Content-Type: application/json webhook-id: 9b2f0c1e-6a3d-4f7e-8b19-2c4d5e6f7a80 webhook-timestamp: 1790000000 webhook-signature: v1,K5oZfzN95Z9UVu1EsfQmfVNQhnkZ2pj9o9NDN/H/pI4= {"type":"invoice.paid","timestamp":"2026-10-06T09:30:00Z","data":{"invoiceId":"...","status":"paid"}} ``` | Header | Value | | --- | --- | | `Content-Type` | `application/json` | | `webhook-id` | The delivery ID. It stays the same across retries of one delivery. | | `webhook-timestamp` | Unix time in seconds when the request was signed. | | `webhook-signature` | `v1,` followed by the base64 HMAC-SHA256 of `{id}.{timestamp}.{body}`, one per active secret, separated by spaces while a rotation overlaps. | The body is the Standard Webhooks envelope: `type` is the event kind, `timestamp` is when the delivery was queued, and `data` is the event payload. ### Verify the signature [#verify-the-signature] Any Standard Webhooks library verifies these requests, for example the `standardwebhooks` package: ```ts import { Webhook } from "standardwebhooks"; const webhook = new Webhook(process.env.CENTRAKIT_WEBHOOK_SECRET); export async function POST(request: Request) { const body = await request.text(); const event = webhook.verify(body, Object.fromEntries(request.headers)); // event.type, event.data return new Response(null, { status: 204 }); } ``` The secret starts with `whsec_`. The library rejects timestamps older than five minutes. Use `webhook-id` to ignore a delivery you already processed. ### Deliveries and retries [#deliveries-and-retries] Events are queued as deliveries for every enabled destination subscribed to the event kind, within about a minute of the event. The `webhook_deliveries` background job runs every minute, claims due deliveries in batches of 25 with a two-minute lease, sends 10 at a time, and records the outcome: | Outcome | Result | | --- | --- | | `2xx` response | `completed`. | | `408`, `429` or `5xx` response, or a network error | `failed`, retried on the schedule below. | | Any other response | `dead_lettered`. No retry. | | URL blocked by the outbound request policy, or a redirect | `dead_lettered`. No retry. | | The eighth failed attempt | `dead_lettered`. | | Destination disabled when the delivery is due | `canceled`. | Retries wait 5 seconds, 5 minutes, 30 minutes, 2 hours, 5 hours and then 10 hours, each with random jitter up to that wait. A `Retry-After` header asks for a longer wait, up to a day. Only public HTTPS addresses are reached, redirects are not followed, and at most 64 KiB of a response is read. CentraKit stores the response status, the first 2,000 characters of the response body, the error and the duration on each delivery. A destination whose deliveries keep failing for five days without a success is disabled, and its pending deliveries are canceled. `failingSince`, `disabledAt` and `disabledReason` on the destination show why. Enabling it again clears them. Delivery statuses are `pending`, `processing`, `completed`, `failed`, `dead_lettered` and `canceled`. In the app, the deliveries table on a destination has a Retry action that queues a finished delivery again from its first attempt; a delivery that is still pending or processing cannot be retried, and neither can one of a disabled destination. Send test queues a `webhook.test` delivery with `{ "kind": "webhook.test", "sentAt": "..." }`. A workflow webhook step queues a `workflow.action` delivery to its destination with the step's body and returns `deliveryId` and `status: "queued"`; the delivery job sends it like any other delivery. ### Rotate a destination secret [#rotate-a-destination-secret] `POST /webhooks/destinations/{destinationId}/rotate-secret` creates a new secret and returns it once. The previous secret keeps signing for 24 hours next to the new one, so receivers can switch without missing a delivery. Signing secrets are stored in Supabase Vault. ## Incoming endpoints [#incoming-endpoints] ### Create an endpoint [#create-an-endpoint] ```bash curl -X POST \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ https://app.centrakit.com/api/v1/organizations/$ORG_ID/webhooks/endpoints \ -d '{ "name": "Shop orders", "verify": "standard-webhooks" }' ``` The `201` response includes the endpoint, its `token` and its `secret`. The receiving URL is: ```text https://app.centrakit.com/api/v1/public/webhooks/{token} ``` The host is the app host of the organization's brand, such as `app.mechup.com` for a MechUp organization. Receiving URLs created before the move to brand hosts used `api.centrakit.com`, which no longer exists, so update them in the sending system. CentraKit stores only a hash of the token, so the URL cannot be shown again. List and get responses return neither the token nor the secret; only creation, a change of verification and the rotations return them. Pass `workflowDefinitionId` to bind the endpoint to one workflow. `verify` decides how a delivery proves where it came from: | `verify` | A delivery must carry | | --- | --- | | `standard-webhooks` (default) | A [Standard Webhooks](https://www.standardwebhooks.com) signature made with the endpoint's `whsec_` secret. | | `hmac-sha256` | The hex HMAC-SHA256 of the body, optionally prefixed with `sha256=`, in the `signatureHeader` header (`x-signature`). | | `shared-secret` | The endpoint's secret itself in the `signatureHeader` header (`x-webhook-secret`), for senders that can set a header but cannot sign. | | `none` | Only the token in the URL. Use it for senders that cannot sign; the URL is then the credential and must be kept secret. | Pass `signatureHeader` to name another header for `hmac-sha256` or `shared-secret`. A shared secret travels with every request, so it protects against a leaked URL but not against someone who can read the traffic; CentraKit compares it in constant time and never stores the header. ### Change an endpoint [#change-an-endpoint] `PATCH /webhooks/endpoints/{endpointId}` changes the `name`, binds the endpoint to another workflow with `workflowDefinitionId` (`null` removes the binding), changes `verify` or `signatureHeader`, or sets `enabled`. The URL stays the same. When `verify` changes to a signing mode, the response carries the new `secret` once and the previous secret stops working at once; otherwise `secret` is `null`. ### Send to an endpoint [#send-to-an-endpoint] The receiver accepts a `POST` with a body of at most 256 KB. With `standard-webhooks`, sign it as a Standard Webhooks sender does, for example with the `standardwebhooks` package: ```ts import { Webhook } from "standardwebhooks"; const webhook = new Webhook(process.env.CENTRAKIT_ENDPOINT_SECRET); const id = crypto.randomUUID(); const timestamp = new Date(); const body = JSON.stringify({ orderId: "ord_123" }); await fetch(`https://app.centrakit.com/api/v1/public/webhooks/${token}`, { method: "POST", headers: { "Content-Type": "application/json", "webhook-id": id, "webhook-timestamp": String(Math.floor(timestamp.getTime() / 1000)), "webhook-signature": webhook.sign(id, timestamp, body), }, body, }); ``` The timestamp must be within five minutes of the server clock. With `hmac-sha256`: ```bash BODY='{"orderId":"ord_123"}' SIG=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$WEBHOOK_SECRET" -hex | sed 's/^.* //') curl -X POST "https://app.centrakit.com/api/v1/public/webhooks/$WEBHOOK_TOKEN" \ -H "Content-Type: application/json" \ -H "x-signature: sha256=$SIG" \ -d "$BODY" ``` A verified request is stored and answers `202`; a request with a `webhook-id` that arrived before answers `200` and is stored once: ```json { "id": 4182, "duplicate": false } ``` ### What happens with the payload [#what-happens-with-the-payload] Stored deliveries are processed by the `webhook_inbox` background job, which runs every minute and retries a delivery that fails. - A JSON body is used as is; non-JSON text becomes `{ "body": "<text>" }`, and a JSON value that is not an object becomes `{ "body": "<json>" }`. - CentraKit adds `endpointId` and publishes a `webhook.received` event for the organization. The `Idempotency-Key` header, or else the delivery, makes the event unique. - Workflows with a webhook trigger start from that event. If the endpoint is bound to a workflow definition, its published version starts too. - Outgoing destinations subscribed to `webhook.received` receive the payload as well. Each endpoint tracks `receiveCount`, `lastReceivedAt` and `lastStatus`, the HTTP status of its last answer. An organization export includes its endpoints without the token hash and the secrets, and deleting the organization deletes them with their secrets. ### Receiver errors [#receiver-errors] | Status | Code | Cause | | --- | --- | --- | | 401 | `UNAUTHORIZED` | Missing or wrong signature, or a stale timestamp. | | 404 | `NOT_FOUND` | Unknown token, or the endpoint is disabled. | | 413 | `PAYLOAD_TOO_LARGE` | The body is larger than 256 KB. | | 429 | `RATE_LIMITED` | More than 60 requests per minute for one endpoint. The `retry-after` header says when to try again. | | 500 | `FAILED` | The delivery could not be stored. | ### Rotate the token or the secret [#rotate-the-token-or-the-secret] - `POST /webhooks/endpoints/{endpointId}/rotate-token` returns a new token and invalidates the previous URL immediately. - `POST /webhooks/endpoints/{endpointId}/rotate-secret` returns a new secret once and keeps the URL. Pass `{ "graceHours": 24 }` (0 to 168) to let the previous secret verify deliveries for that long, so the sender can switch without dropped deliveries; `previousSecretExpiresAt` says when it stops. Without a grace period the previous secret stops at once. An endpoint with `verify: "none"` has no secret to rotate. ## Permissions [#permissions] | Permission | Allows | | --- | --- | | `webhook.read` | Listing and reading destinations, endpoints and deliveries. | | `webhook.create` | Creating destinations and endpoints. A destination also needs `webhook.update`, which issues its secret. | | `webhook.update` | Updating, renaming and rebinding, enabling and disabling, rotating secrets and tokens, sending tests and retrying. | | `webhook.delete` | Deleting destinations and endpoints. | ## Automation and AI [#automation-and-ai] The [MCP server](/docs/developers/mcp) exposes every webhook operation as a tool: - Deliveries: `listWebhookDeliveries`, `getWebhookDelivery` - Destinations: `listWebhookDestinations`, `createWebhookDestination`, `getWebhookDestination`, `updateWebhookDestination`, `deleteWebhookDestination`, `rotateWebhookDestinationSecret` - Endpoints: `listWebhookEndpoints`, `createWebhookEndpoint`, `getWebhookEndpoint`, `updateWebhookEndpoint`, `deleteWebhookEndpoint`, `rotateWebhookEndpointSecret`, `rotateWebhookEndpointToken` The mutation tools require the `mcp:write` scope and can be held for approval. See [Workflows](/docs/product/workflows) for webhook triggers and the send-webhook action. ## API [#api] Paths are relative to `/api/v1/organizations/{organizationId}`. | Operation | Method and path | | --- | --- | | `listWebhookDestinations` | `GET /webhooks/destinations` | | `createWebhookDestination` | `POST /webhooks/destinations` | | `getWebhookDestination` | `GET /webhooks/destinations/{destinationId}` | | `updateWebhookDestination` | `PATCH /webhooks/destinations/{destinationId}` | | `deleteWebhookDestination` | `DELETE /webhooks/destinations/{destinationId}` | | `rotateWebhookDestinationSecret` | `POST /webhooks/destinations/{destinationId}/rotate-secret` | | `listWebhookEndpoints` | `GET /webhooks/endpoints` | | `createWebhookEndpoint` | `POST /webhooks/endpoints` | | `getWebhookEndpoint` | `GET /webhooks/endpoints/{endpointId}` | | `updateWebhookEndpoint` | `PATCH /webhooks/endpoints/{endpointId}` | | `deleteWebhookEndpoint` | `DELETE /webhooks/endpoints/{endpointId}` | | `rotateWebhookEndpointSecret` | `POST /webhooks/endpoints/{endpointId}/rotate-secret` | | `rotateWebhookEndpointToken` | `POST /webhooks/endpoints/{endpointId}/rotate-token` | | `listWebhookDeliveries` | `GET /webhooks/deliveries` (optional `destinationId` filter) | | `getWebhookDelivery` | `GET /webhooks/deliveries/{deliveryId}` | | `receivePublicWebhook` | `POST /api/v1/public/webhooks/{token}` (no session) | List operations use the standard `offset` and `limit` pagination. Each operation has a generated reference page; see [OpenAPI](/docs/developers/openapi). ## Related pages [#related-pages] - [Integrations](/docs/product/integrations) - [Workflows](/docs/product/workflows) - [REST API](/docs/developers/api) - [Errors](/docs/developers/errors) # Deployment (/docs/getting-started/deployment) How the SaaS app, the API and the docs deploy as services in one Vercel project, including the brand hosts, the path rewrites and the background jobs cron in vercel.json. CentraKit is deployed as three independently built services (`saas`, `api` and `docs`) in one Vercel project rooted at the repository root. The root `vercel.json` owns service definitions, public routing, and cron schedules. Shared packages continue to build through Turborepo. `apps/marketing` and `apps/showcase` stay in the repository but are not deployed. Each service installs with `pnpm install --filter <app>...` and builds with `turbo run build --filter=<app>`. Per-service `ignoreCommand` runs pinned `turbo-ignore` (with `--turbo-version` so the pnpm catalog pin does not break version inference) so unaffected apps skip the build (for example an api-only change rebuilds `api`, and `docs` when OpenAPI or `docs/**` inputs change, but not `saas`). Never list `.git` in `.vercelignore`. Selective skips and Turbo remote cache need git metadata on the build machine. Keep ignoring agent trees, `tests`, `apps/cli`, and other non-runtime paths instead. `saas` and `api` both enable the Workflow SDK. Their Turbo `build` outputs include `app/.well-known/workflow/**`, and each build command sets a distinct `WORKFLOW_QUEUE_NAMESPACE` so the shared deployment does not register duplicate `__wkf_workflow_*` queue consumers. Production builds use `saas` and `api`; every other environment appends `preview` (`saaspreview`, `apipreview`). ## Brand Hosts [#brand-hosts] Every product brand has one app host, and all three services answer on it by path. The brand registry in `packages/foundation/src/brands.ts` lists the hosts: | Brand | Production (`main`) | Preview (`develop`) | | ---------- | -------------------- | -------------------------- | | CentraKit | `app.centrakit.com` | `app.centrakit.dev` | | MechUp | `app.mechup.com` | `mechup.centrakit.dev` | | StableDock | `app.stabledock.app` | `stabledock.centrakit.dev` | | Caresly | `app.caresly.app` | `caresly.centrakit.dev` | | PropDock | none yet | `propdock.centrakit.dev` | | MarryDuck | none yet | `marryduck.centrakit.dev` | | SignalDock | none yet | `signaldock.centrakit.dev` | | Spacelio | none yet | `spacelio.centrakit.dev` | | LegalDock | none yet | `legaldock.centrakit.dev` | | Service | App root | Paths | | --- | --- | --- | | `api` | `apps/api` | `/api`, `/mcp` and `/.well-known/oauth-protected-resource`: REST API, MCP, OAuth, crons | | `docs` | `apps/docs` | `/docs` (`basePath: "/docs"`), `/llms.txt` and `/llms-full.txt` | | `saas` | `apps/saas` | Every other path: the signed-in app, auth pages, the customer portal and `/admin` | The apex and `www` domains of each brand point at its public website on Lovable through DNS and are not part of the deployment. See [Brand hosts and rollout](/docs/operations/brand-hosts) for adding a brand and the provider checklist. Only `develop` and `main` deploy. `git.deploymentEnabled` in `vercel.json` turns deployments off for every other branch, including `changeset-release/*`. ## Environment Variables [#environment-variables] Use the single project's Vercel-managed environment for all services. Shared values such as the Supabase connection and Sentry are configured once. Public URLs come from the brand registry for the environment that `VERCEL_ENV` names, so deployments need no `NEXT_PUBLIC_SITE_URL`. AI Gateway uses OIDC (`VERCEL_OIDC_TOKEN`). Run `pnpm env:pull` from the repository root to refresh `.env.local` (development) and `.env.production.local` (production). Turborepo filters build-time variables through each app's `turbo.json`; add every build-time secret there when an app needs it during `build`, `typecheck`, or another task. Project environment variables are visible to every service. Vercel Services do not provide a general per-service environment allowlist, so treat server-only credentials as project-wide secrets. Use a separate Vercel project when a surface requires an independent credential or deployment-protection boundary. ## Function Region [#function-region] The first-party project runs Vercel Functions in `fra1` (Frankfurt) to match its Supabase `eu-central-1` region. Keep the function and database regions aligned. Static assets remain globally distributed by Vercel's CDN. ## Public Routing [#public-routing] The rewrites in `vercel.json` run in order and apply on every host: - `/portal/calendars/:token` and `/portal/files/:token` go to the API's public calendar and file routes under `/api/v1/public/`. - `/api/:path*` and `/.well-known/oauth-protected-resource` go to the API service. - `/mcp` goes to the API's `/api/mcp` handler. - `/docs/:path*` goes to the docs service, and `/llms.txt` and `/llms-full.txt` go to `/docs/llms.txt` and `/docs/llms-full.txt`. - The final catch-all sends everything else to SaaS. `/api-docs` redirects to [REST API](/docs/developers/api). The SaaS proxy maps the host to a brand and rewrites onto the hidden `[locale]/[brand]` route segments. A host that is not in the registry, including a generated deployment URL, renders the default brand. ## Crons [#crons] `vercel.json` defines one cron: `/api/v1/internal/jobs/drain`, every minute. Vercel calls it with `GET` on the production deployment, and the `/api/:path*` rewrite routes it to the API service. The route runs the background jobs of the better-supabase jobs block on Supabase Queues (pgmq): it queues the jobs whose schedule is due, then works every queue for up to 240 seconds. Schedules live in `better_supabase.job_schedules`, each with its own time zone; the system schedules below are kept in `apps/api/lib/jobs/schedules.ts` and written by the drain route itself. | Job | Schedule (UTC) | What it does | | --- | --- | --- | | `outbox_relay` | Every minute | Relays new workflow events from the outbox to automations, lifecycles and outgoing webhook deliveries. | | `webhook_inbox` | Every minute | Processes the stored Stripe, Resend and WhatsApp webhooks. | | `webhook_deliveries` | Every minute | Sends due outbound webhook deliveries. | | `product_scan_expiry` | Every minute | Settles product scan requests whose run never finished and removes their images. | | `calendar_sync_sweep` | Every 5 minutes | Queues one `calendar_sync` job per connected calendar that is due. | | `file_zip_export_expiry` | Every 5 minutes | Expires finished ZIP downloads and removes their objects. | | `support_session_expiry` | Every 5 minutes | Ends support sessions (View as user) past their expiry and records `support.ended` with `ended_by` `expired`. | | `approvals_sweep` | Every 10 minutes | Escalates and expires pending approval requests and sends the matching notifications. | | `file_upload_reconciliation` | Every 15 minutes | Expires stale file uploads and removes their Storage objects. | | `blocks_purge` | Every 15 minutes | Purges expired idempotency keys, processed webhooks, archived jobs, expired data exports and organizations past their deletion grace period, anonymizes candidates past their retention date and removes their CVs. | | `usage_stripe_report` | Hourly at :23 | Reports usage above the plan quotas to Stripe as meter events. | | `platform_fee_invoicing` | Daily at 05:17 from the 2nd to the 14th | Issues the platform fee invoices and credit notes of the previous month and emails them. It only issues what is missing, so a failed run is retried before the 15th. | | `audit_log_retention` | Daily at 02:17 | Starts the audit log retention workflow. | | `people_time_off_accrual`, `people_sick_leave_follow_up`, `people_timer_stop`, `people_attendance_close` | Daily at 06:07 | Time-off accrual, sick leave follow-up, and forgotten timers and attendance, one job each. | | `commerce_daily` | Daily at 06:17 | Publishes overdue purchase order and promotion events. See [Workflows](/docs/operations/workflows). | Other jobs are queued by the data they work on: `calendar_sync` (one calendar), `file_zip_export` (a ZIP download too large to build in the request, queued when the export is created), `inventory_signals` (one product variant whose stock moved, 30 seconds after the first movement, so a burst of movements publishes one inventory level event), `people_reminders` (one expiring document or ending contract, at 06:07 UTC on its reminder date) and `workflow_schedule` (one firing of a published workflow schedule trigger, in the trigger's time zone). Publishing, archiving or deleting a definition with a schedule trigger updates its `workflow_schedule` schedules in the same transaction, through a database trigger that calls `schedule_job`. A failed job is retried with backoff and kept as a dead letter after its last attempt. The drain route reports to the `api-jobs-drain` Sentry Cron monitor. Set `CRON_SECRET` (at least 32 characters) in the project environment. Without it, the route answers with a `CRON_UNAVAILABLE` problem response instead of running. Keep the `/api/:path*` rewrite ahead of the SaaS catch-all. ## Generated Artifacts [#generated-artifacts] - API OpenAPI output lives at `apps/api/public/openapi.json`. - The typed API client is generated into `packages/api-client`. - The docs app combines authored root docs with generated OpenAPI pages at runtime. Regenerate committed API artifacts after route handler or schema documentation changes. # Local Development (/docs/getting-started/local-development) Install CentraKit, start the local Supabase stack and the apps with or without Portless, and sign in with the seeded personas. ## Prerequisites [#prerequisites] - Node.js 24 and pnpm 12, as pinned in the root `package.json`. - macOS 14 or later on Apple silicon, or Linux. `pnpm supabase:start` runs the local Supabase stack on the CLI's native runtime, so no container engine is needed. - A root `.env.local`. With the repository linked to Vercel, `pnpm env:pull` writes `.env.local` (development) and `.env.production.local` (production). Without a link, copy `.env.example` to `.env.local` and fill in the required values. Install dependencies from the repository root: ```bash pnpm install ``` Every app reads the root env files through `loadMonorepoEnv`, so there are no per-app `.env` files. See [Environment variables](/docs/operations/environment-variables). ## Local overrides [#local-overrides] Keep local-only values in `.env.development.local`. `pnpm env:pull` rewrites `.env.local` but leaves this file alone. `pnpm dev:hosted` starts the SaaS app and the API against the hosted development project instead of the local stack: values in `.env.local` take precedence over `.env.development.local`. `pnpm env:local` rewrites the local Supabase values in `.env.development.local` without restarting the stack. `pnpm supabase:start` writes the Supabase values into this file for the local project it started: `NEXT_PUBLIC_SUPABASE_URL`, `NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY`, `SUPABASE_SECRET_KEY`, `POSTGRES_URL` (direct Postgres for the API's idempotency keys, rate limits and scheduled jobs), and `SMTP_HOST` and `SMTP_PORT` for Mailpit. It replaces those keys in place and keeps every other line. Run `pnpm --filter @workspace/supabase-runtime supabase:env` to write them again without restarting. ```bash # Leave NEXT_PUBLIC_SITE_URL unset, or set it to https://localhost, # so the default brand resolves to https://app.localhost. # NEXT_PUBLIC_SITE_URL=https://localhost ``` ## Supabase [#supabase] Start the local stack, then apply the schema and seeds: ```bash pnpm supabase:start pnpm supabase:sync ``` `pnpm supabase:start` generates a local JWT signing key if needed, starts the local Supabase project and writes its URLs and keys to `.env.development.local`. `pnpm supabase:sync` pushes pending migrations and seeds to the local database, falls back to a full reset when the push fails, and then seeds the Storage buckets. Use `pnpm supabase:reset` only when you want a clean database. It drops all local data and replays every migration and seed. After a reset, sign in again, because existing browser sessions refer to the previous auth database. The local stack runs Postgres, Auth, Storage, Realtime and `imgproxy`, so Storage `/render/image` URLs behave like hosted image transforms. The `pooler` service is intentionally stopped. The project runs on the Supabase CLI stack engine (`[experimental] stack = true` in `supabase/config.toml`): Postgres starts right away and every other service starts on its first request and stops again when idle. Each git worktree and branch gets its own project with its own ports and data. More detail is in [Supabase local dev](/docs/operations/supabase-local-dev). ## Apps [#apps] | Command | Starts | | --- | --- | | `pnpm dev:portless` | SaaS and API on `https://app.localhost` and `https://api.localhost`. | | `pnpm dev` | SaaS and API with plain `next dev` on localhost ports. | | `pnpm dev:api:portless` | Only the API, on `https://api.localhost`. | | `pnpm dev:api` | Only the API, with plain `next dev`. | | `pnpm dev:docs:portless` | The API and the docs site on `https://docs.localhost/docs`. | | `pnpm dev:marketing:portless` | The API and the marketing site. | | `pnpm dev:showcase:portless` | Only the showcase. | | `pnpm dev:all:portless` | Every app that has a `dev:portless` task. | Use the Portless commands for browser testing. The SaaS app calls the API for chat, inbox, AI suggestions and other features, which is why `pnpm dev` and `pnpm dev:portless` always start both. The plain `next dev` commands are useful in cloud and container environments where Portless is not available. Each of the docs, marketing and showcase commands also has a variant without `:portless`. The docs app is mounted at `/docs` through `basePath`, the same path it has on the app host in production, so open `https://docs.localhost/docs` when you run it on its own. In production the API, the MCP server and the docs are the `/api`, `/mcp` and `/docs` paths on each brand's app host, and so is the OAuth protected resource metadata at `/.well-known/oauth-protected-resource`. Locally there are no `vercel.json` rewrites, so the SaaS server forwards the same paths to `https://api.localhost` and `https://docs.localhost`. `https://app.localhost/api/v1/health` and `https://app.localhost/.well-known/oauth-protected-resource` reach the API, and `https://app.localhost/docs` works while the docs server runs. Set `LOCAL_API_ORIGIN` when the API runs somewhere else, as the E2E suite does with `http://127.0.0.1:3101`. The marketing and showcase apps are not deployed; their commands remain for local work. ### Brand hosts [#brand-hosts] One SaaS server serves every brand, and the brand comes from the request host. `pnpm dev:portless` registers eight Portless aliases that point at the SaaS port, so the local brand hosts are: | Brand | Local host | | ---------- | ------------------------------ | | CentraKit | `https://app.localhost` | | MechUp | `https://mechup.localhost` | | StableDock | `https://stabledock.localhost` | | Caresly | `https://caresly.localhost` | | PropDock | `https://propdock.localhost` | | MarryDuck | `https://marryduck.localhost` | | SignalDock | `https://signaldock.localhost` | | Spacelio | `https://spacelio.localhost` | | LegalDock | `https://legaldock.localhost` | The aliases are re-registered each time the SaaS server starts, so they always follow its current port. Cookies are host-only, so sign in on each brand host separately. Each organization belongs to one brand. The CentraKit host lists and opens every organization, and each product brand host only its own. The local Auth redirect allow list in `supabase/config.toml` accepts every brand host. Domain events such as a sent quote or invoice go through the outbox and the background job queues, which Vercel Cron drains every minute in production. In development the API calls the same `/api/v1/internal/jobs/drain` route every minute itself, so workflow emails reach Mailpit and webhooks go out about a minute after the action. When `CRON_SECRET` is not set, the API generates one for the running process. `pnpm dev:oauth` starts the SaaS app on a `localtest.me` host for testing Google sign-in. See [Authentication](/docs/architecture/authentication). ## Seeded logins [#seeded-logins] Every seeded user has the password `Password123!`, except the PropDock owner `propdock@example.com`, who uses `PropDock123!`, the MarryDuck owner `marryduck@example.com`, who uses `MarryDuck123!`, the SignalDock owner `signaldock@example.com`, who uses `SignalDock123!`, the Spacelio owner `spacelio@example.com`, who uses `Spacelio123!`, and the LegalDock owner `legaldock@example.com`, who uses `LegalDock123!`. `test@example.com` (Lieke Hofstra) is the central test account. It owns the four shop organizations and the Caresly care organization, and it is an admin of the PropDock, MarryDuck, SignalDock, Spacelio and LegalDock organizations, which each have their own owner and staff. Each organization covers its own industry: | Organization | Brand | Business | | --- | --- | --- | | `tractor-service` | MechUp | Tractor and garden machinery repair. The deepest seed, with every domain state. | | `auto-garage` | MechUp | Car garage with APK inspections, tyre storage and courtesy cars. | | `bike-scooter-shop` | CentraKit | Bicycle and scooter shop with retail sales, repairs and a webshop. | | `equestrian-center` | StableDock | Riding school and livery stable with boarders, lessons and farrier visits. | | `elderly-care` | Caresly | Dutch elderly care organization (Zorggroep De Lindehof) with a nursing home location and a district nursing team: client records, daily reports, rosters, care tasks and a family inbox. | | `property-management` | PropDock | Amsterdam property manager (Canalside Property Management) with long-term rooms and apartments and short-stay studios: 26 customers across owners, tenants, applicants and guests, units as assets, maintenance work orders, rent and deposit invoices, turnovers and a WhatsApp inbox. | | `wedding-planning` | MarryDuck | Vienna wedding planner (Alpine Vows Wedding Planning) with full-service, partial and day-of planning and destination weddings: 24 customers across couples in every planning stage, prospects, past and cancelled weddings and a corporate gala, weddings with their venue bookings and room blocks as assets, vendor tasks, deposit and balance invoices and a couples WhatsApp inbox. | | `event-services` | SignalDock | Utrecht event services company (Northpoint Event Services) that supplies event security, crowd control, temporary wifi and access gates and finds work from signals: 24 accounts across the sales funnel, scored permit, tender and business register signals as assets, follow-up tasks, quotes, crew invoices, a signal desk WhatsApp inbox and a Brabant partner on the portal. | | `accounting-firm` | Spacelio | Rotterdam accounting firm (Harbourline Accountants) with annual accounts, tax returns, payroll and deal work: 24 customers across business and private clients, prospects and an archived client, engagement files as assets with document checklists, a data room for a company sale, engagement letters, invoices with the HLI number prefix, client document requests and a client WhatsApp inbox. | | `law-firm` | LegalDock | Hague law firm (Lindenberg Advocaten) in employment, corporate, family and real estate law: 24 clients across prospects with a conflict check, active, on hold and archived matters, matter files as assets, procedural deadlines, engagement letters as quotes, hourly, fixed-fee and subscription invoices with trust money deposits, and a client WhatsApp inbox. | | Email | Access | | --- | --- | | `test@example.com` | Owner of all four shop organizations and `elderly-care`, admin of `property-management`, `wedding-planning`, `event-services`, `accounting-firm` and `law-firm`, the seeded system admin, and a portal contact of De Haan Landscaping. | | `test1@example.com` | Member of `tractor-service`, set up in People as a field service mechanic (Jeroen Bakker). | | `test2@example.com` | Viewer in `bike-scooter-shop` (Sanne Visser). | | `test3@example.com` | Portal-only private customer of both `tractor-service` and `auto-garage` (Pieter Brouwer). | | `test4@example.com` | Portal-only contact for the business customer Green Acre Farms BV in `tractor-service` (Hendrik van Laar). | | `test5@example.com` | Portal-only boarder at `equestrian-center` (Charlotte Veenstra). | | `test6@example.com` | Portal-only family member of client 7001 in `elderly-care` (Ellen Kleinjan, daughter of Johanna de Vries). | | `propdock@example.com` | Owner of `property-management` (Hannah Whitfield), with the password `PropDock123!`. Sign in on `https://propdock.localhost`. | | `prop.tenant@example.com` | Portal-only tenant of Room 2 at Prinsengracht 412 in `property-management` (Emma Clarke). | | `prop.owner@example.com` | Portal-only owner of Prinsengracht 412 in `property-management` (Richard Hale). | | `marryduck@example.com` | Owner of `wedding-planning` (Eleanor Ashdown), with the password `MarryDuck123!`. Sign in on `https://marryduck.localhost`. | | `wed.couple@example.com` | Portal-only bride of customer 9001 in `wedding-planning` (Amelia Sinclair). | | `wed.parent@example.com` | Portal-only mother of the bride of customer 9001 in `wedding-planning` (Diane Whitcombe). | | `signaldock@example.com` | Owner of `event-services` (Harriet Langford), with the password `SignalDock123!`. Sign in on `https://signaldock.localhost`. | | `sig.client@example.com` | Portal-only contact of the festival client 10001 in `event-services` (Freya Lockhart). | | `sig.partner@example.com` | Portal-only contact of the Brabant partner 10002 in `event-services` (Gavin Stroud), who sees the leads offered to the partner. | | `spacelio@example.com` | Owner of `accounting-firm` (Philippa Grantham), with the password `Spacelio123!`. Sign in on `https://spacelio.localhost`. | | `space.client@example.com` | Portal-only director of client 11001 Bakker Holding in `accounting-firm` (Cordelia Ashcombe). | | `space.buyer@example.com` | Portal-only buyer-side adviser of client 11005 Noordzee Capital Partners in `accounting-firm` (Dominic Brewster), with access to the data room. | | `legaldock@example.com` | Owner and managing partner of `law-firm` (Margaret Hollingsworth), with the password `LegalDock123!`. Sign in on `https://legaldock.localhost`. | | `law.client@example.com` | Portal-only general counsel of client 12001 Brightwater Foods BV in `law-firm` (Geoffrey Talbot). | | `law.private@example.com` | Portal-only private client 12002 with a family law matter in `law-firm` (Lavinia Ellison). | The seed also creates role-specific accounts for permission testing: | Email | Access | | --- | --- | | `support@example.com` | The `system-support` system role, with no organization membership. | | `owner@example.com` | Finance and settings role in all four shop organizations and `elderly-care`. | | `operator@example.com` | Admin in `tractor-service` and `auto-garage`. | | `team.lead@example.com` | Service planner in `tractor-service` and `auto-garage`. | | `crm.editor@example.com` | Sales advisor in `tractor-service` and `bike-scooter-shop`. | | `assets.editor@example.com` | Parts manager in `tractor-service` and `bike-scooter-shop`. | | `viewer@example.com` | Read-only custom role in `tractor-service`. | | `bike.mechanic@example.com` | Mechanic in `bike-scooter-shop`. | | `bike.retail@example.com` | Sales advisor in `bike-scooter-shop`. | | `garage.apk@example.com` | Mechanic and APK inspector in `auto-garage`. | | `garage.advisor@example.com` | Service advisor in `auto-garage`. | | `stable.manager@example.com` | Admin in `equestrian-center`. | | `stable.instructor@example.com` | Instructor with the service planner role in `equestrian-center`. | | `stable.viewer@example.com` | Read-only custom role in `equestrian-center`. | | `care.manager@example.com` | Admin (location manager) in `elderly-care` (Marjolein Brugman). | | `care.evv@example.com` | Primary carer (EVV'er) with the `seed-care-evv` custom role in `elderly-care` (Anouk Rietdijk): clients, notes, tasks, inbox approval and agenda. | | `care.nurse@example.com` | District nurse (level 5) with the `seed-care-professional` custom role in `elderly-care` (Samira Bouzid). | | `care.carer@example.com` | Certified carer (level 3, mostly night shifts) with the `seed-care-professional` custom role in `elderly-care` (Daan Lubbers). | | `care.planner@example.com` | Planner with the `seed-care-planner` custom role in `elderly-care` (Renske Veltman). | | `prop.manager@example.com` | Admin in `property-management` who runs finance and owner relations (Daniel Fairbanks). | | `prop.leasing@example.com` | Leasing coordinator with the `seed-prop-leasing` custom role in `property-management` (Priya Raman): waiting list, viewings, screening and leases. | | `prop.host@example.com` | Guest operations lead with the `seed-prop-host` custom role in `property-management` (Lucas Moreau): guest inbox, bookings and turnovers. | | `prop.maintenance@example.com` | Maintenance coordinator with the `seed-prop-maintenance` custom role in `property-management` (Tom Gallagher): work orders, suppliers and materials. | | `prop.cleaner@example.com` | Housekeeping lead with the `seed-prop-cleaner` custom role in `property-management` (Ana Ferreira): assigned turnovers only. | | `wed.manager@example.com` | Admin in `wedding-planning` who runs finance and the business (Oliver Kingsley). | | `wed.planner@example.com` | Lead planner with the `seed-wed-planner` custom role in `wedding-planning` (Grace Whitlock): couples, tasks, the agenda, quotes and the inbox. | | `wed.coordinator@example.com` | Day-of coordinator with the `seed-wed-coordinator` custom role in `wedding-planning` (Rosie Calder): wedding day timelines and assigned weddings. | | `wed.vendors@example.com` | Vendor manager with the `seed-wed-vendors` custom role in `wedding-planning` (Samuel Rowntree): suppliers, purchase orders and vendor contracts. | | `wed.assistant@example.com` | Planning assistant with the `seed-wed-assistant` custom role in `wedding-planning` (Lily Fenwick): RSVPs, stationery and checklists on assigned weddings. | | `sig.manager@example.com` | Admin in `event-services` who runs operations, finance and the business (Callum Pemberton). | | `sig.bizdev@example.com` | Business developer with the `seed-sig-bizdev` custom role in `event-services` (Nadia Hollis): opportunities, meetings, quotes and the inbox. | | `sig.analyst@example.com` | Signal analyst with the `seed-sig-analyst` custom role in `event-services` (Felix Marlowe): reviews and scores incoming signals. | | `sig.ops@example.com` | Event operations lead with the `seed-sig-ops` custom role in `event-services` (Owen Treadwell): crews, site surveys, kit and the event agenda. | | `sig.assistant@example.com` | Office and planning assistant with the `seed-sig-assistant` custom role in `event-services` (Ivy Redfern): account preparation, follow-up tasks and the meeting agenda. | | `space.manager@example.com` | Admin in `accounting-firm` who runs the office and payroll (Martin Elsworth). | | `space.accountant@example.com` | Senior accountant with the `seed-space-accountant` custom role in `accounting-firm` (Naomi Pritchard): clients, engagement files, tasks, documents, engagement letters and invoices. | | `space.tax@example.com` | Tax adviser with the `seed-space-tax` custom role in `accounting-firm` (Laurence Thackeray): tax returns, tasks, documents and the knowledge base. | | `space.deals@example.com` | Deal adviser with the `seed-space-deals` custom role in `accounting-firm` (Julian Hartigan): deal clients and the data room only. | | `space.assistant@example.com` | Junior assistant with the `seed-space-assistant` custom role in `accounting-firm` (Zoe Marchbank): document requests, reminders and checklists. | | `law.manager@example.com` | Admin in `law-firm` who runs the office and finance (Nathan Cresswell). | | `law.lawyer@example.com` | Senior associate in employment law with the `seed-law-lawyer` custom role in `law-firm` (Edward Blackwood): clients, matter files, tasks, the agenda, documents, engagement letters and time. | | `law.paralegal@example.com` | Paralegal with the `seed-law-paralegal` custom role in `law-firm` (Imogen Radcliffe): conflict checks, documents, tasks and deadlines, without finance. | | `law.billing@example.com` | Legal secretary with the `seed-law-billing` custom role in `law-firm` (Felicity Wainwright): invoices, time review and trust money deposits. | | `law.trainee@example.com` | Trainee lawyer with the `seed-law-trainee` custom role in `law-firm` (Rupert Langley): assigned matters, documents and own time. | | `disabled@example.com` | A mechanic in `tractor-service` whose account is disabled, so signing in is refused. | | `external@example.com` | Owner of `harbor-marine-service` and of `cedar-hvac-rental`, which is disabled. No access to the shop organizations. | Each shop organization has customers, contacts, customer assets, products and inventory, tasks, agenda items, quotes, invoices and orders in every status, plus files, inbox threads, chat history, notifications, employees, time, expenses, hiring, workflows, audit logs and billing data. Seed files live in `supabase/seeds/`, and each one starts with a `-- Covers:` comment that lists the scenarios it seeds. ## Common issues [#common-issues] - `pnpm supabase:sync` warns about unset Google OAuth variables. Google sign-in is optional locally, so the warning is safe to ignore. - The Stripe keys in `.env.local` are placeholders, so billing flows do not work locally. Everything else does. - Off Vercel the SaaS server forwards `/api`, `/mcp`, `/.well-known/oauth-protected-resource` and `/docs` to `https://api.localhost` and `https://docs.localhost`. `LOCAL_API_ORIGIN` overrides the API target. - Lefthook installs its hooks from its npm `postinstall` outside CI. Run `pnpm exec lefthook install --force` to refresh them. - If a browser session behaves strangely after a reset, clear the cookies for the `.localhost` hosts and sign in again. - Portless serves a git worktree on its own host, such as `https://my-branch.app.localhost`. The brand aliases have no worktree prefix, so `https://mechup.localhost`, `https://stabledock.localhost`, `https://caresly.localhost`, `https://propdock.localhost`, `https://marryduck.localhost`, `https://signaldock.localhost`, `https://spacelio.localhost` and `https://legaldock.localhost` point at whichever SaaS server started last. The local Auth redirect allow list in `supabase/config.toml` accepts `https://*.app.localhost/**`, so sign-up confirmation, magic link and password reset emails link back to that host. A running stack keeps the list it started with, so run `pnpm supabase:stop` and `pnpm supabase:start` after changing it. For a host that is not on the list, Auth falls back to `site_url`. - Locally the sign-up confirmation email uses the default Supabase link, because the stack engine cannot load templates from `content_path`. The link verifies through Auth and lands on `/auth/confirm` with a PKCE `code`, so open it in the browser where you signed up. Hosted projects keep the `token_hash` template from `[remotes.*]` in `supabase/config.toml`. - Checking out another branch starts a separate, empty Supabase project. Run `pnpm supabase:start` and `pnpm supabase:sync` on the new branch. `pnpm exec supabase stack list` shows every local project, and `pnpm --filter @workspace/supabase-runtime supabase:destroy` deletes the current one and its data. # Overview (/docs/getting-started/overview) The CentraKit apps, every product area with its route and guide, and the local URLs for each app. CentraKit is an AI-native, multi-tenant platform for running a service business. Organizations, customers, work, commerce, conversations and automation share one data model, and the same data is available through the SaaS app, an assistant, a REST API, an MCP server and a CLI. The repository is a pnpm and Turborepo monorepo. [Monorepo](/docs/architecture/monorepo) describes the packages behind the apps. ## Apps [#apps] | App | Purpose | | --- | --- | | `apps/saas` | The signed-in web app: organization workspaces, the customer portal, profile pages and system admin. | | `apps/api` | Every route handler: the REST API under `/api/v1`, the MCP server, OAuth, crons, uploads and AI streams. | | `apps/docs` | This documentation site, built with Fumadocs from the root `docs/` folder. | | `apps/marketing` | The public marketing site template. | | `apps/showcase` | The template showcase served on the apex domain. | | `apps/cli` | The `centrakit` command-line client for the REST API. | ## Product map [#product-map] Organization routes live under `/{orgSlug}`, for example `/tractor-service/quotes`. The table paths below are relative to that prefix unless they start with `/admin`, `/profile` or `/organizations`. ### Workspace [#workspace] | Area | Route | What it covers | | --- | --- | --- | | [Dashboard](/docs/product/dashboard) | `/{orgSlug}` | The organization home page with key figures and recent activity. | | [Command center](/docs/product/command-center) | `Cmd+K` or `Ctrl+K` | Search across records, recent items and quick create commands, plus a hand-off to the assistant. | | [Notifications](/docs/product/notifications) | `/notifications` | Mentions, alerts and pending approval requests for the signed-in user. | | [Approvals](/docs/product/approvals) | `/roles/approval-rules` | Rules that hold an action until an approver signs off, whether a person, an AI agent, a workflow, MCP or the API started it. | | [Chat](/docs/product/chat) | `/chats` | Conversations with the AI assistant, including activity history. | | [Inbox](/docs/product/inbox) | `/inbox` | Shared customer messages from connected channels such as email and WhatsApp. | ### Customers and sales [#customers-and-sales] | Area | Route | What it covers | | --- | --- | --- | | [Customers](/docs/product/customers) | `/customers` | Customers, their contacts and the assets they own. | | [Customer portal](/docs/product/customer-portal) | `/portal` | The customer-facing view of their assets, quotes, invoices and orders. | | [Quotes](/docs/product/quotes) | `/quotes` | The quote pipeline, drafts and quote templates. | | [Invoices](/docs/product/invoices) | `/invoices` | The invoice pipeline, reminders and online invoice payments under `/payments`. | | [Orders](/docs/product/orders) | `/orders` | Sales orders, shipments and promotions. | | [Products](/docs/product/products) | `/products` | The product catalog, inventory, suppliers, purchase orders and labels. | | [Content](/docs/product/content) | `/content` | Shared templates, brands, categories and tags. | ### Work [#work] | Area | Route | What it covers | | --- | --- | --- | | [Tasks](/docs/product/tasks) | `/tasks` | Task board, task list and the tasks assigned to you. | | [Agenda](/docs/product/agenda) | `/agenda` | Agenda items, shared calendars and the planning view under `/agenda/planning`. | | [Files](/docs/product/files) | `/files` | Organization files and drives. | ### People [#people] | Area | Route | What it covers | | --- | --- | --- | | [People](/docs/product/people) | `/people` | Employees, org chart, onboarding, skills, equipment, performance and payroll under `/payroll`. | | [Hiring](/docs/product/hiring) | `/hiring` | Jobs, applications and candidate email templates. | | [Time](/docs/product/time) | `/time` | Timesheets, team time and time off. | | [Expenses](/docs/product/expenses) | `/expenses` | Expense claims, trips, approvals and reimbursements. | ### Insights and automation [#insights-and-automation] | Area | Route | What it covers | | --- | --- | --- | | [Insights](/docs/product/insights) | `/insights` | Analytics and reporting across sales, work and people. | | [Workflows](/docs/product/workflows) | `/workflows` | Workflow graphs, runs and the starter automations for templates. | | [Integrations](/docs/product/integrations) | `/integrations` | Connected apps, messaging channels and webhooks. | ### Organization [#organization] | Area | Route | What it covers | | --- | --- | --- | | [Organizations](/docs/product/organizations) | `/organizations` | Listing, creating and switching between organizations. | | [Settings and branding](/docs/product/settings-and-branding) | `/settings` | Organization profile, branding and AI settings under `/ai/settings`. | | [Access control](/docs/product/access-control) | `/users`, `/teams`, `/roles` | Users and invitations, teams, and roles with their permissions. | | [Billing and usage](/docs/product/billing-and-usage) | `/billing`, `/usage` | The organization subscription, plan invoices and usage limits. | | [Audit log](/docs/product/audit-log) | `/audit-log` | Who changed what in the organization, with export. | | [Profile](/docs/product/profile) | `/profile` | Personal details, sign-in methods and communication preferences. | ### System admin [#system-admin] System admins manage the whole platform under `/admin`: users, organizations, system roles, feature flags, the platform audit log, subscriptions, platform invoices, plans, payouts, platform settings, analytics, integrations, knowledge, workflows and AI settings. Start at the [admin dashboard](/docs/admin/dashboard). ## Local URLs [#local-urls] `pnpm dev:portless` serves the apps on named HTTPS hosts through Portless: | App | URL | | --- | --- | | SaaS | `https://app.localhost`, `https://mechup.localhost`, `https://stabledock.localhost`, `https://caresly.localhost`, `https://propdock.localhost`, `https://marryduck.localhost`, `https://signaldock.localhost`, `https://spacelio.localhost` and `https://legaldock.localhost`, one host per brand | | API | `https://api.localhost`, also reachable as `/api` and `/mcp` on the SaaS hosts | | Docs | `https://docs.localhost/docs`, also reachable as `/docs` on the SaaS hosts | In production every brand has one app host, such as `app.centrakit.com`, and the API, MCP server and docs are paths on it. The docs app starts with `pnpm dev:docs:portless`. Without Portless, `pnpm dev` runs plain `next dev`, and each app prints the localhost port it chose. See [Local development](/docs/getting-started/local-development) for setup. # CentraKit Docs (/docs) Start here for CentraKit product guides, system admin, the REST API, MCP server and CLI, AI agent access, and the architecture and operations notes. CentraKit is an AI-native, multi-tenant platform for running a service business. Customers, quotes, invoices, orders, products, tasks, the agenda, people, conversations and workflows share one data model, and you can work with that data in the web app, through the assistant, or over the REST API, the MCP server and the CLI. ## Get started [#get-started] - [Overview](/docs/getting-started/overview): The apps, every product area and where each one lives. - [Local development](/docs/getting-started/local-development): Run Supabase and the apps locally and sign in with the seeded personas. - [Deployment](/docs/getting-started/deployment): Vercel services, domains, routing and crons. ## Product guides [#product-guides] - [Dashboard](/docs/product/dashboard): The organization home page. - [Chat](/docs/product/chat): Work with the AI assistant. - [Inbox](/docs/product/inbox): Customer messages from connected channels. - [Customers](/docs/product/customers): Customers, contacts and customer assets. - [Quotes](/docs/product/quotes): Draft, send and follow up on quotes. - [Invoices](/docs/product/invoices): Invoice, remind and collect payment. - [Orders](/docs/product/orders): Sales orders, shipments and promotions. - [Products](/docs/product/products): Catalog, inventory and purchasing. - [Tasks](/docs/product/tasks): Plan and track work. - [Agenda](/docs/product/agenda): Appointments, calendars and planning. - [People](/docs/product/people): Employees, onboarding and payroll. - [Workflows](/docs/product/workflows): Automate lifecycles and approvals. The [overview](/docs/getting-started/overview) lists every product area, including the customer portal, time, expenses, hiring, insights, integrations and organization settings. ## System admin [#system-admin] - [Admin dashboard](/docs/admin/dashboard): Platform health and operations at a glance. - [Users](/docs/admin/users): Every user account on the platform. - [Organizations](/docs/admin/organizations): Create and manage tenants. - [Roles and permissions](/docs/admin/roles-and-permissions): System roles and approval rules. ## Developers [#developers] - [REST API](/docs/developers/api): Resources, pagination and conventions for /api/v1. - [MCP server](/docs/developers/mcp): Connect AI clients to CentraKit tools. - [CLI](/docs/developers/cli): The centrakit command-line client. - [Webhooks](/docs/developers/webhooks): Receive events from CentraKit. ## For AI agents [#for-ai-agents] - [AI agents](/docs/developers/ai-agents): How agents read these docs and act on CentraKit data. Agents can read the whole site as plain text. `/llms.txt` lists every page with its description, and `/llms-full.txt` contains the full text of every page in one file. ## Architecture and operations [#architecture-and-operations] - [Monorepo](/docs/architecture/monorepo): Apps, packages and import boundaries. - [Service layers](/docs/architecture/service-layers): Contracts, Supabase implementations and request wiring. - [Tenancy](/docs/architecture/tenancy): Organization slugs in the URL and access guards. - [RBAC](/docs/architecture/rbac): Roles, permissions and enforcement. - [CI and release](/docs/operations/ci-and-release): GitHub Actions, branches and Changesets. - [Workflows runtime](/docs/operations/workflows): System graphs, schedules and inspecting runs. # Brand hosts and rollout (/docs/operations/brand-hosts) How each product brand maps to one app host, how to add a brand, and the checklist for the cutover from the api, docs and sample hosts to path routing. One deployment serves every product brand. Each brand has one `app.<brand>` host, and the REST API, the MCP server and these docs are paths on it: `/api`, `/mcp` and `/docs`. The decision and its trade-offs are in [ADR 0011](/docs/decisions/0011-multi-brand-path-routing). ## Hosts [#hosts] The brand registry in `packages/foundation/src/brands.ts` lists the hosts per environment. The environment comes from `VERCEL_ENV`: `production` on `main`, `development` on `develop`, and `local` everywhere else. | Brand | Production | Development | Local | | --- | --- | --- | --- | | CentraKit | `app.centrakit.com` | `app.centrakit.dev` | `app.localhost` | | MechUp | `app.mechup.com` | `mechup.centrakit.dev` | `mechup.localhost` | | StableDock | `app.stabledock.app` | `stabledock.centrakit.dev` | `stabledock.localhost` | | Caresly | `app.caresly.app` | `caresly.centrakit.dev` | `caresly.localhost` | | PropDock | none yet | `propdock.centrakit.dev` | `propdock.localhost` | | MarryDuck | none yet | `marryduck.centrakit.dev` | `marryduck.localhost` | | SignalDock | none yet | `signaldock.centrakit.dev` | `signaldock.localhost` | | Spacelio | none yet | `spacelio.centrakit.dev` | `spacelio.localhost` | | LegalDock | none yet | `legaldock.centrakit.dev` | `legaldock.localhost` | The apex and `www` domains (`centrakit.com`, `mechup.com`, `stabledock.app` and `caresly.app`) host each brand's public website on Lovable. They point there through DNS and are not Vercel domains. A host that is not in the registry, such as a generated `*.vercel.app` deployment URL, renders the default brand. A brand without a production domain, such as PropDock, lists no production host. `brandAppHost` and `brandUrls` then fall back to the default brand's host, which serves every tenant, so its organizations open on `app.centrakit.com` until the brand gets its own domain. Each organization belongs to one brand (`organizations.brand`). The CentraKit host lists and opens the organizations of every brand, so the platform team manages all tenants from `app.centrakit.com` and `app.centrakit.dev`. A product brand host such as `app.mechup.com` lists and opens only its own organizations. Platform admins move an organization to another brand from its detail page in `/admin/organizations`. ## Add a brand [#add-a-brand] 1. Add the brand to `brandIds` and `brands` in `packages/foundation/src/brands.ts`, with its name, description, website, hosts per environment and assets. Set `shadcnPreset` when the brand has its own theme. 2. Add `https://<host>/**` for its hosts to `additional_redirect_urls` in `supabase/config.toml`, under `[auth]` for local and under `[remotes.main.auth]` and `[remotes.develop.auth]`. 3. Add a Portless alias for the local host to the SaaS `dev:portless:brands` script. 4. Give the brand its look: - Declare its fonts in `apps/saas/lib/brand/fonts.ts`. - Add its mark, favicons, touch icon, PWA icons and sign-in hero to `apps/saas/public/brands/<id>/` (and the mark to `apps/docs/public/brands/<id>/`), then point the registry's `assets` at them. - Add its light and dark token blocks to `packages/ui/src/styles/brands.css` and its hex twins to `brandThemeColors` in `packages/ui/src/lib/brand-chrome.ts`. - Update the brand section of `DESIGN.md`. 5. Follow the provider steps in the checklist below for the new hosts. ## Cutover checklist [#cutover-checklist] Work through these steps for production (`main`) and repeat them with the development hosts for `develop`. ### Vercel [#vercel] - Keep `app.centrakit.com` on the project and add `app.mechup.com`, `app.stabledock.app` and `app.caresly.app` as production domains. - Keep `app.centrakit.dev` on the `develop` branch and add `mechup.centrakit.dev`, `stabledock.centrakit.dev`, `caresly.centrakit.dev`, `propdock.centrakit.dev`, `marryduck.centrakit.dev`, `signaldock.centrakit.dev`, `spacelio.centrakit.dev` and `legaldock.centrakit.dev` to the same branch. - Remove `api.centrakit.com`, `docs.centrakit.com`, `sample.centrakit.com`, `centrakit.com` and `www.centrakit.com`, and their `.dev` counterparts. The cutover has no redirects from the old hosts. - Delete `NEXT_PUBLIC_SITE_URL` from the production and preview environments. Hosts come from the registry there, and the variable only overrides the default brand's origin in the local environment. ### DNS [#dns] - Point `app.mechup.com`, `app.stabledock.app` and `app.caresly.app` at Vercel with the record the Vercel domain settings show. - Point each apex and `www` host at Lovable with the records from the Lovable custom domain settings. - Remove the `api`, `docs` and `sample` records once the old hosts are off the Vercel project. ### Supabase [#supabase] - `supabase/config.toml` lists every brand host in `additional_redirect_urls` for `main` and `develop`. The Supabase GitHub integration applies it when the change reaches each branch. Confirm the list under Authentication, URL Configuration in the dashboard afterwards. - The OAuth server consent page is `site_url` plus `/oauth/consent`, so it stays on the CentraKit host for every brand. - If the send-email Auth hook points at the API host, change it to `/api/v1/email/supabase/auth` on `app.centrakit.com` (and `app.centrakit.dev` for `develop`). ### MCP and OAuth clients [#mcp-and-oauth-clients] - The MCP resource moves from `https://api.centrakit.com/api/mcp` to `https://app.<brand>/mcp`. Tokens are accepted for the `/mcp` URL of every brand host in the environment, Supabase's default `authenticated` audience and `MCP_RESOURCE_AUDIENCE`. - For a transition, set `MCP_RESOURCE_AUDIENCE` to `https://api.centrakit.com/api/mcp` so tokens issued for the old resource keep working after a client switches its URL. Tell MCP users to update their server URL, then remove the value once those tokens have been replaced. - Update the redirect URIs of OAuth clients registered by hand in Supabase that point at the old hosts. Clients that registered dynamically, such as MCP clients, keep their own redirect URIs. - Release the CLI, which defaults to `https://app.centrakit.com/api` and takes `--host` for another brand. ### Google [#google] - The Google OAuth client stays shared, and its authorized redirect URI stays the Supabase callback URL. - On the OAuth consent screen, add `mechup.com`, `stabledock.app` and `caresly.app` to the authorized domains if the screen or the client lists URLs on those domains. The consent screen still shows the CentraKit app name. - Google Calendar runs through Vercel Connect and returns to the host that started the flow. Check that the connector accepts return URLs on every brand host. ### Stripe [#stripe] - Change the webhook endpoint to `https://app.centrakit.com/api/v1/billing/stripe/webhook` in live mode, and to the `app.centrakit.dev` URL for the test mode endpoint `develop` uses. The signing secret does not change unless you create a new endpoint. - Checkout, Customer Portal and Connect onboarding return URLs are built per request from the organization's brand, so they need no change. Update any default return URL or business website in the Customer Portal and Connect settings that still names an old host. - Stripe Checkout, receipts and the Customer Portal still show the shared CentraKit account branding. ### Email [#email] - Change the Resend webhook to `/api/v1/email/resend/webhook` on the app host. - Every brand sends from the one `EMAIL_FROM` address, with the brand name as the sender name. Before a brand sends from its own domain, verify that domain in Resend (SPF, DKIM and DMARC). ### Other webhooks [#other-webhooks] - WhatsApp channel webhooks are registered per connection. Update the callback URL at Meta for each existing connection to the URL the connection page shows now. - Incoming webhook URLs (`/api/v1/public/webhooks/{token}`) and REST clients move from `api.centrakit.com` to the organization's brand host. Tell the owners of existing integrations to update them. ### Monitoring and analytics [#monitoring-and-analytics] - Add every brand host to the Sentry allowed domains of the browser projects, and remove the old hosts. - If a PostHog project tracks these hosts, update its authorized URLs the same way. - Add the brand hosts to the Marker.io widget's allowed domains. ### Verify after deploy [#verify-after-deploy] Run these checks on every brand host, for example `https://app.mechup.com`: - `/login` renders the brand's name and mark, and signing in lands on an organization of that brand. - `/api/v1/health` answers from the API. - `/mcp` answers `401` with a `WWW-Authenticate` header whose `resource_metadata` points at `/.well-known/oauth-protected-resource` on the same host. - `/.well-known/oauth-protected-resource` returns `"resource": "https://<host>/mcp"`. - `/docs`, `/docs/developers/mcp.md`, `/llms.txt` and `/docs/mcp` serve the docs, and `/api-docs` redirects to `/docs/developers/api`. - `/robots.txt` and `/docs/sitemap.xml` load, and the sitemap lists pages on the canonical docs origin. Submit the sitemaps in Google Search Console for each brand host. - The old `api.*`, `docs.*` and `sample.*` hosts no longer resolve to the deployment. - Set the brand on existing production organizations that belong to MechUp, StableDock or Caresly from `/admin/organizations`. # CI And Release (/docs/operations/ci-and-release) GitHub Actions workflows, branch protection, verification jobs, and CLI release flow. CentraKit uses a small set of focused GitHub Actions workflows. ## Workflows [#workflows] - `.github/workflows/ci.yml` runs on pull requests, pushes to `develop` and `main`, and manual dispatch. A `PR title` job runs commitlint on the pull request title. It calls `verify.yml` (affected workspaces only on pull requests), then builds and checks payload budgets. - `.github/workflows/verify.yml` is the reusable gate with four jobs: - `Static checks` installs once and runs lint, format, Knip, Turbo boundaries, `pnpm audit:high`, OpenAPI drift and the i18n catalog test as one `parallel` step group, so each check keeps its own log. - `Typecheck` and `Test` each get their own runner, because they are CPU-bound on the 2 vCPU standard runner. `Test` uploads the Turbo run summary, so per-suite timings show when a suite needs sharding (above about 5 minutes). - `Integration (1/3)` to `(3/3)` shard the cross-workspace `@workspace/integration-tests` suite with Vitest `--shard`, because it took about 7 minutes on one runner. `Test` runs every other workspace. - `Live database suites` starts the local stack on the native runtime, restores the service archives that `database.yml` caches, loads the Stripe demo fixture, and runs `tests/integration/supabase/` with `REQUIRE_LOCAL_SUPABASE=true` and `RUN_LIVE_RLS_TESTS=true`. The shards have no database, so the live RLS and repository suites skip there. In this job a missing stack throws from `readLocalStatus` instead of skipping. It runs on every CI run, not only on affected workspaces, because a repository or policy change anywhere can break these suites. - The `Build` job in `ci.yml` builds Next.js apps one at a time (`--concurrency=1`), because concurrent Turbopack builds exhaust the 7 GB runner. The Turbopack build cache is off on GitHub Actions (ADR 0016), because a cold SaaS build with it runs the runner out of memory. A background step logs memory and disk usage every 15 seconds during the build. - `.github/workflows/database.yml` runs when `supabase/**` or the Supabase CLI pin changes. It starts the local stack on the Supabase CLI native runtime, without Docker, and caches the downloaded service archives in `~/.supabase/cache/stack` per CLI version (saved only on `develop`). It then runs the pgTAP suite with `pnpm supabase:test`, checks the declarative schemas against the migrations, and runs the read-only checks (SQL modules, generated types, `supabase db lint`) in parallel. - `.github/workflows/security.yml` runs zizmor and actionlint on workflow changes, and a non-blocking `pnpm audit signatures` when the lockfile changes. Run both linters locally from their release binaries before pushing a workflow change; `.github/actionlint.yaml` ignores the syntax actionlint 1.7.12 cannot parse yet (`$/`, `parallel`, `cache-mode`, `concurrency.queue`). - `.github/workflows/release.yml` runs as the last job of CI on pushes to `main`, after `Verify` and `Build` pass, so it does not check, build or test the same commit again. It uses Changesets to open or update the `chore: version packages` PR. Merging that PR tags the release, creates the GitHub Releases, and merges `main` back into `develop`. Release runs queue (`queue: max`) instead of cancelling each other, and release jobs use `cache-mode: none`, so a cache entry never reaches a release. - `.github/workflows/release-cli.yml` runs for published CLI releases and manual retries to package the tarball and attach it to the GitHub release. - `.github/workflows/react-doctor.yml` runs [React Doctor](https://www.react.doctor/ci) on pull requests and on pushes to `develop` and `main`. On a pull request it scans the changed files, posts a summary comment with the new findings, and fails on new warnings or errors (`blocking: warning`). Run the same scan locally with `pnpm doctor:react`; `pnpm doctor` also runs the PermDock and better-supabase doctors. - `.github/dependabot.yml` opens one grouped weekly PR each for npm and GitHub Actions, after a two-day cooldown. - `.github/actions/setup` is the shared composite action: Turborepo Remote Cache over OIDC, then `pnpm/setup` for pnpm, the Node 24 runtime, the store cache, and a frozen install. Only the `Build` job saves the pnpm store, and only on `develop`; every other job passes `save-cache: "false"` and restores that entry. - Same-repo actions and reusable workflows use the `$/` self-repository syntax. GitHub-owned actions use major tags, and third-party actions are pinned to a commit SHA with a `# vX.Y.Z` comment (`.github/zizmor.yml` enforces this). Every checkout sets `persist-credentials: false`, except the release job, which pushes. ## Required Repository Configuration [#required-repository-configuration] ### Variables [#variables] - `TURBO_TEAM`: Vercel team slug for Turborepo remote cache. Use `scaledock`. - `NEXT_PUBLIC_SITE_URL`: optional CI override for Next.js env validation. Defaults to `https://localhost`. - `NEXT_PUBLIC_SUPABASE_URL`: optional CI placeholder override for Next.js env validation. - `NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY`: optional CI placeholder override for Next.js env validation. ### Secrets [#secrets] - `SENTRY_AUTH_TOKEN`: optional. CI does not upload source maps or create Sentry releases; Vercel production builds do. ### Remote cache (OIDC) [#remote-cache-oidc] CI uses [`vercel/setup-turborepo-remote-cache-action`](https://github.com/vercel/setup-turborepo-remote-cache-action) to exchange a GitHub OIDC token for a short-lived Turborepo token. That token is team-scoped and can write cache artifacts. Workflows need `id-token: write`. Create the matching Vercel policy once. In the team **Build and Deployment** settings, open [OIDC Policies for CLI Access](https://vercel.com/d?to=%2F%5Bteam%5D%2F%7E%2Fsettings%2Fbuild-and-deployment%3FaddOidcPolicy%3Dturborepo-cli&title=Add+a+Turborepo+CLI+OIDC+Policy) and add a Turborepo CLI policy for `ScaleDockHQ/CentraKit`. Workflows do not set `TURBO_TOKEN` themselves, so the token from the OIDC exchange is the only one Turborepo sees. Without the policy the setup step logs `Failed to exchange OIDC token` and every task runs without remote cache. ```bash gh variable set TURBO_TEAM --body "scaledock" --repo ScaleDockHQ/CentraKit gh variable set NEXT_PUBLIC_SITE_URL --body "https://localhost" --repo ScaleDockHQ/CentraKit ``` Vercel deployments do not need `TURBO_TOKEN`: when builds run `turbo run build` (configured per service in root `vercel.json`), [Vercel Remote Cache](https://vercel.com/docs/monorepos/remote-caching) is authenticated automatically. Root `turbo.json` sets `remoteCache.teamSlug` / `teamId` for the linked `centrakit` project. ### Local remote cache [#local-remote-cache] From the repo root (one-time per machine): ```bash pnpm exec turbo login pnpm exec turbo link ``` This writes gitignored `.turbo/config.json`. After linking, local `turbo run build` shares artifacts with Vercel and GitHub Actions. ## Branch Model [#branch-model] - `develop` is the default branch. Commit and push to it directly; feature branches are the exception and get no Vercel preview. - `main` moves only through the pull request from `develop` to `main`, merged whenever a release is wanted. Never commit to `main`. - Vercel deploys `develop` as preview (`app.centrakit.dev`, plus the brand previews `mechup.centrakit.dev`, `stabledock.centrakit.dev`, `caresly.centrakit.dev`, `propdock.centrakit.dev`, `marryduck.centrakit.dev`, `signaldock.centrakit.dev`, `spacelio.centrakit.dev` and `legaldock.centrakit.dev`) and `main` as production (`app.centrakit.com`, `app.mechup.com`, `app.stabledock.app` and `app.caresly.app`). The API and docs are the `/api`, `/mcp` and `/docs` paths on those hosts. `git.deploymentEnabled` in root `vercel.json` denies every other branch. - Supabase: `main` maps to the production project and `develop` to the persistent Supabase branch. The Supabase GitHub integration applies migrations and `config.toml` on push; do not `supabase db push` from Actions. - Dependabot targets `develop`. The Changesets version PR targets `main` from `changeset-release/main`; `vercel.json` skips Vercel deployments for `changeset-release/*` branches. The repository plan is private without rulesets, so there is no enforced branch protection. The checks below are the ones to require on the release PR once protection is available: - `PR title` - `Verify / Static checks` - `Verify / Typecheck` - `Verify / Test` - `Verify / Integration (1/3)`, `(2/3)` and `(3/3)` - `Build` - `React Doctor` The `Build` job also captures SaaS and CLI payload artifacts and compares deterministic SaaS build metrics with the latest successful artifact on the base branch. Regressions emit warnings by default. Set the `PAYLOAD_COMPARE_MODE` repository variable to `enforce`, or choose `enforce` for a manual workflow run, to make budget regressions fail the job. ## Payload Baselines [#payload-baselines] The `Build` job records shipped-cost artifacts after `turbo run build`, so the payload steps reuse that build instead of compiling the SaaS app again: - SaaS: `pnpm --filter saas baseline:payload` reads the `next build` output, writes `.next/diagnostics/payload-artifact.json`, and uploads static CSS/chunks, manifests, and `.nft.json` traces. - Bundle analyzer: `pnpm --filter saas analyze:payload` runs `next experimental-analyze --output`, a second full compile of about eight minutes on CI. It runs only on a manual workflow run, and its output lands in the same `saas-payload-baseline` artifact. - CLI: `pnpm --filter centrakit baseline:payload` packs a baseline tarball and prints `dist/cli.js` / tarball byte sizes. Locally: ```bash pnpm exec turbo run baseline:payload --filter=saas --filter=./apps/cli ``` The versioned SaaS artifact records, per audited route, uncompressed production JS, route-only JS after removing chunks shared by every audited route, obtainable prerendered Flight/RSC bytes, server entry bytes, and the unique files/bytes referenced by its NFT trace. Audited routes are Dashboard, Planning, Tasks/all, Products, Admin subscriptions, Audit log, Agenda detail, and Product detail. CI selects only audited routes affected by changed source paths. A shared App Shell change selects all audited routes. Shared production JS is compared once with a zero-growth budget, so the same shared regression is not repeated for every route. Route budgets allow the larger of the absolute or percentage allowance: 25 KiB/5% route JS, 10 KiB/5% Flight and server entry, and 100 KiB/2% server trace. Missing or schema-incompatible base artifacts warn and skip comparison, including in enforce mode. Runtime timings are intentionally excluded because CI compares build artifacts only. ## Verification Strategy [#verification-strategy] - Pull requests and pushes to `develop` and `main` run the same check set. Payload baselines compare against the latest successful run on the PR base branch (or the pushed branch). - Independent jobs run in parallel to reduce wall-clock time, and cheap checks share one install through `parallel` steps. - The OpenAPI step runs `pnpm openapi:generate` and fails if committed API artifacts drift. ## Versioning [#versioning] Every user-visible change gets a changeset (`pnpm changeset`). The apps share one fixed version (`.changeset/config.json` `fixed` group with `privatePackages.version` and `tag`), so the product has a single release number. `pnpm version-packages` runs `scripts/root-changelog.ts`, which reads the pending changesets, runs `changeset version` (per-app `CHANGELOG.md` files), and prepends a dated section with every changeset summary to the root `CHANGELOG.md`. It then formats the changelogs. The docs site renders the root changelog. ## CLI Release Flow [#cli-release-flow] Release automation is split into two workflows: 1. `release.yml` opens or updates the `chore: version packages` PR from the pending changesets. 2. Merging that PR tags `centrakit@<version>` and publishes the GitHub release, and `release-cli.yml` packages the tarball and uploads it to the release. `release-cli.yml` also keeps `workflow_dispatch` for retrying an asset upload against an existing release tag. # Database Reset (/docs/operations/database-reset) Reset the hosted develop and production databases after a migration squash. The move to Declarative Schemas 2.0 replaced every migration with one squashed set: the extensions, one `init`, the event triggers, the module data, the data migrations and the PermDock seeds. The schema also changed in ways that cannot be migrated in place: `uuid` keys for customers, assets and teams, renamed columns, enum status columns and the `document_parties` table. The hosted databases still record the old migration versions. When the squashed set reaches them, the Supabase GitHub integration runs the new `init` against the old schema and fails with `relation already exists`. Repairing the migration history is not an option either, because the old schema does not match the new `init`. Both hosted databases are therefore reset once. CI does not catch this, because it always starts from an empty database. The product owner approved a reset of production that drops its data. Take the dumps in step 2 regardless. ## Before you start [#before-you-start] - Work from a checkout of the branch that carries the squash, with the local stack able to run (`pnpm supabase:start`). - Have the production database password from the dashboard (**Project Settings**, then **Database**). `supabase branches get` never returns a connection string for `main`. - Never use `--linked`. The local link points at production, and a reset with it would run seeds there. - Plan a maintenance window for production. Users are signed out, and every organization, customer and document is removed. ## Steps [#steps] 1. **Freeze deploys.** Pause merges to `develop` and `main` until step 9 is done. 2. **Dump both databases.** The develop URL comes from `supabase branches get`; the production URL is the session-mode connection string with the password filled in. ```sh DEVELOP_DB_URL="$(pnpm exec supabase branches get develop --project-ref gunugimgrrnxqynjghqe -o env | grep '^POSTGRES_URL=' | cut -d= -f2- | tr -d '"')" PRODUCTION_DB_URL="postgresql://postgres.gunugimgrrnxqynjghqe:<password>@<session-pooler-host>:5432/postgres" pnpm exec supabase db dump --db-url "$DEVELOP_DB_URL" -f develop-schema.sql pnpm exec supabase db dump --db-url "$DEVELOP_DB_URL" --data-only --use-copy -f develop-data.sql pnpm exec supabase db dump --db-url "$PRODUCTION_DB_URL" -f production-schema.sql pnpm exec supabase db dump --db-url "$PRODUCTION_DB_URL" --data-only --use-copy -f production-data.sql pnpm exec supabase db dump --db-url "$PRODUCTION_DB_URL" --data-only --use-copy -s auth,storage -f production-auth-storage.sql ``` Keep the dumps outside the repository. They hold personal data and secrets. 3. **Raise the lock limit on production.** A remote reset drops every object in one transaction and fails with `out of shared memory` at the default lock limit. Develop already runs with `max_locks_per_transaction=1024`. This restarts the production database: ```sh pnpm exec supabase postgres-config update --experimental --project-ref gunugimgrrnxqynjghqe --config max_locks_per_transaction=1024 ``` 4. **Reset develop from the squash branch, before merging it.** A remote reset keeps the job queue tables in the `pgmq` schema but leaves them without their `msg_id` column, and `better_supabase.ensure_job_queue` then skips them, so the seeds fail with `column "msg_id" does not exist`. Drop the queues first with this `drop-job-queues.sql`, then reset. The reset applies the squashed migrations and the demo seeds: ```sql do $$ declare r record; begin for r in select queue_name from pgmq.meta loop perform pgmq.drop_queue(r.queue_name); end loop; for r in select c.relname from pg_class as c join pg_namespace as n on n.oid = c.relnamespace where n.nspname = 'pgmq' and c.relkind in ('r', 'p') and (c.relname like 'q\_%' or c.relname like 'a\_%') loop execute format('drop table pgmq.%I', r.relname); end loop; end $$; ``` ```sh psql "$DEVELOP_DB_URL" -f drop-job-queues.sql pnpm exec supabase db reset --db-url "$DEVELOP_DB_URL" pnpm stripe:sync:migrate --db-url "$DEVELOP_DB_URL" ``` The migration history now lists the squashed versions, so the integration has nothing left to apply when the pull request merges. 5. **Merge the pull request into `develop`.** Confirm that the Supabase Preview check passes and that it reports no pending migrations. Sign in on `app.centrakit.dev` with a seeded login and open a quote, an invoice and a calendar. 6. **Reset production without seeds.** Production keeps seeding disabled, and `--db-url` reads the base `[db.seed]` setting, so pass `--no-seed`. Drop the job queues first, as in step 4: ```sh psql "$PRODUCTION_DB_URL" -f drop-job-queues.sql pnpm exec supabase db reset --db-url "$PRODUCTION_DB_URL" --no-seed pnpm stripe:sync:migrate --db-url "$PRODUCTION_DB_URL" pnpm stripe:sync:backfill ``` Run the backfill as described in [Stripe Billing](/docs/operations/stripe-billing). 7. **Check production before release.** In the SQL editor: ```sql select count(*) from supabase_migrations.schema_migrations; select count(*) from public.roles where system; select count(*) from public.billing_plans; select count(*) from storage.buckets; select count(*) from auth.users; ``` The first four must match a fresh local reset (`pnpm supabase:reset`). If `auth.users` still has rows, their profiles and memberships are gone: delete those users in the dashboard, or recreate the owners and invite the rest. Remove Storage objects whose rows no longer exist with the bucket browser or the Storage API. 8. **Release to `main`.** Merge the `develop` to `main` pull request. The integration applies `config.toml` and finds no pending migrations. 9. **Create the platform admin again.** Sign up on `app.centrakit.com`, then grant the system role in the SQL editor and sign in again so the token carries it: ```sql insert into public.user_roles (user_id, role_id) select users.id, roles.id from auth.users as users cross join public.roles as roles where users.email = '<admin email>' and roles.scope = 'system' and roles.key = 'platform-admin'; ``` ## If a step fails [#if-a-step-fails] - A failed reset rolls back its transaction, so the database keeps its old state. Fix the cause, such as the lock limit, and run the reset again. - If the integration fails after a merge, the database still has the old migration history. Run the reset for that database from the merged commit, then rerun the check. - To go back to the old schema, restore the dumps from step 2 into a fresh project. The squashed migrations cannot be applied on top of them. # Environment Variables (/docs/operations/environment-variables) Root env files, Vercel env management, Supabase values, Stripe values, and Turbo task env forwarding. Apps load environment variables from the **repository root**. Deployed services share one Vercel project environment. Next.js apps call `loadMonorepoEnv` (via `@workspace/next-config`) so they read the root files even though each app lives under `apps/<name>`. ## Local Files [#local-files] | File | Purpose | | --- | --- | | `.env.example` | Tracked template (no secrets) | | `.env.local` | From `pnpm env:pull:development` (gitignored) | | `.env.development.local` | Local overrides that survive pulls (Portless URLs, local Supabase, `DEV_LOGIN_*`) | | `.env.production.local` | From `pnpm env:pull:production` for local production builds | | `supabase/.env.example` → `supabase/.env.local` | Supabase CLI Google OAuth (`GOOGLE_OAUTH_CLIENT_ID` / `_SECRET`) and CLI-only settings | Copy `.env.example` to `.env.local` when you are not linked to Vercel yet, then fill required values. Prefer `pnpm env:pull` (development + production) when the root project is linked. For local Supabase, `pnpm supabase:start` writes `NEXT_PUBLIC_SUPABASE_URL`, `NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY`, `SUPABASE_SECRET_KEY`, `POSTGRES_URL`, `SMTP_HOST` and `SMTP_PORT` into `.env.development.local` from `supabase status`. Ports differ per git worktree and branch, so do not copy them by hand. Other common values are: ```bash # Optional. Leave it unset or set it to https://localhost so the default brand # resolves to https://app.localhost. # NEXT_PUBLIC_SITE_URL=https://localhost ``` ## Vercel [#vercel] Link the repository root to the CentraKit Vercel project and manage deployed values there (including marketplace integrations for Sentry, Supabase, Stripe, and Resend). ```bash pnpm env:pull # development → .env.local + production → .env.production.local pnpm env:pull:development pnpm env:pull:production pnpm env:pull:preview # → .env.preview.local (optional; Next does not auto-load it) ``` Prefer shared integration names (`NEXT_PUBLIC_SENTRY_DSN` / `SENTRY_*`, Supabase, Stripe, Resend). AI Gateway and Vercel Connect use `VERCEL_OIDC_TOKEN` from `pnpm env:pull`. Sentry SDK initialization is disabled in local development. Keep local-only overrides in `.env.development.local` so development pulls into `.env.local` do not wipe Portless or local Supabase settings. ## Turbo Forwarding [#turbo-forwarding] Turborepo only forwards variables listed in a task's `env` array or global env configuration. If a build needs a secret or public value, add it to that app's `turbo.json` task configuration, root `globalEnv`, or `globalPassThroughEnv`. `NEXT_PUBLIC_SITE_URL` is in root `globalEnv` so CI and local `next build` hash it. `SENTRY_AUTH_TOKEN` is in build `passThroughEnv` so the Sentry plugin can see it without affecting the cache hash. ## Public URLs and brands [#public-urls-and-brands] Public URLs come from the brand registry in `packages/foundation/src/brands.ts`, not from env. Each brand has one app host per environment, and the API, MCP server and docs are the `/api`, `/mcp` and `/docs` paths on it. `brandUrls(brand)` returns those URLs. | Variable | Effect | | --- | --- | | `VERCEL_ENV` | Picks the host set: `production` uses the production hosts, `preview` and `development` use the `*.centrakit.dev` hosts, and anything else, including unset, uses the local `*.localhost` hosts. Client bundles read `NEXT_PUBLIC_VERCEL_ENV`, which Vercel sets to the same value. | | `NEXT_PUBLIC_SITE_URL` | Only overrides the default brand's origin in the local environment, for example `http://localhost:3100` in E2E builds. `https://localhost` or an unset value keeps `https://app.localhost`. Deployed environments ignore it. | | `LOCAL_API_ORIGIN` | Off Vercel only. The API server that the SaaS `/api`, `/mcp` and `/.well-known/oauth-protected-resource` rewrites target, for example `http://127.0.0.1:3101` in E2E builds. Unset uses `https://api.localhost`. | | `MCP_RESOURCE_AUDIENCE` | An extra accepted token audience for the MCP server, next to the `/mcp` URL of every brand host and Supabase's default `authenticated` audience. Set it to `https://api.centrakit.com/api/mcp` while MCP clients move off the old host. | See [Brand hosts and rollout](/docs/operations/brand-hosts) for the host list. ## Sensitive Values [#sensitive-values] Do not commit `.env.local` files or credentials. Document variable names and ownership, not real secret values. # Stripe Billing (/docs/operations/stripe-billing) Stripe Billing on the Stripe Sync Engine, customer-invoice payment surfaces, environment variables, Connect setup, webhooks, and go-live checks. CentraKit uses Stripe for two distinct money flows: 1. **SaaS subscription billing:** organization plans, Customer Portal, metered overage. Stripe is the source of truth for subscriptions, invoices, payment methods, tax ids and the billing contact. The [Stripe Sync Engine](https://github.com/supabase/stripe-sync-engine) mirrors them into the `stripe` schema of the database, and the better-supabase `billing` block links each organization to its Stripe customer (`better_supabase.billing_customers`). CentraKit keeps only its plan catalog; usage is counted by the better-supabase `usage` block. 2. **Customer invoice payments:** tenants collect payment on CentraKit invoices via Stripe Connect destination charges. CentraKit invoices remain the accounting source of truth (numbering, PDF, status history). Stripe PaymentIntents collect funds; CentraKit is the **merchant of record**. Never collect or store raw card numbers, CVC, bank account details, legacy Tokens, Sources, Charges, or Card Element data. Prefer the Stripe Payment Element. Never persist PaymentIntent client secrets. ## SaaS Subscription Surfaces [#saas-subscription-surfaces] - Checkout Sessions in `subscription` mode start new subscriptions and upgrades. The billing block creates the Stripe customer on the first checkout, with the organization id in `metadata.organization_id`, and links it. - Stripe Customer Portal handles subscription management, payment methods, invoice history, billing details, and tax IDs where Stripe supports the flow. - Billing details edited in CentraKit (company name, email, address and tax ids) are written to the Stripe customer, which is their record; invoice recipients stay in `organization_invoice_recipients`. - The billing pages read Stripe's objects from the Sync Engine tables: `public.organization_subscription(organization_id)` matches the subscription item's price to the plan catalog (`billing_plan_prices.stripe_price_id`, or the price metadata `plan_key`, `credit_package_key` and `included_credits`), and `billing.organization_plans` gives the plan every organization is on (Starter without an active, trialing or past due subscription). Checkout and Portal return pages are UX hints only. - `organization_subscription_history` stays CentraKit's analytics record: after each `customer.subscription.*` event and each refresh, `capture_organization_subscription_history` appends a row when the plan, status, quantity or schedule changed. - Subscription invoice PDF access is mediated by CentraKit before redirecting to Stripe `invoice_pdf` or Hosted Invoice Page URLs. - Metered usage is reported to Stripe every hour by the `usage_stripe_report` job, as meter events named by `billing_usage_meters.stripe_event_name`. Only the overage above each plan's included quantity in the billing period is sent; a meter that is unlimited on the plan sends nothing, and organizations without a Stripe customer are skipped until they have one. - Platform fees on online invoice payments are invoiced by ScaleDock itself, because Stripe does not invoice application fees. The `platform_fee_invoicing` job runs daily from the 2nd to the 14th of each month and calls `issue_platform_fee_documents` for every organization with fees not yet invoiced up to the end of the previous month in Europe/Amsterdam, with fee refunds not yet credited, or with documents that were issued but not emailed yet. Each organization gets one invoice per VAT treatment (`domestic`, `reverse_charge`, `outside_scope`) with one line per customer invoice payment, taken from the fee snapshot on `invoice_payments`, and one credit note for the refunded part of fees invoiced earlier. Invoices and credit notes share the number sequence `PF-<year>-<six digits>` and are marked as settled on the issue date, because the fee was already withheld through Stripe. The job needs the `platform_fee_issuer` platform setting to be complete, reads it with the service role because only platform staff can read it (it holds the IBAN), takes the issuer country from `platform_fee_vat.issuerCountry`, and refreshes the organization's VIES check when it is older than 30 days. When VIES or the member state service is unavailable, a previous valid check of the same number is kept. The documents are emailed with the PDF to the organization's invoice recipients and reply-to address, and `emailed_at` is set after sending; a document whose email failed is sent again on the next run. An organization without invoice recipients and without a reply-to address is skipped instead of failing the run: its documents stay unsent, the run result counts it under `unmailed`, and the job logs a warning. The documents are kept when an organization is purged, with the buyer snapshot. - Plan features are entitlements: the better-supabase `entitlements` module (plans source) reads `billing.organization_plans` and the included rows of `billing_plan_features`. A feature row can carry a `value`, which `better_supabase.tenant_entitlement_value` returns. Audit-log retention is the `value` of the `audit-logs` feature in days (Starter 1, Pro 7, Business 90, Enterprise 365, or the `audit_log_retention_days` metadata on the Enterprise Stripe subscription), and file history is the `value` of `file-history`. ## Customer Invoice Payment Surfaces [#customer-invoice-payment-surfaces] - Each organization has one Stripe Accounts v2 account (ADR 0030). **Activate online payments** under `/{orgSlug}/payments/settings` (`payment.manage`) creates it with the `merchant` configuration, or adds that configuration to an existing account, with `dashboard: none` and Stripe as fees and losses collector. CentraKit prefills the name, address, phone, KvK and VAT numbers, the brand MCC and product description, the statement descriptor prefix and the business URL (the organization website, or its public business page outside local development). Activation refuses to start while the business page details are incomplete. - The rest of the verification runs in the embedded `account_onboarding` component, followed by `notification_banner` and `account_management`. The Payments tab lists online payments from `invoice_payments` itself and opens `payment_details` for one payment, with refund and dispute management; the Payouts tab embeds `payouts`. Account sessions are created server-side per request (`createPaymentAccountSessionAction`). - `organization_payment_accounts` mirrors the `card_payments` and `stripe_balance.payouts` capability statuses, the most urgent requirements deadline and the derived status. Online invoice send is blocked until the account is `enabled` with `card_payments` active. For an EU organization outside the Netherlands, activation also stores the VIES result for its VAT number, and keeps a previous valid result when VIES is unavailable. - Invoice payments are direct charges on the organization's account. CentraKit creates the PaymentIntent only when the customer clicks Pay, never on send or while the page renders. It carries `application_fee_amount` from `platform_settings.invoice_payment_fee` (default fixed 1 EUR excluding VAT) with VAT added by the `platform_fee_vat` setting (which also holds the issuer country that decides what counts as domestic), and a statement descriptor suffix built from the invoice number. The fee net, VAT and VAT treatment are snapshotted on `invoice_payments`. - Customers pay on `/{orgSlug}/portal/invoices/[invoiceId]` via the embedded Payment Element, initialized with the connected account (`stripeAccount`) and a deferred intent; the intent is created by a rate-limited server action when the customer submits. Dynamic payment methods (card, iDEAL, etc.) follow Stripe Dashboard + currency/country configuration. - Online payment is offered for invoices in euros only, because the accounts default to EUR and the platform fee documents are in EUR. Other currencies show that online payment is unavailable. - Every payment attempt gets its own PaymentIntent idempotency key, so a new attempt after a cancellation never replays the canceled intent. A canceled intent is closed and replaced. While a payment is processing, such as SEPA Direct Debit, the portal shows the processing state and refuses a second payment. - Mapping lives in `invoice_payments`. Webhooks update payment + invoice status atomically and never move a succeeded payment back to a pending status; browser returns only refresh UI. - Plan billing uses the same account with the `customer` configuration. Creating it needs the organization address with its country; checkout asks for the address first. - Manual mark-paid and invoice cancel cancel collectible PaymentIntents. Refunds and disputes are owned by CentraKit as merchant of record and mirrored into `invoice_payments` status. ## Required Environment [#required-environment] Set these per environment in Vercel (Stripe integration covers the keys): ```bash STRIPE_SECRET_KEY=sk_test_... STRIPE_WEBHOOK_SECRET=whsec_... STRIPE_CONNECT_WEBHOOK_SECRET=whsec_... STRIPE_ACCOUNTS_WEBHOOK_SECRET=whsec_... NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_... POSTGRES_URL=postgres://... ``` `STRIPE_CONNECT_WEBHOOK_SECRET` signs the event destination for events on connected accounts, and `STRIPE_ACCOUNTS_WEBHOOK_SECRET` signs the thin event destination for Accounts v2 account events (ADR 0030). `POSTGRES_URL` (the Supabase integration sets it, as does `POSTGRES_URL_NON_POOLING`) is the direct database connection the billing block and the Stripe Sync Engine use, in both the SaaS app and the API app. Plan price IDs are not environment variables. Keep them in Stripe and the platform database. Use separate test and live products, prices, Portal configuration, Connect settings, and webhook signing secrets. Enable Connect in the Stripe Dashboard and complete platform profile / approval prerequisites before going live with destination charges. ## Stripe Sync Engine [#stripe-sync-engine] CentraKit runs the Sync Engine as the `@supabase/stripe-sync-engine` library inside the API app rather than as a separate service. The Stripe webhook route verifies each event and stores it in the webhook inbox. The `webhook_inbox` background job then hands every platform event to the billing consumer `processStripeBillingEvent` (`packages/core-platform/src/billing-webhooks.ts`), which lets the Sync Engine store the object (`StripeSync.processEvent`) and then lets the billing block link the customer and emit its `billing.*` events. Events of connected accounts never reach the engine: they arrive on their own endpoints, described under Customer invoice payments. Events the engine does not store skip it. The engine owns the `stripe` schema. CentraKit's declarative schema and `pnpm supabase:diff` never touch it; `pnpm stripe:sync:migrate` installs and upgrades it with the engine's own migrations (tracked in `stripe.migrations`). The script first creates `stripe.invoice_status`, because engine 0.48.5 checks for an existing `invoice_status` type in every schema and would otherwise skip it next to CentraKit's `public.invoice_status`. Stripe timestamps are `integer` columns in the engine's schema, so dates after January 2038 cannot be stored. ### Hosted setup [#hosted-setup] Do these once per Supabase project (production `main` and the persistent `develop` branch), and again after a Sync Engine upgrade: 1. Install the schema with the project's session-mode connection string: `pnpm stripe:sync:migrate --db-url "<POSTGRES_URL_NON_POOLING>"`. It is idempotent. Do not install the dashboard's Stripe Sync Engine integration on top of it, because both would manage the same schema. 2. Backfill existing Stripe data into the tables: `STRIPE_SECRET_KEY=<key> pnpm stripe:sync:backfill --db-url "<POSTGRES_URL_NON_POOLING>"` (`--object subscription` or `--since 2026-01-01` narrow it). 3. Confirm `POSTGRES_URL` is set for the SaaS and API projects in all three Vercel environments. 4. Point the Stripe webhook endpoint at `/api/v1/billing/stripe/webhook` with the events listed below, and set `STRIPE_WEBHOOK_SECRET` to its signing secret. 5. Publish the plan prices from `/admin/plans`, so `billing_plan_prices.stripe_price_id` maps each Stripe price to its plan; prices created in the Stripe Dashboard need `plan_key`, `credit_package_key` and `included_credits` metadata instead. 6. For an Enterprise audit-log retention override, set `audit_log_retention_days` in the Stripe subscription's metadata. When the tables drift (a missed webhook), **Refresh from Stripe** in the admin billing detail fetches the organization's subscriptions, invoices, payment methods and tax ids again (`syncFromStripe`). ## Local Setup [#local-setup] `pnpm supabase:reset` and `pnpm supabase:sync` run `pnpm stripe:sync:migrate --local --demo`, which installs the Sync Engine schema in the local stack and loads the demo rows from `packages/stripe/fixtures/sync-engine-demo.sql` for the seeded organizations. Stripe keys are placeholders locally, so the billing pages read the demo rows and every call to Stripe (checkout, portal, plan changes) fails with a provider error. With real test keys: 1. Create Stripe test products and recurring prices for Starter, Pro, and Business monthly/yearly plans, or publish them from `/admin/plans`. 2. Create Stripe meters matching the seeded usage event names such as `centrakit_ai_input_tokens`, `centrakit_ai_output_tokens`, `centrakit_inbox_messages_sent`, `centrakit_api_requests`, and `centrakit_mcp_tool_calls`. 3. Ensure Stripe keys come from the Vercel Stripe integration (`pnpm env:pull`). 4. Forward webhooks to the CentraKit Stripe webhook route with the Stripe CLI (include the subscription billing events below, plus `payment_intent.*`, `charge.refunded`, `charge.dispute.*`, and `payout.*`; forward connected-account events to the Connect route with `--forward-connect-to` and thin account events to the accounts route with `--forward-thin-to`). 5. Configure Stripe Customer Portal in test mode for subscription changes, payment method updates, invoice history, billing details, and tax IDs. 6. Open `/{orgSlug}/billing` and `/{orgSlug}/usage` with `billing.manage`, and `/{orgSlug}/payments` with `payment.manage`. 7. Complete Connect onboarding for a test organization, send an invoice, and pay via the public and portal surfaces. 8. Check the organization's connected account, its status and the earned platform fee on `/admin/payment-accounts` as a system admin. ### Sandbox seed [#sandbox-seed] `pnpm stripe:sandbox:seed` (`packages/stripe/scripts/sandbox-seed.ts`) turns the demo billing rows into real objects in a Stripe sandbox. It publishes the plan catalog prices that still carry seed ids, creates or reuses one Accounts v2 account with the `customer` configuration for each seeded organization, starts a trial subscription on it for the organizations on a paid plan (`--trial-days`, default 30), syncs the customer and subscription through the Sync Engine, and removes the `cus_seed_` and `price_seed_` demo rows. - It only runs with a Stripe test or sandbox key (`sk_test_` or `rk_test_` in `STRIPE_SECRET_KEY`) and refuses a live key. - It writes to the local Supabase database from `readLocalSupabaseEnv()`, never to `POSTGRES_URL`, because `vercel env pull` can put a hosted URL there. - To seed a hosted develop database, pass its connection string and confirm it: `pnpm stripe:sandbox:seed --db-url "<develop connection string>?sslmode=require" --allow-remote`. `--db-url` without `--allow-remote` stops before connecting. Get the develop string from `pnpm exec supabase branches get develop --project-ref <ref> -o env`, and never point it at production. ## Webhook Events [#webhook-events] The webhook endpoint verifies the `stripe-signature` header, stores each Stripe event once by its event id in the webhook inbox and answers `202`. A repeated delivery of a stored event gets `200` and is not processed again. The inbox worker processes stored events within a minute, retries a failed event with backoff, and gives it up after eight attempts; the admin dashboard lists failed and given-up events. ### Subscription billing [#subscription-billing] The Sync Engine stores these; the billing block also handles `checkout.session.completed` and `customer.subscription.*`. - `checkout.session.completed` - `customer.subscription.created` - `customer.subscription.updated` - `customer.subscription.deleted` - `customer.created` - `customer.updated` - `customer.tax_id.created` - `customer.tax_id.updated` - `customer.tax_id.deleted` - `price.created` - `price.updated` - `payment_method.attached` - `payment_method.detached` - `invoice.created` - `invoice.finalized` - `invoice.finalization_failed` - `invoice.paid` - `invoice.payment_failed` - `invoice.payment_action_required` - `invoice.upcoming` - `invoice.voided` - `invoice.marked_uncollectible` ### Customer invoice payments [#customer-invoice-payments] Invoice payments are direct charges on the organization's connected account, so their events arrive on two separate endpoints, each with its own inbox and signing secret. Both are routed by the account on the event (`event.account` or the thin event's related account), never by metadata. `/api/v1/billing/stripe/connect/webhook` (`STRIPE_CONNECT_WEBHOOK_SECRET`) receives snapshot events from connected accounts: - `payment_intent.succeeded`, `payment_intent.processing`, `payment_intent.payment_failed`, `payment_intent.canceled` and `payment_intent.requires_action` update the invoice payment. - `charge.refunded` records the refunded amount and refunds the platform fee: a full refund returns the whole fee and a partial refund returns it in proportion. Refunds made in the embedded payment details component never refund the fee themselves, so this step does it. The event carries the cumulative refunded amount, so only an increase counts as a new refund. - `charge.dispute.created` and `charge.dispute.funds_withdrawn` mark the payment disputed; `charge.dispute.closed` returns it to succeeded when the dispute is won and keeps it disputed when lost, with the lost amount in `invoice_payments.dispute_lost_cents`. Disputes do not refund the fee. - `recompute_invoice_amounts` counts succeeded, disputed and refunded payments net of `refunded_cents` and `dispute_lost_cents`. A paid invoice owes at most that returned money, after charges and issued credit notes, so a credit note that covers it keeps the invoice settled and an invoice marked paid without payment rows stays at zero. After a new refund or lost dispute the worker calls `reopen_invoice_after_payment_reversal` (service role only), which moves a paid invoice with an amount due back to sent, overdue or uncollectible and writes the status history. - A new refund or lost dispute dispatches `invoice.refunded` or `invoice.dispute-lost` and an in-app notification of the same type to the invoice creator. Both are keyed by the charge and its cumulative refunded amount, or by the dispute id, so a repeated event reopens and notifies once. - `refund.created` and `charge.dispute.updated` are acknowledged without a state change. - `payout.created`, `payout.paid`, `payout.canceled` and `payout.updated` are acknowledged; `payout.failed` also refreshes the stored account state. - A refund or dispute that arrives before `payment_intent.succeeded` first settles the payment, so the paid transition and its workflows run before the reversal. - A payment update only applies when the stored status is still the one it read, so the portal return and the webhook for the same payment report each transition once. Payments created as v1 destination charges before Accounts v2 live on the platform account, so their PaymentIntent, refund and dispute events arrive on `/api/v1/billing/stripe/webhook`. That handler looks the PaymentIntent up among stored invoice payments whose account is no longer a payment account and applies the event with the same rules as above. `/api/v1/billing/stripe/accounts/webhook` (`STRIPE_ACCOUNTS_WEBHOOK_SECRET`) receives thin v2 account events (`v2.core.account[requirements].updated` and the merchant and customer `capability_status_updated` events). The handler retrieves the account and stores its status, so the Payments page updates without a manual refresh. ## Go-Live Checklist [#go-live-checklist] - Confirm live Stripe products, prices, and lookup keys match configured environment variables. - Confirm live Stripe meters use the same event names as `billing_usage_meters.stripe_event_name`. - Register the live webhook endpoint and verify `STRIPE_WEBHOOK_SECRET` is the live signing secret. - Run `pnpm stripe:sync:migrate` and `pnpm stripe:sync:backfill` against the production database (see Hosted setup). - Configure Customer Portal, invoice branding, support details, tax settings, Smart Retries, and dunning. - Complete Stripe Connect platform onboarding/approval; confirm Accounts v2 with the merchant configuration, Stripe as fees and losses collector, and the embedded components. - Confirm `/admin/settings` invoice payment fee matches the commercial agreement. - Replay sandbox webhooks and test card, iDEAL, SCA, retry, failure, refund, dispute, cancellation, reactivation, and invoice download scenarios for both subscription and customer-invoice flows. - Confirm no application code path handles raw payment credentials or legacy Stripe APIs. - Confirm all four plans expose audit logs and the daily retention workflow applies their configured tier, including any Enterprise override. - Monitor webhook failures, payment failures, subscription drift, invoice download failures, Connect account restrictions, and reconciliation errors. # Supabase Local Dev (/docs/operations/supabase-local-dev) Local Supabase startup, sync, reset, seeds, storage, and generated types. The local Supabase stack backs the SaaS and API apps during development. ## Startup [#startup] ```bash pnpm supabase:start pnpm supabase:sync ``` The sync task pushes pending migrations and seeds to the local database (falling back to a full reset when the push fails) and then seeds the Storage buckets. OAuth-related warnings about missing Google credentials are safe unless you are testing OAuth. ## Stack engine [#stack-engine] `supabase/config.toml` turns on the Supabase CLI stack engine with `[experimental] stack = true`. It needs CLI 2.121.0 or later, which the pnpm catalog pins. - Postgres starts right away. Every other service starts on its first request and stops after a minute idle, so an idle project uses a few hundred MiB instead of several GiB. - The config sets no ports. The CLI assigns them per project from the range 20000 to 32767 and keeps them across restarts. `pnpm supabase:start` writes the URLs and keys to `.env.development.local`, and scripts read them from `supabase status --output-format json` through `@workspace/foundation/local-supabase`. - A project is identified by its directory and git branch. Each git worktree gets its own project, so give each coding agent its own worktree and run `pnpm supabase:start` there. Checking out another branch in the same directory starts a separate, empty project. - `pnpm supabase:start` passes `--runtime native`, which runs every service as a plain process without a container engine, the same as CI. A project keeps the runtime it was created with, so for a project created earlier on Docker the script falls back to `--runtime auto` and stays on Docker until you destroy it. - `pnpm exec supabase stack list` lists every local project on the machine. `pnpm supabase:stop` stops this one and keeps its data, and `pnpm --filter @workspace/supabase-runtime supabase:destroy` deletes it. - Local `start` and `status` have no `-o` flag. Use `--output-format json` or `--env`. `supabase stop --no-backup` is replaced by `supabase stack destroy`. - The engine rejects Auth email templates with `content_path`, so the confirmation template lives under `[remotes.main]` and `[remotes.develop]`. Local confirmation emails use the default link, which `/auth/confirm` accepts as a PKCE `code`. Stack projects keep their data apart from the previous engine. After switching, stop the old containers with `SUPABASE_EXPERIMENTAL_STACK=0 pnpm exec supabase stop`, then run `pnpm supabase:start` and `pnpm supabase:sync`. ## Reset [#reset] Use reset only when a clean local database is intended: ```bash pnpm supabase:reset ``` After reset, browser sessions from the previous local auth database are stale. Sign in again with the seeded test account. ## Seed Account [#seed-account] - Email: `test@example.com` - Password: `Password123!` - Organizations: `tractor-service`, `auto-garage`, `bike-scooter-shop`, `equestrian-center` `test@example.com` is also the seeded system admin. All seeded users share the same password. [Local development](/docs/getting-started/local-development#seeded-logins) lists every persona, including the portal-only customers `test3@example.com`, `test4@example.com` and `test5@example.com` and the role-specific accounts such as `owner@example.com`, `crm.editor@example.com` and `viewer@example.com`. Seed files provide deterministic coverage for every shop organization: customers, contacts, roles, teams, products, inventory, customer assets, tasks, agenda items, quotes, invoices, orders, files, inbox threads, AI chat history, notifications, employees, time, expenses, hiring, workflows, webhooks, audit logs and billing read models. Files `100` to `170` in `supabase/seeds/` hold the demo data, one domain per file, and each starts with a `-- Covers:` comment. `docs/agents/supabase-schema.md` describes the id scheme and the conventions for adding rows. ## Storage [#storage] Storage buckets are configured through `supabase/config.toml` and seeded from `supabase/seed-buckets/`. Bucket ids match product features: `users`, `customers`, `organization`, `assets`, `products`, `files`, `knowledge`, `ai`, `integrations`, `audit-logs`, and `workflows`. The reset flow recreates those local buckets and loads public seed objects from the matching folders. Object paths are UUID-first (`{orgId|userId}/…`); never use org slugs as the tenant folder. On a hosted project, create the eleven new buckets and delete the old ids after deploy. Existing hosted objects are discarded for this cutover; path columns 404 until objects are re-uploaded or the remote database is reset. ## Generated Types [#generated-types] Run `pnpm supabase:types` after schema changes. It runs `better-supabase gen` against the local database with `better-supabase.config.ts` and writes the schema and database types to `packages/supabase-runtime/src/generated/`. The config lists both `public` and `graphql_public`, so the types keep the GraphQL definitions. CI runs `better-supabase gen --check` and fails when the committed files are out of date. # Vercel metrics pass (/docs/operations/vercel-metrics-pass) How to run a metrics-first vercel-optimize / CDN + PPR health check for CentraKit apps. Use this after shipping Cache Components or Functions changes. Do **not** invent cost or caching recommendations from a code-only grep. Follow the `vercel-optimize` skill in `.agents/skills/vercel-optimize/SKILL.md`: collect signals first, apply its gate, then investigate. ## Prerequisites [#prerequisites] - Repository root linked to the single CentraKit project via `vercel link` - Observability Plus (or equivalent) enabled on the project - Production (or a representative preview) traffic available ## Checklist [#checklist] 1. **CDN and PPR.** Sample `vercel metrics` grouped by `cache_result`, `ppr_state`, and service for SaaS hub routes. Rising `blocking` or unexpected `BYPASS` on authenticated shells is a regression signal (see the `cdn-caching` skill in `.agents/skills/cdn-caching/SKILL.md`). 2. **Functions.** Compare duration and CPU by service for the AI routes in `apps/api/app/api/v1/ai/` (chat and specialists set `maxDuration = 300`, transcription `60`) and for the API service's per-minute jobs drain, `/api/v1/internal/jobs/drain`. 3. **Crons.** Confirm Sentry Cron monitors receive check-ins from the App Router handlers (`cronMonitor` from `apps/api/lib/cron-monitor.ts` on the drain route; `automaticVercelMonitors` is off for App Router). Record findings in the PR or ops runbook; only change platform memory/region/Fluid settings when metrics justify it. # Workflows (/docs/operations/workflows) Inspect durable graph runs for quote, invoice, task, agenda, and scheduled system automations. Product automations are published system graphs in the `<stamp>_system_workflows.sql` data migration in `supabase/migrations/`. Domain mutations and the Stripe webhook call `dispatchWorkflowEvent`; a schedule trigger fires as a `workflow_schedule` background job (see [Deployment](/docs/getting-started/deployment)) that starts the run. The Workflow SDK executes each matched graph via `runWorkflowGraph`. ## Quote and invoice cadences [#quote-and-invoice-cadences] | Graph | Start | Notes | | --- | --- | --- | | `quote-lifecycle` | `quote.sent` | 7-day and 1-day customer reminders, owner nudge, expire, 30-day auto-reject, then waits for `quote.invoiced` after acceptance | | `quote-accepted` | `quote.accepted` | Emails, then draft invoice + follow-up task. Cancels the lifecycle sleep for that quote | | `quote-rejected` | `quote.rejected` | Owner notify. Cancels the lifecycle sleep | | `invoice-lifecycle` | `invoice.sent` | Template reminder cadence (negative `daysAfterDue` allowed), overdue, owner nudge | | `invoice-paid` / `invoice-cancelled` | matching events | Notify; cancel the lifecycle sleep for that invoice | | `invoice-payment-failed` / `invoice-disputed` | Stripe webhook | Notify only; lifecycle reminders keep running | | `invoice.refunded` / `invoice.dispute-lost` | Stripe webhook | Domain events only, with no system graph. The webhook reopens a paid invoice and notifies the invoice creator; a cancelled lifecycle does not restart | `quote.invoiced` is written by the `invoices_emit_quote_invoiced` trigger on `public.invoices` through `better_supabase.emit_event`, in the same transaction as the invoice. It fires when an invoice of kind `invoice` that is not cancelled is created for a quote, linked to one through `source_quote_id`, or created for an order whose `quote_id` points at the quote. The payload carries `quoteId`, `invoiceId`, `templateId` and, for order invoices, `orderId`. After Accepted, and after Deposit paid on the deposit path, the quote lifecycle reads `quote.get-status`, whose `invoiced` output says whether such an invoice already exists, and only waits for `quote.invoiced` when it does not. When a lifecycle reaches a terminal stage it cancels the automations still running on the record, except those started by an event that moved the lifecycle into a success stage (for example `quote.accepted` and `quote.invoiced`), so the Quote accepted run that creates the draft invoice and follow-up task finishes even when its invoice ends the lifecycle. Sibling cancellation uses `cancelRunsForDomainRecord` so a run asleep in `delay` does not wait for the next status check. On a terminal quote or invoice event it cancels every active run on that record, whichever workflow key started it, so organization copies of the lifecycle graphs stop too. ## Order, purchase order, and shipment lifecycles [#order-purchase-order-and-shipment-lifecycles] | Graph | Start | Happy path | | --- | --- | --- | | `order-lifecycle` | `order.confirmed` | Confirmed, paid, picking, shipped, delivered, invoiced, completed | | `purchase-order-lifecycle` | `purchase-order.sent` | Sent, confirmed (manual), in transit, received, billed (manual) | | `shipment-lifecycle` | `shipment.shipped` | In transit, out for delivery, delivered | The dispatcher routes an event to one subject by its prefix, so an order lifecycle only hears `order.*` events. `reactToCommerceEvent` (`packages/core-workflow-runtime/src/workflow-runtime/order-events.ts`) mirrors related events onto the order and purchase order: - `invoice.sent` / `invoice.paid` become `order.invoiced` / `order.paid`. - Outbound shipment events become `order.shipped`, `order.ready-for-pickup`, `order.delivered`, or `order.picked-up`. A delivered return becomes `order.returned`. - An inbound shipment with a purchase order becomes `purchase-order.in-transit`. The same reactor creates an order when a quote is accepted and its template has **create order on accept** set. It also sends the built-in staff notifications for confirmed and backordered orders, shipment exceptions, received and overdue purchase orders, and low stock. The `commerce_daily` background job (`17 6 * * *`) publishes `purchase-order.overdue` once per promised date, `promotion.expiring` for active promotions ending within 3 days, and `promotion.limit-reached` when committed redemptions reach the total limit. ## Starter automations [#starter-automations] Templates offer one-click starters from `packages/automation/src/workflows/starter-automations.ts`. Most are attached to the template. The starters triggered by stock events are created organization-wide instead, because `inventory.*` events carry no template. | Starter | Trigger | Does | | --- | --- | --- | | Low stock reorder | `inventory.low-stock` (organization-wide) | Drafts a purchase order from the reorder point and sends it. The `purchase-order.send` authorize floor can require approval | | Purchase order overdue | `purchase-order.overdue` | Reminds the supplier and notifies the purchaser | | Back in stock | `inventory.back-in-stock` (organization-wide) | Fills backorders for the variant and tells customers whose order is complete again | | Prepaid order to picking | `order.paid` when the order is paid up front | Creates a draft shipment as the pick list | | Ready for pickup | `order.ready-for-pickup` | Messages the customer (WhatsApp when connected, otherwise email) and reminds them after 7 days | | Delivered, then invoice | `order.delivered` for on-fulfillment orders | Invoices the delivered lines and sends the invoice | ## Scheduled ops [#scheduled-ops] | Graph | Cron (Europe/Amsterdam) | Action | | --- | --- | --- | | `task-due-scan` | `0 7 * * *` | Publish `task.due-soon` / `task.overdue` with idempotency `kind:taskId:dueAt` | | `invoice-overdue-digest` | `0 8 * * 1` | Email overdue invoices | | `draft-stale-digest` | `0 8 * * 1` | Email draft quotes/invoices older than 14 days | | `billing-usage-threshold` | `0 * * * *` | Publish usage-threshold events; `billing-usage-threshold-notify` emails owners | System schedules run once per enabled organization that has not opted out of the key and has no published workflow of its own on it. Each run carries the organization, and the scans and digests only read that organization's records. ## Inspecting runs [#inspecting-runs] ### Local development [#local-development] There is no root `workflow:*` script. The Workflow SDK CLI (`workflow`, also installed as `wf`) is a dependency of `apps/saas` and `apps/api`, so run it through pnpm from the app whose runs you want to see. Start the apps (`pnpm dev:portless`), then open the local run viewer: ```bash pnpm --filter api exec workflow web ``` For non-interactive output, list runs, steps or events with `workflow inspect`: ```bash pnpm --filter api exec workflow inspect runs pnpm --filter api exec workflow inspect steps --runId=<runId> ``` Use `--filter saas` instead for runs started by the SaaS app. Add `--backend vercel --env preview` (or `--env production`) to inspect a deployed environment with your `vc login` session. ### Production / preview [#production--preview] Open the Vercel dashboard and go to **Workflows** under **Observability**. Runs keep the domain id (`quoteId`, `invoiceId`, `taskId`, `agendaItemId`) on the trigger payload. ## REST [#rest] Organization workflows expose `POST …/workflows/definitions/{definitionId}/publish`, `POST …/workflows/runs/{runId}/cancel`, `POST …/workflows/runs/{runId}/restart` (start a failed or cancelled run again without repeating completed side effects), and `POST …/workflows/runs/{runId}/approval` (approve or deny a waiting approval step; the caller also needs the permission the approval node requires). `GET …/workflows/runs?definitionKey=` lists runs across every version of a key. ## When a workflow goes wrong [#when-a-workflow-goes-wrong] `workflow-failed-alert` emails organization owners when another published graph fails. It does not rematch itself. Check the failed run's step error, then replay or fix the domain record and republish if the definition changed. # Access control (/docs/product/access-control) How to manage an organization's users, invitations, teams, roles and approval rules, and how built-in and custom roles grant permissions. ## Overview [#overview] Access control decides who can open an organization and what they can do there. It lives in the sidebar under Organization, Access: | Route | Purpose | | --- | --- | | `/{orgSlug}/users` | Members tab with every user of the organization. | | `/{orgSlug}/users/{userId}` | One user's details, role and access. | | `/{orgSlug}/users/invitations` | Pending invitations tab. | | `/{orgSlug}/users/invitations/create` | Invite a user. | | `/{orgSlug}/teams` | Teams list. | | `/{orgSlug}/teams/create` | Create a team. | | `/{orgSlug}/roles` | Roles & Permissions, with the Roles tab. | | `/{orgSlug}/roles/create` | Create a custom role. | | `/{orgSlug}/roles/approval-rules` | Approval rules tab. See [Approvals](/docs/product/approvals). | Customer contacts who only use the [customer portal](/docs/product/customer-portal) are not users of the organization and do not appear here. ## Key concepts [#key-concepts] ### Users [#users] A user is a person with a role in the organization. Each user has exactly one organization role. Users can be suspended in the organization or removed from it, which leaves their account and their other organizations as they are. Disabling an account blocks it on the whole platform, so only a system administrator can do it, under [Users](/docs/admin/users). ### Roles [#roles] A role is a set of permissions. Every organization has four built-in roles that cannot be edited or deleted: | Role | Access | | --- | --- | | Owner | Everything an admin can do, plus billing, deleting the organization and transferring ownership. | | Admin | Manage users, settings, roles and integrations. | | Member | Standard write access to business data. | | Viewer | Read-only access. | You can add custom roles from the permissions that Admin, Member and Viewer allow. Owner-only permissions, such as managing billing or deleting the organization, never fit in a custom role. Built-in roles show as Locked in the roles list. ### Permissions [#permissions] Permissions follow the pattern `resource.action`, for example `quote.send`. Most resources have read, create, update and delete permissions, and some add their own actions. Every page of these docs lists the permissions it needs. See [RBAC](/docs/architecture/rbac) for how permissions are checked. ### Teams [#teams] A team groups users who work together, such as Field Operations. Teams are used to assign work, share calendars and mention groups with `@`. Teams do not grant permissions. A team has a name, a slug, an icon and a description, and can be archived when it is no longer used. ### Invitations [#invitations] An invitation asks someone to join the organization with a role. It is sent by email and is valid for seven days. Pending invitations can be edited, resent or revoked until they are accepted. An expired invitation can be edited again after it is resent. Inviting an address that already has a pending invitation replaces it, and resending sends a new link, after which the earlier link stops working. ## Manage users [#manage-users] ### Invite a user [#invite-a-user] 1. Open Users and select Invite, or open `/{orgSlug}/users/invitations/create`. 2. Enter the email address and pick a role. First and last name are optional. 3. Select Send invitation. The invitee opens the link at `/invitations/{token}`. They log in or create an account with that email address, confirm it, and then join the organization with the chosen role. If they are signed in with a different email address, the page tells them to switch accounts. Expired, revoked and already accepted invitations show a clear message instead. An invitee who already has an account can also accept or decline the invitation from their notifications. When the person who sent the invitation can no longer invite with that role, the invitation can no longer be accepted. ### Manage pending invitations [#manage-pending-invitations] The Pending invitations tab lists email, role, sent date and expiry, with Active and Expired filters. Row actions are Open, Copy email, Resend invitation and Revoke invitation. Select several rows to revoke them together. ### Change a user's role or access [#change-a-users-role-or-access] Open a user from the Members tab. On the detail page you can: - Choose a different role. The list holds the roles you may hand out: an owner or admin assigns Admin, Member and Viewer, and a custom role appears when you hold every permission it grants. Owner is never in the list; ownership moves only through Transfer ownership in the organization settings. - Update the user's profile details and photo. - Suspend the user under Member access with **Suspend member** and confirm. A suspended member keeps their role and their account, can still sign in and use their other organizations, but loses every permission in this organization: the organization drops out of their session, its pages and data, until you choose **Resume member**. The users list marks them **Suspended**. You cannot suspend yourself, a member whose role is above your own, or the last active owner. - Remove the user from the organization under Member access. Type the confirmation text to confirm. The Members tab can be searched and filtered by Access and Account status. Row actions are Open and Copy email. ## Manage teams [#manage-teams] 1. Open Teams and select New team. 2. Enter the name, slug, icon and description, and select Create team. 3. On the team page, use Add team members to pick existing users of the organization, and save. Archive a team to hide it from active lists, or unarchive it to bring it back. Deleting a team is permanent. Both actions are available on the team page under Danger zone and as row and bulk actions in the list. ## Manage roles [#manage-roles] 1. Open Roles & Permissions and select New role. 2. Enter a name. The key is generated from the name and must be lowercase with dashes. 3. Add an optional description. 4. Select the permissions the role grants, and save. The list shows only permissions you hold yourself, and saving a role with any other permission is refused. To change a custom role, open it from the list and edit its details or permissions. Delete a role from its row actions. Give its users and pending invitations another role first, because a role that is still assigned cannot be deleted. ## Permissions [#permissions-1] | Permission | Allows | | --- | --- | | `member.read` | See users and invitations. | | `member.invite` | Invite users and resend or revoke invitations. | | `member.update` | Change roles, profile details and access. | | `member.remove` | Suspend, resume and remove users in the organization. | | `team.read`, `.create`, `.update`, `.delete` | Work with teams. | | `role.read`, `.create`, `.update`, `.delete` | Work with custom roles. | | `approvalPolicy.manage` | Manage approval rules. | ## Automation and AI [#automation-and-ai] The assistant can read teams, users, invitations, roles and permissions. It can also create and update teams, invite users, change roles and edit custom roles, within the same limits as the role editor. Every change to access asks for approval first. See [Chat](/docs/product/chat). ## API [#api] All routes live under `/api/v1/organizations/{organizationId}/`: | Method and path | Purpose | | --- | --- | | `GET members` | List users. | | `POST members` | Invite a user. | | `GET members/{userId}` | Read one user. | | `PATCH members/{userId}` | Change a user's role. | | `GET teams` and `POST teams` | List and create teams. | | `GET`, `PATCH` and `DELETE teams/{teamId}` | Read, update and delete a team. | | `PUT teams/{teamId}/members` | Replace a team's members. | | `GET invitations` | List invitations. | | `GET`, `PATCH` and `DELETE invitations/{invitationId}` | Read, update and revoke an invitation. | Not yet available: REST endpoints for roles and approval rules. See [API](/docs/developers/api) and [OpenAPI](/docs/developers/openapi). ## Related pages [#related-pages] - [Approvals](/docs/product/approvals) - [Organizations](/docs/product/organizations) - [Audit log](/docs/product/audit-log) - [RBAC](/docs/architecture/rbac) - [Roles and permissions](/docs/admin/roles-and-permissions) # Agenda (/docs/product/agenda) How to schedule agenda items in shared and personal calendars, plan task work on the Planning board, subscribe through iCalendar feeds and import Google Calendar events. ## Overview [#overview] Agenda is the organization's calendar. It holds agenda items in calendars that people own or share, and a Planning board that schedules task work onto people, teams or customers. Agenda lives under Workspace, Work in the sidebar, next to Tasks and Planning. The routes are: | Route | Purpose | | --- | --- | | `/{orgSlug}/agenda` | Calendar with Day, Week, Month and Agenda list views. Week is the default. | | `/{orgSlug}/agenda/{agendaItemId}` | Agenda item detail. | | `/{orgSlug}/agenda/calendars` | Calendars you can view or manage. | | `/{orgSlug}/agenda/calendars/{calendarId}` | Calendar settings. | | `/{orgSlug}/agenda/planning` | Planning board. | ## Key concepts [#key-concepts] ### Calendars [#calendars] Every agenda item belongs to one calendar. A calendar has a name, description, color, default time zone and an owner, and its owner can share it with users and teams as Viewer or Editor. Each person chooses which calendars are shown in their own view. **Remove from list** takes a calendar out of your own list without deleting it; add it back with New calendar and Browse calendars. Calendars you turn off move below the ones you show the next time the agenda loads. Agenda items of done tasks are hidden unless you show completed tasks in the filters. Three calendars are reserved. They cannot be deleted or transferred: | Calendar | Purpose | | --- | --- | | Organization | The default company calendar. | | Personal | Each user's own calendar. | | Planning | One organization-wide calendar that holds scheduled task work. It is always shared with every user. | Calendars imported from Google Calendar are read-only in CentraKit. Their details, ownership, sharing and deletion are managed at the source. ### Agenda items [#agenda-items] An agenda item follows standard calendar semantics so it can travel through iCalendar feeds: - Title, rich text description, start and end, or all day. - Location and meeting URL. - Show as Busy or Free, and status Confirmed, Tentative or Cancelled. - Invitees: people, teams, customers or any email address. - Recurrence: daily, weekly, monthly or yearly, with an interval, weekdays, a day of the month or weekday position, and an end date. - Notifications a number of minutes or days before the start, shown in the app or sent by email. - A related task, which links the item to work. When you edit or delete a recurring item, choose This event or Entire series. ### Planning [#planning] The Planning board is a timeline of people, teams or customers (Group rows by). It has Day, Week and Month scales and respects working hours. Open tasks that are not scheduled yet sit in a side panel that you can filter and sort by due date or priority. Assigning a task on the board creates an agenda item on the Planning calendar and links it to the task. The reserved time shows up on the task as a draft time entry until someone confirms it. The board flags scheduling clashes and people who are missing a task's required skills. ## Work with Agenda [#work-with-agenda] ### Create an agenda item [#create-an-agenda-item] Select New agenda item, or select a time slot in the Day, Week or Month view, and fill in the schedule, invitees, recurrence and notifications. Drag an item to move it, or drag its edge to resize it. Open the item for its full detail page, where you can also assign or create a related task. ### Create and share a calendar [#create-and-share-a-calendar] Open Calendars and select New calendar. In the calendar settings you can: - Change the name, color, description and default time zone. - Grant users and teams Viewer or Editor access under Sharing. - Transfer the calendar to yourself, the organization or another user. - Set your own default reminders and choose which changes email you: new, changed and cancelled events, and event responses. - Generate a private iCalendar URL. - Delete the calendar from the Danger zone. This deletes it for everyone, so move or delete its agenda items first. ### Plan task work [#plan-task-work] Open Planning, choose a date and scale, then drag open tasks from the side panel onto a row. Use Reassign task or Remove from planning on a scheduled block. You can also create a task directly from the board. Suggestions shows AI-proposed draft bars for unscheduled tasks. Accept a suggestion to schedule it, dismiss it, or drag it to another slot. The Suggestions filter shows or hides the draft bars. ### Subscribe with iCalendar [#subscribe-with-icalendar] Each calendar's settings page can generate a private iCalendar URL for you. Paste it into another calendar app to subscribe to that calendar. The feed is served from `/portal/calendars/{token}` and is checked against your current access on every request. Anyone with the URL can read the calendar, so use Rotate private URL to invalidate an old one. ### Import Google Calendar [#import-google-calendar] Open the Google Calendar integration from [Integrations](/docs/product/integrations) and select Connect Google Calendar. After you sign in with Google, your primary calendar is enabled and imported right away. Enable other calendars under Enable calendar import. Imported events are read-only and appear under your own calendars. - A background job syncs enabled calendars incrementally every 5 minutes. Select Sync now to refresh immediately. - If the first import fails, the connection shows a notice and Sync now retries it. - If Google access is revoked or expires, the connection is flagged and offers Reconnect. Reconnecting keeps your calendar settings and sync state. The import is one way. To show CentraKit items in Google Calendar, subscribe to a private iCalendar URL instead. Not yet available: two-way sync, CalDAV and outbound invitation emails with RSVP handling. ## Permissions [#permissions] | Key | Grants | | --- | --- | | `agenda.read` | View the agenda and calendars, set personal calendar preferences and notifications, and manage your own private iCalendar URLs. | | `agenda.create` | Create agenda items and calendars. | | `agenda.update` | Edit and move agenda items, change calendar details and sharing, and transfer calendars. | | `agenda.delete` | Delete agenda items and calendars. | Calendar sharing applies on top of these keys: a calendar Viewer cannot edit its items. Planning also needs `task.update` to assign or unassign tasks and `task.create` to create a task from the board. Opening the Google Calendar integration page and changing import settings requires `integration.manage`. ## Automation and AI [#automation-and-ai] The automatic Agenda item follow-up workflow notifies assignees when an item starts and again one hour after it ends, if the item still exists. See [Workflows](/docs/product/workflows). MCP tools for agenda items: | Tool | Kind | | ------------------ | ----- | | `listAgendaItems` | Read | | `getAgendaItem` | Read | | `createAgendaItem` | Write | | `updateAgendaItem` | Write | | `moveAgendaItem` | Write | | `deleteAgendaItem` | Write | The assistant side panel receives the visible agenda as context. See [MCP](/docs/developers/mcp). ## API [#api] Organization paths are relative to `/api/v1/organizations/{organizationId}`. | Resource | Operations | | --- | --- | | `agenda/items` | `listAgendaItems` (GET), `createAgendaItem` (POST) | | `agenda/items/{agendaItemId}` | `getAgendaItem` (GET), `updateAgendaItem` (PATCH), `deleteAgendaItem` (DELETE) | | `agenda/items/{agendaItemId}/move` | `moveAgendaItem` (POST) | Two routes sit outside the organization prefix: - `GET /api/v1/public/calendars/{token}` (`downloadPublicCalendarFeed`) serves the iCalendar feed behind `/portal/calendars/{token}`. - `GET /api/v1/agenda/google-calendar/oauth/start` (`startGoogleCalendarOAuth`) starts the Google sign-in. The Google Calendar sync runs as background jobs: every 5 minutes `calendar_sync_sweep` queues one `calendar_sync` job per calendar that has not synced for 4 minutes. See [API](/docs/developers/api) and [OpenAPI](/docs/developers/openapi). ## Related pages [#related-pages] - [Tasks](/docs/product/tasks) for the work that Planning schedules. - [Time](/docs/product/time) for confirmed time entries. - [Integrations](/docs/product/integrations) for the Google Calendar connection. - [Notifications](/docs/product/notifications) for reminders and calendar emails. # Approvals (/docs/product/approvals) How approval rules make risky actions by people, AI agents, workflows, MCP clients and API keys wait for a human decision, and how approvers decide, escalate and expire requests. ## Overview [#overview] Approvals put a human checkpoint in front of risky actions. When an action matches an approval rule, CentraKit records an approval request and the action waits until someone confirms or approves it. Approval rules are managed on the Approval rules tab of Roles & Permissions at `/{orgSlug}/roles/approval-rules`. Requests are decided from [Notifications](/docs/product/notifications). Approvals cover actions from every source: the assistant, workflows, MCP clients, API keys and people in the app. Requests for time off, timesheets and expenses use the same requests but have their own review flow in [People](/docs/product/people), [Time](/docs/product/time) and [Expenses](/docs/product/expenses). ## Key concepts [#key-concepts] ### Actions and effects [#actions-and-effects] Every gated action has a key, such as `quote.send`, and a risk level of low, medium, high or critical. Actions are also tagged with effects that describe why they are risky: | Effect | Example | | ----------------------- | --------------------------------- | | Customer-facing | Sending a quote to a customer. | | Moves money | Writing off an invoice. | | Destructive | Deleting or archiving records. | | Changes access | Inviting users or changing roles. | | Calls an outside system | Sending data to an integration. | | Bulk change | Updating many records at once. | A rule can target one action, every action with an effect, or any action. ### Approval rules [#approval-rules] A rule has these parts: | Field | Meaning | | --- | --- | | Name and Description | How the rule appears in the list and to approvers. | | Action | One action, or Any action. | | Effect | One effect, or Any effect. | | Who is acting | People, AI agents, Workflows, MCP clients and API keys. Selecting none means everyone. | | Only when | Up to ten conditions on the action input, such as `input.quantity` more than 50. Operators are equals, is not, more than, at least, less than, at most, is one of, contains and is set. | | Requires | Someone else approves, or The requester confirms. | | Approvers | Anyone allowed to do the action, or holders of a chosen permission. | | Approval mode | Any approver, Every approver or In order. | | Approvals needed | The quorum, from 1 to 10. | | Expires after (hours) | The request expires if nobody decides in time. | | Escalate after (hours) and Escalate to | Who is notified when the request is still open after that time. Set both or neither. | | The requester may approve their own request | Allows self-approval. | | Rule is active | Turns the rule on or off without deleting it. | ### Platform rules [#platform-rules] Platform administrators can set rules that apply to every organization. They appear in the list with a lock and the note "Set by the platform. Organizations can add stricter rules but not remove this one." Organization rules can only add approvals, never remove one that the platform requires. See [Roles and permissions](/docs/admin/roles-and-permissions). ### Approval requests [#approval-requests] A request records the action, who asked for it and from where (the assistant, a workflow, an MCP client, the app or the API), and an explanation of the intent, the impact, the data it touches and how to roll it back. | Status | Meaning | | ---------------------- | ----------------------------------------- | | Requested | Waiting for a decision. | | Approved | Approved and ready to run once. | | Denied | Refused. The action does not run. | | Expired | Nobody decided before the deadline. | | Superseded or Canceled | Replaced by a newer request or withdrawn. | | Consumed | The approved action ran. | An approval is tied to the exact input that was requested. If the input changes, the action needs a new approval. Records that wait on a request show Awaiting approval, Approved or Approval denied in their progress bar. ### Actions that are gated today [#actions-that-are-gated-today] Besides AI and workflow actions, these domain actions check approval rules: - Sending a quote and writing off an invoice, in the app and through the API. - Creating, confirming and cancelling orders, and order proposals from the [Inbox](/docs/product/inbox). - Shipping a shipment. - Creating and receiving purchase orders. - Deleting a customer and correcting or counting stock, in the app. The stock input carries `onHandQuantity`, the new quantity on hand. Rules for other actions apply to the assistant, workflows and MCP clients. People in the app are not held back by them yet. ## Manage approval rules [#manage-approval-rules] ### Add a rule [#add-a-rule] 1. Open Roles & Permissions and select the Approval rules tab. 2. Select Add rule. 3. Fill in the New approval rule dialog and select Save. When the organization has no rules yet, the tab shows "No approval rules yet". Risky AI actions still ask the requester for confirmation without a rule. Add a rule to require a second person. ### Edit, pause or delete a rule [#edit-pause-or-delete-a-rule] Use the row actions Edit rule and Delete rule. To pause a rule, turn off Rule is active. Deleting a rule means actions it covers no longer wait for approval. ## Decide a request [#decide-a-request] Approvers get an approval notification in the topbar and in the Notifications hub. Select Approve or Deny on the row. When a rule needs more than one approver, the toast reads "Decision recorded; more approvals are needed" until the quorum is met. For actions started in the app, through the API or from an MCP client, run the action again with the same input after it is approved. The assistant and workflows continue on their own. ### Escalation and expiry [#escalation-and-expiry] A scheduled job runs every ten minutes. It notifies the escalation target for requests that passed their escalation time and marks requests as expired when they pass their expiry time. The requester is notified of the outcome. ## Permissions [#permissions] | Permission | Allows | | --- | --- | | `approvalPolicy.manage` | Create, edit and delete the organization's approval rules. | | `platform.approvalPolicy.manage` | Manage platform rules in Admin. | Deciding a request needs the approver permission set on the rule. By default that is the permission of the action itself. ## Automation and AI [#automation-and-ai] - The assistant asks for approval before risky actions and shows the request in the chat. See [Chat](/docs/product/chat). - Workflows can include an approval step that pauses the run until someone decides. See [Workflows](/docs/product/workflows). - MCP clients receive an `APPROVAL_REQUIRED` error with the `approvalId` when a tool call needs approval. Confirm the request in Notifications or wait for the approver, then call the same tool again with the same input. See [MCP](/docs/developers/mcp). ## API [#api] REST calls that hit an approval rule return `409` with the `APPROVAL_REQUIRED` code and an `approvalId` extension. Retry the same request after approval. See [Errors](/docs/developers/errors). An approval covers exactly the call it was asked for: the same action, the same input, the same person and the same client or agent. The approved call runs once. Repeating it with the same input, or retrying a rejected request, is refused until the original request window ends; change the input to ask again. `GET` and `POST /api/v1/organizations/{organizationId}/approvals` list and decide pending time off, timesheet and expense requests. They do not cover approval rules or other approval requests. Not yet available: REST endpoints for approval rules and general approval requests. ## Related pages [#related-pages] - [Notifications](/docs/product/notifications) - [Access control](/docs/product/access-control) - [Workflows](/docs/product/workflows) - [Audit log](/docs/product/audit-log) - [RBAC](/docs/architecture/rbac) # Audit log (/docs/product/audit-log) How to review, filter, inspect and export the organization audit log, including restricted details, CSV exports, retention per plan and the audit API. ## Overview [#overview] The audit log is the organization's security and change record. It lists who did what, to which record, from which surface and with what outcome. Events are append-only: nobody in the organization can edit or delete them. The audit log lives under Organization, Account, Audit log in the sidebar. The routes are: | Route | Purpose | | -------------------------------- | ----------------------------------------- | | `/{orgSlug}/audit-log` | The event table with filters and exports. | | `/{orgSlug}/audit-log/{eventId}` | Event detail. | Platform administrators have a separate log across organizations at [Admin audit log](/docs/admin/audit-log). For how events are captured and stored, see [Audit logs architecture](/docs/architecture/audit-logs). ## Key concepts [#key-concepts] ### Event fields [#event-fields] Each event records: - Timestamp, shown in local time and UTC. - Actor: a user, platform admin, system, API key, integration, workflow or webhook. Platform admin identities are masked in the organization view. - Event type and category. - Target: the affected record and its type. - Source: SaaS app, API, MCP, AI, workflow, webhook, database or system. - Outcome: success, failure, denied or attempted. - Structured changes and request context, including request ID and correlation ID. ### Categories [#categories] Events are grouped into Authentication, Users, Permissions, Billing, Data, Settings, Integrations, Security and System. ### Restricted details [#restricted-details] Some events carry restricted request context: IP address, session ID and user agent. These fields are hidden by default. Revealing them requires a dedicated permission, and every reveal writes its own `audit.revealed` event. For a changed record, the restricted details also hold the old and new value of each changed field. ### Retention [#retention] Retention depends on the organization's plan: | Plan | Retention | | ---------- | ------------------------------------------------ | | Starter | 1 day | | Pro | 7 days | | Business | 90 days | | Enterprise | 365 days, or a custom period set by the platform | A daily job removes events older than the retention period. See [Billing and usage](/docs/product/billing-and-usage). ## Work with the audit log [#work-with-the-audit-log] ### Find events [#find-events] The table has columns for Timestamp, Actor, Event, Target and Source and outcome. Use Search events and targets to search, and the filters for date, actor kinds, event types, categories, sources and outcomes. Filters and paging live in the URL, so you can share a filtered view. ### Inspect an event [#inspect-an-event] Select a row, or View details in its menu, to open the event detail page. It shows the event ID, actor, organization, local and UTC time, source, category, target, structured changes and request context. From there you can: - Open affected resource to go to the record, when you have permission to view it. - View related events that share the event's correlation ID. - Reveal restricted details, if you hold the reveal permission. - Copy the event ID or request ID from the row menu. ### Export to CSV [#export-to-csv] Select Exports in the table toolbar, then Create export. The export uses the current filters and needs a date range of no more than 90 days. Exports run in the background: refresh the panel to see Pending, Running, Completed, Failed or Expired. Download a completed export with Download export. Files expire after 24 hours. Organization exports never include restricted details. Requesting, completing and downloading an export are audited. ## Permissions [#permissions] | Key | Grants | | ----------------- | -------------------------------------- | | `auditLog.read` | Open the audit log and event details. | | `auditLog.export` | Create and download CSV exports. | | `auditLog.reveal` | Reveal restricted details on an event. | Opening an affected resource also requires the view permission of that record type, such as `customer.read` or `task.read`. See [Access control](/docs/product/access-control). ## Automation and AI [#automation-and-ai] The assistant side panel can summarize the visible results, explain a visible event and suggest filters. MCP tools for the audit log: | Tool | Kind | | ------------------------------- | ----- | | `listOrganizationAuditEvents` | Read | | `createOrganizationAuditExport` | Write | | `getOrganizationAuditExport` | Read | See [MCP](/docs/developers/mcp). ## API [#api] Paths are relative to `/api/v1/organizations/{organizationId}`. | Resource | Operations | | --- | --- | | `audit-events` | `listOrganizationAuditEvents` (GET) returns a cursor-capable page of redacted event summaries. | | `audit-exports` | `createOrganizationAuditExport` (POST) queues a CSV export of redacted events. | | `audit-exports/{exportId}` | `getOrganizationAuditExport` (GET) returns an export job owned by the caller, with a five-minute download URL while the file is available. | `audit-events` requires `auditLog.read`. Both export routes require `auditLog.export`. Restricted details and sensitive metadata are never returned through the API. See [API](/docs/developers/api) and [OpenAPI](/docs/developers/openapi). ## Related pages [#related-pages] - [Audit logs architecture](/docs/architecture/audit-logs) - [Admin audit log](/docs/admin/audit-log) - [Access control](/docs/product/access-control) - [Billing and usage](/docs/product/billing-and-usage) # Billing and usage (/docs/product/billing-and-usage) How an organization chooses a plan, pays through Stripe Checkout and the Stripe Customer Portal, and tracks metered usage, credits and limits on the Usage and Limits page. ## Overview [#overview] Billing covers the organization's subscription to the platform. Usage covers what the organization consumes during the billing period and how that compares with its plan. Both pages live under Organization, Account in the sidebar: | Route | Purpose | | --- | --- | | `/{orgSlug}/billing` | Billing: current plan, billing details, payment methods and invoice history. | | `/{orgSlug}/billing/checkout/return` | Landing page after Stripe Checkout. | | `/{orgSlug}/billing/manage/return` | Landing page after the Stripe Customer Portal. | | `/{orgSlug}/usage` | Usage and Limits: current-period usage per meter. | This page is about paying for the platform. For invoices that the organization sends to its own customers, see [Invoices](/docs/product/invoices). ## Key concepts [#key-concepts] ### Plans [#plans] | Plan key | Name | Summary | | --- | --- | --- | | `starter` | Starter | Free and limited, for evaluation. Every organization starts here. | | `pro` | Pro | Self-serve plan for small teams that need AI, inbox, API and workflow credits. | | `business` | Business | Higher included credits, unlimited active users and longer audit log retention. | | `enterprise` | Enterprise | Custom, quote-only plan with volume-based credit pricing, dedicated support and onboarding. | Pro and Business are sold as credit packages, billed monthly or yearly. Larger packages include more credits, and unused credits roll over: the page shows the current package credits plus unexpired rollover credits, and the oldest balance is used first. Enterprise is arranged with the platform team and cannot be selected in the app. Plans, packages, prices and limits are managed by platform administrators in [Admin plans](/docs/admin/plans), so the exact numbers can differ per deployment. ### Meters [#meters] Usage is counted per meter and day and summed for the current billing period, which follows the subscription's own period (the calendar month without a subscription). Each record keeps who or what used it, such as the user, the API, MCP, a workflow or the assistant. Billable meters count toward the plan and can produce overage: | Meter | Counts | | --- | --- | | `ai.input_tokens` | Input tokens for AI chat, suggestions, agents, inbox auto-replies and workflows. | | `ai.output_tokens` | Output tokens for the same features. | | `inbox.messages_received` | Inbound messages on connected inbox channels. | | `inbox.messages_sent` | Outbound human and AI replies on connected inbox channels. | | `api.requests` | Organization-scoped REST API requests. | | `mcp.tool_calls` | External MCP tool calls. | | `workflow.steps` | Weighted workflow steps. | | `files.storage_bytes` | File storage, which also sets the Files storage quota. | `workflow.steps` is the billable workflow meter. Actions and AI steps count once, paid registry lookups such as a KvK lookup carry a higher weight, and flow control such as triggers, conditions, delays, stages, waits and approvals is free. Steps that are skipped bill nothing. Lifecycle runs and test runs are not billed. `workflow.runs` is tracked for reporting only. Other meters are tracked without billing: knowledge retrievals, memory operations, file uploads and extractions, media processing, email deliveries, notifications and active users. Active users are still a plan limit. ### Limits [#limits] Each plan sets an included quantity (its quota) and an enforcement mode per meter. A meter without an included quantity on a plan has no limit on that plan: | Mode | Behavior | | --- | --- | | Hard limit | Features that check an allowance, such as AI turns and workflow steps, are blocked once the included quantity is used. Starter uses this mode. | | Included plus overage | Usage above the included quantity is billed as metered overage. Pro and Business use this mode. | | Soft limit | Usage is tracked against a custom or unlimited allowance. Enterprise uses this mode. | AI chat checks the AI allowance before a turn starts, and starting a workflow action checks the step allowance first. The usage counters are the source of truth for the Usage and Limits page; reporting overage to Stripe metered billing is not switched on yet. An hourly workflow emails organization owners when usage crosses 80 percent and 100 percent of an included quantity. ## Manage billing [#manage-billing] ### Choose or change a plan [#choose-or-change-a-plan] Open Billing. The Current plan section shows the active plan, its features and credits, and Choose a plan. Pick a plan, a credit package and Monthly or Yearly, then continue to Stripe Checkout. After payment you land on the checkout return page. The return page is only a hint. Stripe sends the final subscription update through a verified webhook, so the Billing page can show Stripe is finalizing your plan for a short time. Refresh if the change is not visible yet. ### Manage the subscription in Stripe [#manage-the-subscription-in-stripe] Select Manage in Stripe, or Manage payment methods in the Payment methods section, to open the Stripe Customer Portal. Cards and payment methods are managed in Stripe; the app stores only safe display metadata for them. After you leave the portal you land on the manage return page. ### Billing details [#billing-details] The Billing details section holds the company name, billing email, invoice email recipients, address and tax ID with its type, such as EU VAT or Dutch BTW. Saving them updates your customer record at Stripe, which uses them on subscription invoices; the invoice email recipients stay in CentraKit. ### Invoice history [#invoice-history] Invoice history lists the subscription invoices from Stripe with their status. Select a row to download the Stripe invoice PDF. ### Platform fee invoices [#platform-fee-invoices] When customers pay your invoices online, ScaleDock keeps a fixed platform fee per payment. By the 15th of the following month the Platform fee invoices section under Invoice history lists one invoice for the fees of the previous month, with a line per customer invoice and the VAT shown. The fee was already withheld from your Stripe payments, so the invoice is marked as settled and there is nothing to pay. EU organizations with a valid VAT number get reverse charge. When a fee is refunded after it was invoiced, a credit note follows. Both are emailed as a PDF to your invoice recipients and can be downloaded from the list, which needs the billing manage permission. ## Review usage [#review-usage] Open Usage and Limits to see, for each meter, the current-period quantity against the included quantity, credits used and estimated overage. The page also shows the total credit limit, whether auto top-up is enabled and the latest usage records with who or what used each meter. Explain usage asks the assistant side panel to walk through the numbers. ## Permissions [#permissions] | Key | Grants | | --- | --- | | `billing.manage` | Open Billing and Usage and Limits, start checkout, open the Stripe Customer Portal and edit billing details. | Platform administrators manage plans and subscriptions with `platform.billing.read` and `platform.billing.manage`. See [Admin billing](/docs/admin/billing) and [Access control](/docs/product/access-control). ## Automation and AI [#automation-and-ai] The automatic Billing usage threshold workflows publish an event when usage crosses a threshold and email the organization owners. See [Workflows](/docs/product/workflows). Billing has no MCP tools and no organization REST resources. Checkout, the portal and billing details run as server actions in the app. Stripe calls `POST /api/v1/billing/stripe/webhook` to update the local billing state. ## Related pages [#related-pages] - [Stripe billing operations](/docs/operations/stripe-billing) for environment variables, webhook events and go-live checks. - [Admin plans](/docs/admin/plans) for plan and limit configuration. - [Audit log](/docs/product/audit-log) for retention per plan. - [Files](/docs/product/files) for the storage quota. # Chat (/docs/product/chat) How the CentraKit AI assistant works in the Chats page and side panel, including Ask, Plan and Agent modes, models, attachments, commands, approvals, agent runs, sharing, contextual actions, knowledge retrieval and organization AI settings. ## Overview [#overview] Chat is the AI assistant of CentraKit. It answers questions about the organization, plans work and carries out actions with the same permissions and approvals as the person using it. A prompt can never grant access that the user does not already have. The assistant is available in two places: - The **Chats** page, at the top of the organization sidebar, with the chat history beside the conversation. - The **side panel**, which opens from the top bar on any organization page without leaving it. | Route | Purpose | | --- | --- | | `/{orgSlug}/chats` | Chat history and a new chat. | | `/{orgSlug}/chats/{threadId}` | One chat. | | `/{orgSlug}/chats/activity` | Opens [Notifications](/docs/product/notifications) filtered to items that need action, such as AI approvals and proposed artifacts. | | `/{orgSlug}/ai/settings` | Organization AI settings. | | `/portal/chats/{token}` | Read-only shared chat, opened from a share link. | ## Key concepts [#key-concepts] ### Chats [#chats] A chat is a thread of messages. Chats are private to their owner by default. The history groups chats into Pinned and recent periods and can be searched. The actions menu on a chat offers Share, Rename, Download (a transcript), Mute notifications, Pin or Unpin, Archive and Delete. ### Modes [#modes] | Mode | Use it to | | --- | --- | | Ask | Get answers about the organization from its data and knowledge. | | Plan | Get a structured plan. Review the plan before moving into Agent mode. | | Agent | Let the assistant carry out the work with tools, as a durable agent run. | The highest mode an organization can use and the risk level that needs approval are set by platform administrators. See [Admin AI settings](/docs/admin/ai-settings). ### Models [#models] **Auto** chooses the model, reasoning effort and context window for each request. You can also pick one of the enabled models, a reasoning effort, a context window and a speed (Normal or Fast). A chosen model must support the attached files. When it does not, CentraKit asks you to choose Auto or a compatible model instead of switching silently. Each settled answer shows the model that actually produced it. ### Tools and approvals [#tools-and-approvals] The assistant works through tools for customers, assets, tasks, quotes, invoices, orders, purchase orders, promotions, agenda, files, knowledge, inbox, time off, HR approvals, analytics, workflows, integrations, notifications, access, billing and usage, plus web search. Every tool checks the user's permission. Routine reads run without asking and are summarized as gathered context. Changes show an approval card with **Approve** and **Deny**. When several changes are proposed at once, they appear as one checklist, and you can approve all, deny all or approve a subset. Only approved tools run. ### Agent runs [#agent-runs] An Agent mode request runs as an agent run with steps for planning, tasks, tool calls, approvals, checks and handoff. A run can be queued, planning, awaiting approval, running, checking, retrying, succeeded, partially succeeded, failed, canceled, stopped or escalated. If a connection drops, the chat reconnects to the run. **Request human handoff** asks a person to take over and records why. Approvals are always decided by people, never by an agent. ### Contextual actions and artifacts [#contextual-actions-and-artifacts] Many pages offer AI actions for what is on screen, such as summarizing a customer, reviewing a quote before sending, drafting an inbox reply or explaining an analytics report. Drafts are applied to the form as reviewable changes, and nothing is saved until you save the form. Larger results are stored as artifacts, such as plans, reviews, summaries, customer follow-ups and workflow suggestions. An artifact is proposed, applying, applied, rejected or stale when the page it was made for has changed. ## Work with chats [#work-with-chats] ### Start a chat [#start-a-chat] Select **New chat**, or open the side panel. A new chat offers three starter suggestions from your organization context. Type a message and send it. After each answer, the assistant suggests follow-up questions, and **Regenerate suggestions** fetches three new ones. ### Add context and commands [#add-context-and-commands] - Type `@` or `#` to reference users, teams and records such as customers, tasks, quotes and invoices. References appear as chips with a preview on hover. - Type `/web` to search the public web. - Type `/workflow` and pick a published organization workflow to run. ### Attach files and use your voice [#attach-files-and-use-your-voice] Select **Add attachment** to upload files, take a picture or take a screenshot. Images, audio and PDFs are checked against the chosen model. Every upload is also checked for its content first: a file that does not match its type, or an executable or script, is refused. Only people who can read the chat can open its files. Select **Voice input** to dictate a message, which is transcribed into the composer. ### Queue, stop and retry [#queue-stop-and-retry] You can keep typing while the assistant answers. Extra messages wait under Queued messages, where you can edit or delete them. Stop ends the current answer. When a response fails, the notice explains the cause, such as a rate limit, the organization usage limit, an unsupported attachment or an unavailable model, and offers the matching retry or fix. ### Rate an answer [#rate-an-answer] Mark an answer as **Good response** or **Poor response**. Ratings are private to you and store no message text. ### Share a chat [#share-a-chat] Select **Share** in the chat's actions menu: 1. Add organization users as **Participants** to give them access to the chat. 2. Select **Create link** under Read-only links to copy a link that anyone can open at `/portal/chats/{token}` without signing in. 3. Select **Revoke** to end a link at once. A revoked, expired or invalid link shows "Shared chat unavailable". ## Knowledge retrieval [#knowledge-retrieval] The assistant searches the organization's uploaded knowledge, such as manuals, policies and handbooks, and can list and read the knowledge files and their versions. Knowledge search is for documents, not for app records like customers or invoices. Organizations upload knowledge under AI, Knowledge in the sidebar, and platform administrators manage platform knowledge in [Admin knowledge](/docs/admin/knowledge). MCP clients use the same retrieval through the `retrieveKnowledge` tool. ## Organization AI settings [#organization-ai-settings] `/{orgSlug}/ai/settings` controls AI for one organization: | Setting | Meaning | | --- | --- | | Inference region | EU or US pins inference and provider storage to that zone. Global routes for availability and latency with no residency guarantee. Pinning a region can cost more. | | Chat instructions | Optional terminology and tone for Ask, Plan and Agent. They cannot override permissions, approvals, modes, tools or platform safety rules. | | Background actions | Whether AI actions may run in the background. | | Private memory | Whether the assistant keeps private memory per user, and for how many days. | | External integrations | Whether AI actions may use connected integrations. | | Daily credit budget | A daily limit on AI credits for the organization. | | Contextual actions | Every contextual action per page, each with its own on and off switch. | ## Permissions [#permissions] | Permission | Allows | | --- | --- | | `chat.read` | Open Chats and the side panel and read chats you can access. | | `chat.create` | Start chats, send messages and start specialists. | | `chat.update` | Rename, pin and archive chats. | | `chat.delete` | Delete chats. | | `knowledge.read` | Let the assistant search organization knowledge. | | `organization.update` | Edit organization AI settings. | Only the owner of a chat can change, share or delete it and manage its participants. Participants can read and post in the chat. Each tool also needs the permission of the feature it touches. See [Access control](/docs/product/access-control) and [RBAC](/docs/architecture/rbac). ## Automation and AI [#automation-and-ai] - **Research specialist** and **Review specialist** run a focused, read-only investigation or review from fresh context. They are started through the API. - AI actions, approvals and proposed artifacts that need you appear in [Notifications](/docs/product/notifications). - MCP clients can retrieve knowledge with `retrieveKnowledge`. See [MCP](/docs/developers/mcp) and [AI agents](/docs/developers/ai-agents). ## API [#api] The chat transport lives under `/api/v1/ai/`: | Resource | Routes | | --- | --- | | Chat stream | `POST ai/chat`, `GET ai/chat/{threadId}/stream` to resume, `POST ai/chat/{threadId}/stop` | | Threads | `GET ai/chat/threads`, `PATCH` and `DELETE ai/chat/{threadId}` | | Participants and sharing | `ai/chat/{threadId}/participants`, `ai/chat/{threadId}/share` | | Attachments | `POST ai/chat/attachments`, `GET ai/chat/attachments/{attachmentId}` | | Suggestions | `POST ai/chat/starter-suggestions`, `POST ai/chat/follow-ups` | | Voice | `POST ai/transcribe` | | Specialists | `GET` and `POST ai/specialists` | Organization routes live under `/api/v1/organizations/{organizationId}/ai/`: `threads`, `threads/{threadId}/messages`, `activity`, `models`, `agent-runs/{runId}` with `events`, `approvals/{approvalId}` and `handoff`, `contextual-actions/{actionId}` and `contextual-artifacts`. Shared chats are read from `GET /api/v1/public/chats/{token}`, and knowledge from `POST /api/v1/organizations/{organizationId}/knowledge/retrieve`. See [API](/docs/developers/api) and [OpenAPI](/docs/developers/openapi). ## Related pages [#related-pages] - [Notifications](/docs/product/notifications) - [Inbox](/docs/product/inbox) - [Workflows](/docs/product/workflows) - [Admin AI settings](/docs/admin/ai-settings) - [Admin knowledge](/docs/admin/knowledge) # Command center (/docs/product/command-center) How to use the command center to jump to pages, search records, create items, run workflows and start AI prompts from one keyboard-driven dialog. ## Overview [#overview] The command center is a search dialog that works on every page of the app. Open it with Cmd+K on macOS or Ctrl+K on Windows and Linux, or select the search trigger in the sidebar. The dialog is titled "Command center" and its search field reads "Search pages, actions, and more". What the command center offers depends on the navigation mode you are in (Organization, Customer or Admin) and on your permissions. You only see commands and records you are allowed to use. ## Key concepts [#key-concepts] ### Groups [#groups] Results are grouped in this order: | Group | Contents | | --- | --- | | Recent | Up to 12 pages and records you opened from the command center. The list is stored in your browser per navigation mode and organization. | | Create | Shortcuts to create a record. | | Actions | Workflow runs and other actions. | | AI | Prompts that open the assistant with the current page as context. | | Records | Records that match your search text. | | Page groups | Every page in the sidebar, grouped the same way as the sidebar. | ### Create commands [#create-commands] In an organization you can create a customer, task, product, quote, invoice or workflow, as long as you have the matching create permission. Create organization and View all organizations are always available because they live outside a tenant. In Admin mode the command center adds Create workflow, Invite user and Run system workflow for administrators with the matching `platform.*` permission. ### Record search [#record-search] Type at least one character to search records. The search waits briefly after you stop typing and then returns up to 24 results, mixed across record types so one type does not crowd out the others. | Mode | Record types | | --- | --- | | Organization | Customers, tasks, products, customer assets, quotes and invoices. | | Customer | Your quotes, invoices and assets in the [customer portal](/docs/product/customer-portal). | | Admin | Organizations, users, platform invoices and system workflows. | ## Use the command center [#use-the-command-center] ### Jump to a page or record [#jump-to-a-page-or-record] Open the command center, type part of the page title or record name, and press Enter on the result. Use the arrow keys to move between results and Escape to close the dialog. ### Run a workflow [#run-a-workflow] 1. Choose Run workflow in the Actions group. 2. Search for a published workflow with a manual trigger. The list shows up to 20 matches. 3. Confirm in the "Run workflow?" dialog with Run now. CentraKit starts the run and opens its run page. See [Workflows](/docs/product/workflows). ### Ask the assistant [#ask-the-assistant] The AI group has three prompts: Ask AI to summarize this page, Ask AI to draft a follow-up and Ask AI to find related records. Each one opens the side-panel assistant with a draft message and the current page attached as context. The message is not sent until you review it and send it. See [Chat](/docs/product/chat). The AI group only appears when AI is available for the organization and you can start chats. ## Permissions [#permissions] Each command and record type uses the permission of the feature behind it. | Item | Permission | | --- | --- | | Create customer, task, product, quote, invoice or workflow | `customer.create`, `task.create`, `product.create`, `quote.create`, `invoice.create`, `workflow.create` | | Run workflow | `workflow.run` | | AI prompts | `chat.create` | | Record results in an organization | `customer.read`, `task.read`, `product.read`, `asset.read`, `quote.read`, `invoice.read` | | Admin record results | `platform.organization.read`, `platform.user.read`, `platform.billing.read`, `platform.workflow.manage` | | Admin create commands | `platform.workflow.manage`, `platform.user.manage` | ## API [#api] `GET /api/v1/command-center/search` returns the same record results as the dialog. It takes these query parameters: | Parameter | Description | | --------- | ----------------------------------------------------- | | `mode` | `organization`, `customer` or `admin`. | | `query` | Search text, 1 to 160 characters. | | `orgSlug` | Required for the `organization` and `customer` modes. | | `limit` | 1 to 40 results. The default is 24. | Each result has an `id`, `kind`, `label`, `description`, `href` and `organizationId`. The `admin` mode requires `platform.admin.access`. Responses are never cached. See [API](/docs/developers/api) and [OpenAPI](/docs/developers/openapi). ## Related pages [#related-pages] - [Dashboard](/docs/product/dashboard) - [Workflows](/docs/product/workflows) - [Chat](/docs/product/chat) - [Access control](/docs/product/access-control) # Content (/docs/product/content) The /content hub for document and task templates with their lifecycles and automations, product brands, the product category tree, and organization tags. ## Overview [#overview] Content holds the reusable building blocks that other features draw on. It lives under **Resources > Content** at `/{orgSlug}/content` and has four tabs: | Tab | Route | Detail page | What it manages | | --- | --- | --- | --- | | Templates | `/content/templates` | `/content/templates/[templateId]` | Defaults for invoices, quotes, tasks, orders and purchase orders | | Brands | `/content/brands` | `/content/brands/[brandId]` | Product brands and manufacturers | | Tags | `/content/tags` | `/content/tags/[tagKind]/[tagSlug]` | Reusable labels for tasks, customers and products | | Categories | `/content/categories` | `/content/categories/[categorySlug]` | The product category tree | Each tab has a create page at `/content/<tab>/create`. Opening `/content` redirects to the first tab you have permission to view, and tabs you cannot view are hidden. ## Templates [#templates] ### Key concepts [#key-concepts] A template has a type, a language and optional default status for its type. | Type | Used for | | --- | --- | | Invoice | New invoices, with default due days and selectable standard costs | | Quote | New quotes, with default validity, closing text, email message, prefilled lines, and the option to create an order when accepted | | Task | New tasks, seeding title, description, status, priority, assignees, tags, an optional customer and draft subtasks | | Order | New orders, with default delivery and invoicing settings | | Purchase order | New purchase orders, with purchase terms, a default receive location and the email to the supplier | The language is English, Dutch or German. Quote and invoice templates also carry the title, introduction, closing, footer, currency, VAT mode, VAT regime, discount and terms and conditions as a URL, rich text or attached PDF. The default template of a type is used for new documents of that type. You cannot delete the last template of a type, and deleting a template does not affect documents already created from it. A new organization starts with a default invoice, a deposit invoice, a default quote, a repair estimate and a service visit task template. The invoice and quote templates are written in the language of the organization's customer emails, which follows its operating country or, without one, its address country (see [Settings and branding](/docs/product/settings-and-branding)). When that country changes later, invoice and quote templates whose texts are still the starter texts move to the new language. A template whose title, introduction, closing, email message or footer you edited keeps all its texts. The public product page and the product PDF use a fixed layout and do not read a template. ### Create and edit a template [#create-and-edit-a-template] 1. Open **Content > Templates** and select **New template**, or open **More template types** to pick a type. 2. Fill in the **Template identity** section: name, description, language, and whether it is the default for its type. 3. Edit the remaining sections. They are the same sections as the live task, quote or invoice page, without the parts that only make sense on a real record, such as the customer picker, send actions, absolute dates, and task comments, time and materials. 4. Save. To use a template, choose **From template** in the New split button on the quote, invoice or task lists. See [Quotes](/docs/product/quotes), [Invoices](/docs/product/invoices) and [Tasks](/docs/product/tasks). ### Workflow tab [#workflow-tab] Every template has a **Workflow** tab at `/content/templates/[templateId]/workflow` with two sections. **Lifecycle** sets the stages and automatic steps that records created from the template go through. The template follows the organization default lifecycle, or the platform default when the organization has none, unless you pick another one. Select **Customize** to make an organization copy of the lifecycle before you change it. For invoice templates, the **Payment reminders** editor shows the reminder ladder as a visual cadence. Each reminder is set in days relative to the due date, from -365 to 365, so a reminder can fire before the due date. A lifecycle can have up to 20 reminders, each on a unique day, with an optional custom email subject and message. Saving reminders publishes the lifecycle, including any unpublished changes made in the workflow editor. **Automations** react to what happens to records from the template, while the lifecycle stays in charge of the status. The section lists: - Automations attached to this template. **Detach** removes one. - Automations that run for every template. Switch one off to skip it for this template. - **Attach existing** to attach a published automation, **Add from starter** to create one from a starter, and **New automation** to open the workflow editor. | Starter | Template type | What it does | | --- | --- | --- | | Big-ticket quote | Quote | Tells the team when a quote over 10,000 goes out | | Viewed, not accepted | Quote | Reminds the team to follow up when a quote was viewed but not decided in 3 days | | B2C 14-day letter | Invoice | Sends individual customers the 14-day letter once an invoice is overdue | | Payment failed | Invoice | Flags the invoice and alerts the team when an online payment fails | | Low stock reorder | Purchase order | Drafts a purchase order when stock runs low and sends it to the supplier | | Purchase order overdue | Purchase order | Reminds the supplier and tells the purchaser when a purchase order is late | | Back in stock | Order | Fills backorders when stock arrives and tells customers their order is complete | | Prepaid order to picking | Order | Releases a prepaid order to picking as soon as it is paid | | Ready for pickup | Order | Messages the customer when an order is ready for pickup, and reminds them after 7 days | | Delivered, then invoice | Order | Invoices what was delivered and sends it, for orders invoiced on fulfillment | Starters triggered by stock events, such as Low stock reorder and Back in stock, run for all products or all orders instead of being attached to one template. See [Workflows](/docs/product/workflows) for editing automations. ## Brands [#brands] A brand has a name, slug, website, description, a logo and gallery images, and tags. Products link to one brand. Create one with **New brand**, or create it inline from a product's brand picker. Deleting a brand removes its images and tag assignments. Products that used it keep their details but lose the brand link. ## Categories [#categories] Categories form a tree that organizes products. A category has a name, slug, icon, description and an optional parent. - Select **New category** for a top-level category, or **New subcategory** on a category to nest one under it. - Reorder categories, or use **Move category** to change the parent. - Products can belong to several categories. A parent category does not automatically include its subcategories' products. Deleting a category removes it from products, and its subcategories move to the top level. ## Tags [#tags] Tags are reusable labels with a name, color and description. Each tag has a kind: | Kind | Applied to | | -------- | ---------- | | Task | Tasks | | Customer | Customers | | Product | Products | Brands can carry tags as well. Create tags with **New tag**, or inline from a tag picker. A tag opens at `/content/tags/[tagKind]/[tagSlug]`. Deleting a tag removes all of its assignments. ## Permissions [#permissions] | Key | Grants | | --- | --- | | `template.read`, `.create`, `.update`, `.delete` | Manage templates | | `brand.read`, `.create`, `.update`, `.delete` | Manage brands and brand images | | `category.read`, `.create`, `.update`, `.delete` | Manage categories | | `tag.read`, `.create`, `.update`, `.delete` | Manage tags | On the Workflow tab, customizing a lifecycle or adding a starter automation also needs `workflow.create`, and editing reminders needs `workflow.update`. See [Access control](/docs/product/access-control). ## Automation and AI [#automation-and-ai] - The assistant can read and change templates, create and edit brands and categories, and create, update, delete and assign tags. See [Chat](/docs/product/chat). - MCP tools: `listTags`, `getTag`, `createTag` and `updateTag`. See [MCP](/docs/developers/mcp). Not yet available: MCP tools for templates, brands and categories. ## API [#api] All paths are under `/api/v1/organizations/{organizationId}`. | Resource | Operations | | --- | --- | | `/templates` | [List](/docs/openapi/listTemplates), [create](/docs/openapi/createTemplate), [get](/docs/openapi/getTemplate), [update](/docs/openapi/updateTemplate), [delete](/docs/openapi/deleteTemplate) | | `/brands` | [List](/docs/openapi/listBrands), [create](/docs/openapi/createBrand), [get](/docs/openapi/getBrand), [update](/docs/openapi/updateBrand), [delete](/docs/openapi/deleteBrand) | | `/product-categories` | [List](/docs/openapi/listProductCategories), [create](/docs/openapi/createProductCategory), [get](/docs/openapi/getProductCategory), [update](/docs/openapi/updateProductCategory), [delete](/docs/openapi/deleteProductCategory), [move](/docs/openapi/moveProductCategory) | | `/tags` | [List](/docs/openapi/listTags), [create](/docs/openapi/createTag), [get](/docs/openapi/getTag), [update](/docs/openapi/updateTag), [delete](/docs/openapi/deleteTag) | ## Related pages [#related-pages] - [Products](/docs/product/products) - [Quotes](/docs/product/quotes), [Invoices](/docs/product/invoices) and [Orders](/docs/product/orders) - [Tasks](/docs/product/tasks) and [Customers](/docs/product/customers) - [Workflows](/docs/product/workflows) and [Workflow operations](/docs/operations/workflows) - [Access control](/docs/product/access-control) # Customer portal (/docs/product/customer-portal) What customers see in the customer portal, how they accept, reject or request changes to quotes, pay invoices, follow orders and view assets, and which portal pages and token links work without signing in. ## Overview [#overview] The customer portal is where an organization's customers follow their business with it. It lives at `/{orgSlug}/portal` and shows the quotes, invoices, orders and assets of the customer that the signed-in contact belongs to. | Route | Purpose | | --- | --- | | `/{orgSlug}/portal` | Portal dashboard. | | `/{orgSlug}/portal/quotes` and `/{orgSlug}/portal/quotes/{quoteId}` | Quotes and quote detail. | | `/{orgSlug}/portal/invoices` and `/{orgSlug}/portal/invoices/{invoiceId}` | Invoices and invoice detail with payment. | | `/{orgSlug}/portal/orders` and `/{orgSlug}/portal/orders/{orderId}` | Orders and order progress. | | `/{orgSlug}/portal/assets` and `/{orgSlug}/portal/assets/{customerAssetId}` | Installed assets. | | `/{orgSlug}/portal/products/{productId}` | Public product page. | | `/{orgSlug}/portal/careers` and `/{orgSlug}/portal/careers/{jobId}` | Public careers page with open positions. | | `/{orgSlug}/portal/about` | Public business page with the organization's name, address, KVK number, VAT number, contact details and website. Available once the organization has activated online payments. | The portal sidebar has a My group with Dashboard and Assets, and a Finance group with Quotes, Invoices and Orders. The product, careers and business pages are reached through shared links. ## Key concepts [#key-concepts] ### Who can open the portal [#who-can-open-the-portal] - Portal-only users are customer contacts whose contact record is linked to a CentraKit account. When they open `/{orgSlug}`, CentraKit sends them to the portal. The organizations overview shows their role as Customer. - Users of the organization can open the portal from the organization switcher: point at the organization and choose Portal, or Organization to go back. Their lists stay empty unless they are also a customer contact. Not yet available: inviting a customer contact to the portal from the app. ### What customers see [#what-customers-see] Customers only see records of the customers they are a contact for: - Quotes and invoices that have been sent. Drafts stay internal. - Orders, with their deliveries, invoices and downloads. - Assets, except archived ones. Internal notes, purchase prices, CRM notes and files stay internal. The portal uses the organization's logo, colors and social links from [settings](/docs/product/settings-and-branding). ### Public links [#public-links] Some portal pages work without signing in, so the organization can share them by email or link: | Link | What it shows | | --- | --- | | `/{orgSlug}/portal/quotes/{quoteId}` | A sent quote, with its PDF and terms. The customer can respond without an account. | | `/{orgSlug}/portal/invoices/{invoiceId}` | A sent invoice, with its PDF and payment. | | `/{orgSlug}/portal/products/{productId}` | A product page. | | `/{orgSlug}/portal/careers` | Open positions and the application form. | | `/{orgSlug}/portal/about` | The organization's name, address, KVK number, VAT number, contact details and website. | | `/portal/chats/{token}` | A read-only shared chat. | | `/portal/onboarding/{token}` | A pre-boarding form where a new hire fills in private details and uploads requested documents. See [People](/docs/product/people). | | `/portal/calendars/{token}` | An iCalendar feed to subscribe to. See [Agenda](/docs/product/agenda). | | `/portal/files/{token}` | A shared file download. See [Files](/docs/product/files). | Token links stop working when they expire or are revoked. ## Use the portal [#use-the-portal] ### Dashboard [#dashboard] The dashboard shows metric cards for open quotes awaiting review, unpaid invoices with the overdue count, the outstanding balance and installed assets with warranties ending soon. Needs your attention lists quotes to review and invoices to pay. Quick links, Recent assets and Recent activity complete the page. When nothing is open, it reads "You're all caught up". ### Respond to a quote [#respond-to-a-quote] Open a quote to read it and download the PDF. The Quote response section has three choices: - Accept: enter your name and email, choose any optional lines, and confirm. When the quote requires a deposit, the button reads Accept and pay deposit, and a deposit invoice is created for you to pay. - Reject: enter your name, email and a reason. - Request changes: describe what should change. The quote moves to Changes requested, its reminders stop, and the team is notified. You receive a revised quote. If the organization publishes a newer version while you are reading, the page tells you that this version is no longer valid. The team sees when you viewed the quote. See [Quotes](/docs/product/quotes). ### Pay an invoice [#pay-an-invoice] Open an invoice from Invoices or the dashboard. When the invoice can be paid online, the page shows a card and payment form powered by Stripe. Online payment is available for invoices in euros. After a bank payment such as SEPA Direct Debit, the page shows that the payment is being processed until the bank confirms it, and a failed payment shows an error so you can try again. The invoice list can be filtered by Sent, Paid, Overdue and Cancelled. See [Invoices](/docs/product/invoices). ### Follow an order [#follow-an-order] An order shows its progress (Ordered, Ready for pickup or Shipped, Completed), the items and totals, deliveries with Track shipment links, its invoices with amounts to pay and PDF downloads, and any downloads and license keys. See [Orders](/docs/product/orders). ### View assets [#view-assets] The Assets page lists the equipment installed at your customer. An asset shows its serial number, asset tag, condition, installed date, warranty details, service interval, location and description. ### Apply for a job [#apply-for-a-job] The careers page lists open positions. Open one and fill in your name, email, phone, LinkedIn profile, CV (PDF or image, up to 10 MB) and motivation, agree to the processing of your details, and select Send application. You can also agree to have your details kept for one year. See [Hiring](/docs/product/hiring). ## Permissions [#permissions] Portal access does not use roles. A contact sees the portal of each organization where they are linked to a customer, and only that customer's records. Staff permissions decide what an organization publishes: for example `quote.send` to send a quote and `invoice.send` to send an invoice. ## Automation and AI [#automation-and-ai] Customer responses start workflows. Accepting, rejecting or requesting changes to a quote raises the matching quote events, cancels the quote's pending reminder runs and notifies the team. See [Workflows](/docs/product/workflows) and [Notifications](/docs/product/notifications). ## API [#api] `POST /api/v1/public/portal/{orgSlug}/quotes/{quoteId}/request-changes` records a change request without signing in. The quote must be sent and still open. See [API](/docs/developers/api) and [OpenAPI](/docs/developers/openapi). To try the portal locally, sign in with the seeded portal-only logins `test3@example.com` (a private customer of two organizations), `test4@example.com` (a business contact) or `test5@example.com` (a stable boarder). See [Local development](/docs/getting-started/local-development). ## Related pages [#related-pages] - [Customers](/docs/product/customers) - [Quotes](/docs/product/quotes) - [Invoices](/docs/product/invoices) - [Orders](/docs/product/orders) - [Hiring](/docs/product/hiring) # Customers (/docs/product/customers) How to manage customers, their contacts, locations, installed assets, notes and files, and how KvK and VIES lookups and @ and ## Overview [#overview] Customers are the businesses and people an organization works for. A customer record holds business details, contacts, locations, the assets installed at the customer, internal notes and files. Quotes, invoices, orders, tasks and conversations all link back to it. Customers live under Workspace in the sidebar. | Route | Purpose | | --- | --- | | `/{orgSlug}/customers` | Customers list, with the Overview, All contacts and All assets tabs. | | `/{orgSlug}/customers/create` | Create a customer. | | `/{orgSlug}/customers/{customerId}` | Customer detail, with the Overview, Contacts, Notes, Locations, Assets and Files tabs. | | `/{orgSlug}/customers/{customerId}/contacts/{contactId}` | Contact detail. | | `/{orgSlug}/customers/{customerId}/locations/{locationId}` | Location detail. | | `/{orgSlug}/customers/{customerId}/assets/{customerAssetId}` | Asset detail, with the Overview and Tasks tabs. | ## Key concepts [#key-concepts] ### Customer [#customer] A customer is either a Business or an Individual. Its status is Prospect, Active or Archived. | Section | Fields | | --- | --- | | Overview | Status, description, tags and the users it is assigned to. | | Primary contact | First name, last name, job title, email and phone. | | Business | Company name, billing name, KVK, VAT or tax ID, billing email, website and logo. | | Address | Location name, address lines, city, postal code, state or province and country. | | Properties | Created and updated details, and the archive date and reason. | A customer needs at least a primary contact name or email. Invoices are sent to the billing email, or to the contact email for individuals, so the form warns when it is missing. ### Contacts [#contacts] A contact is a person at the customer, with a photo, name, job title, email and phone. One contact is the primary contact. The same person can be a contact for several customers. A contact can also sign in to the [customer portal](/docs/product/customer-portal) when their contact record is linked to a CentraKit account. ### Locations [#locations] A location is an address of the customer, such as a head office or a site. One location is primary. Assets and work can be tied to a location. ### Customer assets [#customer-assets] A customer asset is one installed unit of a [product](/docs/product/products) at the customer, such as a tractor or a bike. It records: - Serial number, asset tag and barcode. - Location, installed date, purchase date, purchase price and currency. - Warranty end date, provider and reference, and a service interval in days. - Condition: new, good, fair, poor or unknown. - Status: active, inactive, maintenance, retired or archived. - Tags, custom attributes, files and notes, and the quote or invoice it came from. The Tasks tab on an asset lists the work done on it. Customers see their active assets in the portal. ### Notes [#notes] The Notes tab is a timeline of internal notes from your team. Notes support rich text, `@` mentions of users and teams, and `#` references to records such as tasks, customers, quotes and invoices. Mentioned users are notified. Customers never see notes. ### Files [#files] The Files tab shows the customer's folder in organization [Files](/docs/product/files). Every customer gets its own folder in the customers drive. Files stay internal and are not shown in the portal. ## Work with customers [#work-with-customers] ### Create a customer [#create-a-customer] 1. Open Customers and select New customer. 2. Choose Business or Individual. 3. Fill in the primary contact, business details and address. 4. Select Save. ### Retrieve business details [#retrieve-business-details] In the Business or Address section of the customer form, select Retrieve information. CentraKit searches the KvK (Dutch Chamber of Commerce) register, reads the KvK profile, checks the VAT ID in VIES and searches the web, then combines what it found. KvK and VIES are treated as authoritative. Only empty fields are filled, and nothing is saved until you review the form and save it. The result lists its sources. If nothing new is found, the form tells you that filled fields were left as they are. When you leave the KVK or VAT field, CentraKit also checks the number and shows Verified or Not found. The check never blocks saving. ### Add contacts, locations and assets [#add-contacts-locations-and-assets] Use the Contacts, Locations and Assets tabs on the customer. Each tab has a create action, and the row actions let you make a contact or location primary. The All contacts and All assets tabs on the list page show these records across all customers. ### Archive or delete a customer [#archive-or-delete-a-customer] Open the customer and use the Danger zone. Archive customer asks for a reason and hides the customer from active lists. Unarchive brings it back. Delete customer removes it permanently. You can also archive, unarchive and delete from the list, one at a time or in bulk. ### Find customers [#find-customers] Search the list and filter by status, type, owner and tags. Unassigned shows customers without an owner. Select several rows to change status, owner or tags together. ## Permissions [#permissions] | Permission | Allows | | --- | --- | | `customer.read` | See customers, contacts and locations. | | `customer.create` | Create customers. | | `customer.update` | Edit customers, contacts and locations. | | `customer.delete` | Archive and delete customers. | | `asset.read`, `.create`, `.update`, `.delete` | Work with customer assets. | | `comment.read` | See the notes on customers and assets. | | `comment.create` | Add notes, and edit or delete their own. | | `comment.moderate` | Delete other members' notes. | Notes can be edited only by their author. A deleted note disappears from the thread. The Files tab also needs permission to view organization Files. ## Automation and AI [#automation-and-ai] - Workflows can start when a customer is created or when a lookup is requested. Steps include Send customer welcome email, Search KvK, KvK profile, VIES check and web search. See [Workflows](/docs/product/workflows). - The assistant can list, read, summarize, create and update customers, contacts, locations, notes and assets. Changes ask for approval based on their risk. See [Chat](/docs/product/chat). - MCP clients can list, read, create and update customers and list and read customer assets. See [MCP](/docs/developers/mcp). ## API [#api] All routes live under `/api/v1/organizations/{organizationId}/`: | Resource | Routes | | --- | --- | | Customers | `customers`, `customers/{customerId}` | | Contacts | `customers/{customerId}/contacts`, `.../contacts/{contactId}`, `.../contacts/{contactId}/primary` | | Locations | `customers/{customerId}/locations`, `.../locations/{locationId}`, `.../locations/{locationId}/primary` | | Notes | `customers/{customerId}/notes`, `.../notes/{noteId}` | | Customer assets | `customer-assets`, `customer-assets/{customerAssetId}`, `.../notes`, `.../notes/{noteId}` | | Mentions | `GET collaboration/references` searches records for `#` references. | See [API](/docs/developers/api) and [OpenAPI](/docs/developers/openapi). ## Related pages [#related-pages] - [Customer portal](/docs/product/customer-portal) - [Quotes](/docs/product/quotes) - [Invoices](/docs/product/invoices) - [Products](/docs/product/products) - [Tasks](/docs/product/tasks) - [Files](/docs/product/files) # Dashboard (/docs/product/dashboard) What the organization dashboard shows, including metric cards, the operations pulse, capacity allocation, recent activity and the people reminders for absences, celebrations and HR deadlines. ## Overview [#overview] The dashboard is the home page of an organization. It lives at `/{orgSlug}` and is the first item in the sidebar. It gives staff one view of open work, conversations, sales, billing and the latest activity that involves them. Users who reach the organization only as a customer contact are redirected to the [customer portal](/docs/product/customer-portal) at `/{orgSlug}/portal` instead. If the dashboard data cannot be loaded, the page shows "Dashboard is unavailable" with a retry action. Other pages keep working. ## Key concepts [#key-concepts] ### Metric cards [#metric-cards] The top row shows four metric cards. Each card links to the hub where you act on the numbers. | Card | Metric | Secondary line | Opens | | --- | --- | --- | --- | | Work | Open tasks that need attention | Tasks due within three days | `/{orgSlug}/tasks/all` | | Inbox | Active external conversation threads with unread messages | Threads updated today | `/{orgSlug}/inbox` | | Sales | Open quote value of draft and sent quotes | Number of active quotes | `/{orgSlug}/quotes/all` | | Billing | Open invoices | Number of overdue invoices | `/{orgSlug}/invoices/all` | The numbers are computed from a sample of up to 20 recent records per area, so they describe current activity rather than full historical totals. Use [Insights](/docs/product/insights) for complete reporting. ### Operations pulse [#operations-pulse] The Operations pulse card summarizes how work is flowing: - Tasks done: completed tasks in the sample. - Quote win rate: accepted quotes divided by accepted, rejected and expired quotes. - Billing risk: the total of overdue invoices. - Pipeline progress: the share of top-level tasks that are done and invoices that are paid. - Recent signals: closed task work, accepted quotes and invoices that need follow-up. ### Capacity allocation [#capacity-allocation] The Capacity allocation card has Today, Week and Month tabs. It shows the exposure for the selected period and a flow chart that splits attention between Tasks, Messages, Quotes, Invoices and Customers. Select a segment to open that hub. ### Recent activity [#recent-activity] Recent activity is a timeline of the latest ten items from your [notifications](/docs/product/notifications), such as mentions, assignments, inbound messages, invitations, running AI work, approvals that need a decision and failures. Items that need a reply or a review carry a Reply or Review badge. When nothing has happened yet, the card shows "No recent activity". ### People reminders [#people-reminders] When the organization uses [People](/docs/product/people), the dashboard adds side cards that only appear when they have content: - Who's out: approved time off in the next seven days, with a link to the team calendar. - Celebrations: birthdays and work anniversaries in the next two weeks. Employees choose whether theirs are shown on their [profile](/docs/product/profile). - HR reminders: documents and certificates that are about to expire and contracts that are ending. ## Use the dashboard [#use-the-dashboard] ### Go to the work behind a number [#go-to-the-work-behind-a-number] Select a metric card, a capacity segment or an activity item to open the matching list or record. The dashboard does not edit records itself. ### Ask the assistant about the dashboard [#ask-the-assistant-about-the-dashboard] The dashboard shares its metrics, the operations summary and recent activity with the side-panel assistant as page context. Open the assistant and ask, for example, which overdue invoices need follow-up. See [Chat](/docs/product/chat). ## Permissions [#permissions] Any user of the organization can open the dashboard. Each section only counts records the user is allowed to see, so a user without `quote.read` sees no quote figures. | Section | Permission | | ----------------------------- | -------------- | | Who's out | `timeOff.read` | | Celebrations and HR reminders | `person.read` | ## Related pages [#related-pages] - [Notifications](/docs/product/notifications) - [Command center](/docs/product/command-center) - [Tasks](/docs/product/tasks) - [Inbox](/docs/product/inbox) - [Quotes](/docs/product/quotes) - [Invoices](/docs/product/invoices) - [Insights](/docs/product/insights) # Expenses (/docs/product/expenses) How to create expense claims from receipts, mileage and per diem, group them in trips, check them against policies, approve and reimburse them, and rebill billable expenses to customers. ## Overview [#overview] Expenses handles the costs employees make for work. An employee snaps or forwards a receipt, the details fill in automatically, and the claim goes to their manager for approval. Finance then marks approved claims as reimbursed, and billable expenses become invoice suggestions for the customer. Expenses lives under Workspace, Finance, Expenses in the sidebar. | Route | Purpose | | --- | --- | | `/{orgSlug}/expenses` | My claims tab with your own claims and your receipt forwarding address. | | `/{orgSlug}/expenses/approvals` | To approve tab: claims waiting for your decision. | | `/{orgSlug}/expenses/all` | All tab: every claim you can see. | | `/{orgSlug}/expenses/reimburse` | To reimburse tab: approved out-of-pocket claims that still need paying back. | | `/{orgSlug}/expenses/trips` | Trips tab: claims grouped per business trip. | | `/{orgSlug}/expenses/new` | Create a claim. | | `/{orgSlug}/expenses/{expenseId}` | One claim, with its receipt, flags and approval. | You need an employee profile in [People](/docs/product/people) to create claims. ## Key concepts [#key-concepts] ### Claims [#claims] A claim has a date, merchant, category, total including VAT, currency, VAT amount and rate, business purpose, an optional trip and a receipt. Claims in another currency record the exchange rate to the organization currency. | Type | Meaning | | --- | --- | | Paid myself | The employee paid and is paid back. | | Company card | Paid with a company card, so nothing is reimbursed. | | Mileage | Distance in km times the mileage rate per km from HR settings. | | Per diem | Days times the per diem rate per day from HR settings. | A claim is Draft, Awaiting approval, Approved, Rejected or Reimbursed. ### Categories [#categories] Expense categories, such as Travel, Meals or Software, define what people can claim for. Each has a default VAT rate and an optional ledger code that travels with exports. Receipt reading picks the closest category. ### Policies and flags [#policies-and-flags] An expense policy sets a limit per expense, per day or per trip for one category or all categories, and a receipt requirement above an amount. A policy either warns or blocks submission, and can be paused. When a claim breaks a rule it gets a flag that the approver sees with the reason: | Flag | Meaning | | --- | --- | | Over the policy limit | The amount is above a policy limit. | | Receipt missing | A receipt is required but not attached. | | Possible duplicate | A claim with the same details already exists. Duplicates are checked even without policies. | | Overlaps a per diem | The claim falls on days already covered by a per diem. | ### Trips [#trips] A trip groups the claims of one business trip, with a name, destination and dates, so you see what a visit cost in one place. Deleting a trip keeps its claims. ## Work with claims [#work-with-claims] ### Create a claim from a receipt [#create-a-claim-from-a-receipt] 1. Select **New claim**. 2. Drop a receipt (image or PDF, up to 9.5 MB) or select **Take a picture**. The merchant, date, amounts, VAT and category fill in from the receipt. Check them before saving. 3. Add the business purpose and, if it applies, a trip. 4. Select **Save draft** to finish later, or **Submit** to send it for approval. If the receipt cannot be read, fill in the details yourself. Every receipt is checked before anyone else can open it: the file must match its type (an image or a PDF), and executables are blocked. The check runs within a minute of the upload; until then only the person who attached the receipt can open it, and a blocked receipt cannot be opened at all. ### Forward receipts by email [#forward-receipts-by-email] The My claims tab shows your personal receipt forwarding address when receipts by email is on in HR settings. Forward receipts from your own email address, and each attachment becomes a draft claim with the details filled in. Creating a new address stops the old one from working. ### Claim mileage or per diem [#claim-mileage-or-per-diem] Choose **Mileage** as the type, enter the From and To addresses and select **Calculate distance**. Turn on **Round trip** to count both ways. When the route cannot be calculated, enter the one-way distance in km yourself. For **Per diem**, enter the number of days. The amount follows the rates in HR settings. ### Bill an expense to a customer [#bill-an-expense-to-a-customer] In the Customer billing section, choose the customer and optionally the task, and turn on **Bill this expense to the customer**. Once approved, the expense shows up as an invoice suggestion. See [Invoices](/docs/product/invoices). ### Withdraw or delete a claim [#withdraw-or-delete-a-claim] A submitted claim can be withdrawn, which moves it back to draft. Deleting a claim removes it and its receipt permanently. ## Approve and reimburse [#approve-and-reimburse] ### Decide on claims [#decide-on-claims] Claims go to the employee's manager, or to people who can approve expenses. The To approve tab lists claims waiting for you. Select several to approve or reject them at once, or open a claim to review its flags and add a note to the employee. Claims below the automatic approval amount in HR settings are approved as soon as they are submitted. ### Mark claims reimbursed [#mark-claims-reimbursed] The To reimburse tab lists approved out-of-pocket claims. After the money has been paid back, select the claims and mark them reimbursed, with an optional payment reference. ## Set up expenses [#set-up-expenses] Expense categories, expense policies, the mileage and per diem rates, the automatic approval amount and receipts by email are set in [HR settings](/docs/product/people) at `/{orgSlug}/people/settings`. ## Permissions [#permissions] | Permission | Allows | | --- | --- | | `expense.read` | See all claims in the organization. | | `expense.create`, `.update`, `.delete` | Create, edit and delete claims and trips for other employees. | | `expense.approve` | Decide claims for employees without a manager. | | `expense.reimburse` | See the To reimburse tab and mark claims reimbursed. | | `expense.managePolicies` | Manage expense categories and policies. | | `attachment.manage` | Open and delete receipts other people attached. | Every employee can create, edit and delete their own claims and trips, and managers see their reports' claims. ## Automation and AI [#automation-and-ai] - Receipt reading uses AI to fill in the claim from a photo, PDF or forwarded email. - Mileage uses route distance between two addresses. - Workforce insights flag expense outliers. See [People](/docs/product/people). - The assistant lists and decides expense claims waiting for your approval together with leave and timesheets. See [Time](/docs/product/time) and [Chat](/docs/product/chat). ## API [#api] All routes live under `/api/v1/organizations/{organizationId}/`: | Resource | Routes | | --- | --- | | Expense claims | `GET expenses` lists claims for a scope (your own, all, to approve or to reimburse), `POST expenses` creates a draft claim, mileage entry or per diem. | | HR approvals | `GET` and `POST approvals` | The API accepts JSON only, so upload receipts in the app. See [API](/docs/developers/api) and [OpenAPI](/docs/developers/openapi). ## Related pages [#related-pages] - [People](/docs/product/people) - [Time](/docs/product/time) - [Invoices](/docs/product/invoices) - [Approvals](/docs/product/approvals) # Files (/docs/product/files) How to store, share and download organization files in drives and folders, with uploads, versions, comments, share links, ZIP downloads and the Files API. ## Overview [#overview] Files is the organization's document store. Files and folders live in drives, each drive has its own access list, and every file keeps immutable versions, comments and an activity history. Files lives under Workspace, Resources in the sidebar. The routes are: | Route | Purpose | | --- | --- | | `/{orgSlug}/files` | The file browser. The query string selects the drive, scope, folder, view and filters. | | `/{orgSlug}/files/drives/{driveId}` | Drive settings. | | `/{orgSlug}/customers/{customerId}/files` | A customer's folder, embedded in the customer detail page. | | `/portal/files/{token}` | Public share link for one file. | ## Key concepts [#key-concepts] ### Drives [#drives] The left rail groups drives under File locations: | Drive | Rail label | Purpose | | --- | --- | --- | | Company | Organization | The organization-wide drive, created with the organization. | | Customers | Customers | One folder per customer, created automatically. Files go inside a customer folder, not at the drive root. | | Personal | My | Each user's own drive. | | Shared | Shared drives | Drives that users create for a team or project with New shared drive. | Items can be moved within a drive but not between drives. Copy items to put them in another drive. A drive is Active, Archived or Trashed. Archiving hides a drive and its files until it is restored. When a user is removed, their personal drive is either transferred to another user or marked as having no owner. ### Access roles [#access-roles] Access is granted to users and teams on a drive or on an item, and inherits to everything inside a folder. | Role | Can | | --------- | -------------------------------------------------------------- | | Viewer | Open, preview and download. | | Commenter | Everything a Viewer can, plus comment. | | Editor | Everything a Commenter can, plus change the files and folders. | A grant can have an end date. From the day after that date the grant gives no access; it stays in the share list, marked as expired, until someone changes or removes it. These roles apply on top of the organization permissions listed below. ### Shortcuts and filters [#shortcuts-and-filters] File shortcuts in the rail are Shared with me, Recent, Starred and Bin. The browser has list and grid views, sorting, search, and filters for files, folders and owner. The rail also shows storage used against the plan limit, split into Videos, Audio, Photos, Documents, Other and Bin. The total counts every drive, including the bin and older versions. ### Versions, comments and the bin [#versions-comments-and-the-bin] Selecting a file opens the inspector with three tabs: - Info shows type, size, owner, location and dates. - Comments holds the discussion on the file. - Versions lists every uploaded version. Upload version adds a new one, and you can download or restore an older one. Versions are immutable. Move to bin removes items without deleting them. From Bin you can restore items, delete them permanently, or use Restore all and Delete all. Items in the bin and older versions are kept for a plan-based period: 7 days on Starter, 30 on Pro, 90 on Business and 365 on Enterprise, unless an Enterprise override is set. ## Work with Files [#work-with-files] ### Upload files [#upload-files] Drop files onto the browser, or select Upload files. Uploads go straight to storage in resumable chunks and show progress in the Uploads panel. Each upload first reserves storage quota, and finalizing checks the byte size and file signature before the file appears. Uploads that are never finished expire after 15 minutes and are cleaned up by a background job. ### Organize items [#organize-items] Create folders with New folder, then rename, move, duplicate or star items from the row menu. Select several items to move them to the bin, restore, delete permanently or star them in one step. ### Share a file [#share-a-file] Select Share on an item to grant people and teams Viewer, Commenter or Editor access. Each person or team in the list has its own role and an optional end date; the list shows "Until" with the date, or "Expired on" once it has passed. Choose Remove end date in the date picker to make a grant permanent again, then select Save sharing. For a file you can also create a share link under Share links. Anyone with the link can download the file from `/portal/files/{token}`, which redirects to a short-lived signed URL. Revoke link turns the link off. Turn on Restricted in the same dialog, then select Save sharing, to stop the shares on the folders above an item from reaching it. A restricted item and everything inside it are then open only to people who can open the drive (its owner, the people and teams the drive is shared with, and everyone with access to the Company and Customers drives) and to people with a grant on the item itself. A grant on a file or folder inside it still reaches that file or folder. Turn Restricted off to let the folder shares reach it again. Only people who keep access to the item after the change can restrict it, so a share on a folder above it is not enough. Not yet available: share links for folders. Share folders with people or teams instead. ### Download as ZIP [#download-as-zip] Select several items and choose Download ZIP. Selections of up to 100 items and 25 MB stream immediately. Larger selections are queued, built in the background, and offered as a download that expires after one hour. ### Manage a drive [#manage-a-drive] Open a drive's menu and choose Drive settings. The settings page has these sections: - General: rename the drive. - Access: grant users and teams a role and set the default role for new grants. - Owner: transfer the drive to another organization user. - Lifecycle: archive or restore the drive, or permanently delete it with its files. ## Permissions [#permissions] | Key | Grants | | --- | --- | | `file.read` | Browse drives, preview and download files, star items, comment, and see storage and activity. | | `file.create` | Create shared drives and folders, upload files, copy items and request ZIP downloads. | | `file.update` | Rename, move and restore items, restore versions, change sharing and share links, and rename, archive, restore or transfer drives. | | `file.delete` | Delete items permanently and delete drives. | Drive and item roles still decide what each person can do inside a drive. See [Access control](/docs/product/access-control). ## Automation and AI [#automation-and-ai] MCP tools for files are read-only: | Tool | Kind | | ---------------------- | ---- | | `listFileDrives` | Read | | `listFileNodeChildren` | Read | | `getFileNode` | Read | | `createFileDownload` | Read | `createFileDownload` returns a short-lived signed URL without exposing the storage path. See [MCP](/docs/developers/mcp). ## API [#api] Organization paths are relative to `/api/v1/organizations/{organizationId}`. | Resource | Operations | | --- | --- | | `files/drives` | `listFileDrives` (GET) | | `files/nodes/{nodeId}` | `getFileNode` (GET) | | `files/nodes/{nodeId}/children` | `listFileNodeChildren` (GET) | | `files/nodes/{nodeId}/download` | `createFileDownload` (GET) | | `files/uploads` | `createFileUpload` (POST) reserves quota and returns a signed resumable upload transport. | | `files/uploads/{reservationId}/finalize` | `finalizeFileUpload` (POST) | | `files/uploads/{reservationId}/cancel` | `cancelFileUpload` (POST) | | `files/copy` | `copyFileNodes` (POST) copies folder trees recursively. | | `files/zip-exports` | `createFileZipExport` (POST) | | `files/zip-exports/{exportId}` | `getFileZipExport` (GET) | `GET /api/v1/public/files/{token}` (`downloadPublicSharedFile`) serves share links. Background jobs support Files: `file_upload_reconciliation` expires stale uploads and removes their objects every 15 minutes, a `file_zip_export` job builds each ZIP download that is too large for the request as soon as it is requested, and `file_zip_export_expiry` removes expired downloads. See [API](/docs/developers/api) and [OpenAPI](/docs/developers/openapi). ## Related pages [#related-pages] - [Customers](/docs/product/customers) for customer folders. - [Billing and usage](/docs/product/billing-and-usage) for storage limits. - [Audit log](/docs/product/audit-log) for file activity history. # Hiring (/docs/product/hiring) How to open jobs, publish them on the careers page, move candidates through stages on a board, score and interview them, analyze CVs, send candidate emails and hire a candidate into People. ## Overview [#overview] Hiring tracks vacancies and candidates from the first application to the first working day. Each job has its own stages and candidate board, and hiring a candidate creates an employee in [People](/docs/product/people) and starts onboarding. Hiring lives under Workspace, People, Hiring in the sidebar. | Route | Purpose | | --- | --- | | `/{orgSlug}/hiring` | Jobs list, with the Jobs and Email templates tabs. | | `/{orgSlug}/hiring/email-templates` | Candidate email templates. | | `/{orgSlug}/hiring/{jobId}` | Job detail with the Candidates, Stages and Insights tabs. | | `/{orgSlug}/hiring/applications/{applicationId}` | One candidate's application. | | `/{orgSlug}/portal/careers` | Public careers page with all published jobs. | | `/{orgSlug}/portal/careers/{jobId}` | Public job page with the application form. | ## Key concepts [#key-concepts] ### Jobs [#jobs] A job is a vacancy with a title, location, employment type, contract, hours per week, monthly salary range, description and hiring manager. Its status is Draft, Open or Closed. An open job can be published on the careers page. Closing a job keeps its applications, while deleting a job removes the job and all its applications. ### Stages [#stages] Stages are the columns of a job's candidate board, such as Applied, Screening, Interview, Offer and Hired. Each stage can have scorecard criteria, and a required criterion must be scored before a candidate moves past that stage. A stage can also send an email to the candidate. When you remove a stage that still holds candidates, you choose where they go. ### Applications [#applications] An application links a candidate to a job. It records the name, email, phone, LinkedIn profile, CV, motivation and source (careers page, added manually, referral or agency). Its status is In progress, Rejected, Hired or Withdrawn. A candidate who stays in one stage longer than the threshold in HR settings is marked as stalled. ### Candidate data retention [#candidate-data-retention] Candidates agree to the processing of their details when they apply. Their data is deleted after the application closes, unless they opt in to being kept for one year for future positions. The application shows "Data kept until" once the application closes. A background job anonymizes candidates the day after their retention period ends and removes their CVs. The retention period for rejected candidates is set in [HR settings](/docs/product/people). ## Work with jobs [#work-with-jobs] ### Open a job [#open-a-job] 1. Open Hiring and select **New job**. 2. Fill in the title, status, location, employment type, contract, hours per week, hiring manager, monthly salary range and description. 3. Turn on **Show on the public careers page** to publish an open job, and select **Create**. 4. Open the job's **Stages** tab to adjust stages, criteria and stage emails, then select **Save**. The job summary on that tab shows the public job link and an **Edit job** button. ### Add a candidate [#add-a-candidate] Candidates who apply on the careers page appear in the first stage automatically. To add one yourself, open the job and select **Add candidate**. A PDF or image CV is summarized automatically. A CV is checked before it can be downloaded, which takes up to a minute after the upload; a file whose content does not match its type is blocked. ### Move candidates [#move-candidates] Drag a candidate card to another column on the Candidates tab. The board also supports the keyboard: press space to pick up a card, the arrow keys to move it and space again to drop it. Cards show the source, average rating, days in the stage and skills from the CV. ### Score a candidate [#score-a-candidate] Open the application and use **Scorecards**. Each interviewer gives an overall rating and a rating per criterion on a scale of Strong no, No, Yes and Strong yes, with optional notes. The application shows the average rating and everyone's scores. ### Schedule an interview [#schedule-an-interview] Select **Schedule interview** on the application, choose the interviewers and a date, and pick a slot. CentraKit suggests shared free time in the next five days from the interviewers' agendas. The interview is added to the agenda of every interviewer. See [Agenda](/docs/product/agenda). ### Analyze a CV [#analyze-a-cv] Select **Analyze CV** to let AI summarize the uploaded CV with years of experience and skills. The summary is labeled as AI generated and should be checked against the original. Only PDF and image files can be read. ### Reject or hire [#reject-or-hire] - **Reject** closes the application and sends the rejection email to the candidate. Open the candidate first to add a reason or skip the email. - **Hire** creates an employee profile, starts the default onboarding checklist and keeps the CV with the employee. ## Candidate emails [#candidate-emails] The Email templates tab holds the emails sent automatically to candidates: | Template | Sent when | | --- | --- | | Application received | A candidate applies. | | Rejection | An application is rejected. | | Stage email | A candidate enters a stage that has an email. Set these per job on the Stages tab. | Templates support the `{{firstName}}`, `{{jobTitle}}` and `{{organizationName}}` placeholders. Candidates without an email address receive no email. ## Careers page [#careers-page] The careers page at `/{orgSlug}/portal/careers` lists every published job with its location, type and hours. Visitors open a job, read the description and apply with their name, email, phone, LinkedIn profile, CV (PDF or image, up to 10 MB) and motivation. The form asks for consent before it can be sent and limits repeated applications from one connection. ## Insights [#insights] The Insights tab on a job shows the number of applications, candidates in progress, candidates waiting too long and hires. A funnel lists how many candidates reached each stage, the conversion between stages and the average days spent in each stage. A sources breakdown shows where candidates came from. ## Permissions [#permissions] | Permission | Allows | | --- | --- | | `hiring.read` | See jobs and applications, download CVs and fill in your own scorecards. | | `hiring.create` | Create jobs and add candidates. | | `hiring.update` | Edit jobs and stages, move, reject and hire candidates, analyze CVs, schedule interviews and edit email templates. | | `hiring.delete` | Delete jobs with their applications. | | `hiring.manageCareersPage` | Publish or unpublish jobs on the careers page. | The careers page is public and needs no sign-in. ## Automation and AI [#automation-and-ai] - AI CV analysis summarizes experience and skills on the application. - Interview slot suggestions use the interviewers' agendas. - A background job anonymizes candidates after their retention period. ## API [#api] All routes live under `/api/v1/organizations/{organizationId}/`: | Resource | Routes | | ------------ | -------------------------------------- | | Jobs | `GET hiring/jobs` | | Applications | `GET hiring/jobs/{jobId}/applications` | See [API](/docs/developers/api) and [OpenAPI](/docs/developers/openapi). ## Related pages [#related-pages] - [People](/docs/product/people) - [Agenda](/docs/product/agenda) - [Customer portal](/docs/product/customer-portal) - [Access control](/docs/product/access-control) # Inbox (/docs/product/inbox) How the Inbox shows customer conversations from WhatsApp and web channels, how staff reply with attachments and AI suggestions, take over from the AI, approve AI order proposals and track delivery. ## Overview [#overview] Inbox is where an organization talks with its customers over external channels. Every inbound message lands in a conversation thread, the AI can answer or draft replies, and staff can take over, reply, attach files and turn a conversation into a customer, task, appointment or order. Inbox sits at the top of the organization sidebar. | Route | Purpose | | --- | --- | | `/{orgSlug}/inbox` | Conversations list with the selected conversation beside it. | | `/{orgSlug}/inbox/{threadId}` | One conversation. | Channel connections are not configured in Inbox. They are set up under [Integrations](/docs/product/integrations), and **Manage integrations** in the Inbox opens that page. ## Key concepts [#key-concepts] ### Channels and connections [#channels-and-connections] A connection links one external account to the organization. WhatsApp Business Cloud is the channel you can connect today, and web threads also appear in the Inbox. A connection is Draft, Active, Disabled or Error, and has a health status. A WhatsApp connection holds: | Setting | Meaning | | --- | --- | | Name and phone number ID | The WhatsApp Business number to use. | | Connection scope | The organization's company WhatsApp, or your own company phone number. | | Access token, app secret and verify token | Credentials from the Meta app. Leave them blank on edit to keep the current values. | | Auto-reply mode | Off, Draft or Auto, described under AI replies below. | | Reply model | The AI model for replies, or the organization chat default. | | Escalation keywords | Words that make the AI ask a person to take over. | | Fallback message | The reply sent when the AI cannot answer. | | Webhook URL | The callback URL to enter in the Meta app dashboard together with the verify token. | ### Threads [#threads] A thread is one conversation with a customer or contact on one connection. Threads link to the matching customer or contact when one is known. The list groups threads into Pinned and recent periods and can be searched. Each thread is read or unread, can be pinned or archived, and shows the channel it came from. A thread also has a handling status: the AI is handling it, a takeover is requested, a person is handling it, or it is resolved. ### Messages, attachments and delivery [#messages-attachments-and-delivery] Messages show who sent them and when. Staff replies can include file attachments, and photos and other media in the thread are collected in a media strip with a full-screen viewer. Every file is checked for its content before it can be opened: staff uploads right away, files that customers send within a minute of arriving. Every outbound message has a delivery record that moves through queued, sent, delivered, failed, dead-lettered or canceled, with the history of each attempt. ## Work with conversations [#work-with-conversations] ### Reply to a customer [#reply-to-a-customer] 1. Open a conversation. 2. Type a reply, pick one of the suggested replies above the composer, or use voice input. 3. Add attachments with **Add attachment** and select send. If delivery fails, the message shows **Delivery failed** with its attempts, and you can retry it. ### Start a conversation [#start-a-conversation] Select **New chat**, choose a customer or contact and a WhatsApp connection, and write the first message. The recipient needs a WhatsApp or phone number. Outbound chats work with WhatsApp connections only. ### Take over from the AI [#take-over-from-the-ai] When the AI requests a takeover, or whenever you want to answer yourself, select **Take over**. The AI stops replying in that thread until you select **Resume AI**. ### Organize threads [#organize-threads] Use the conversation actions to mark a thread as read or unread, pin or unpin it, archive it or delete it. Deleting a thread permanently removes the conversation and its messages. ## AI replies [#ai-replies] The auto-reply mode of the connection decides what the AI does with new messages: | Mode | Behavior | | --- | --- | | Off | The AI does not reply. Staff answer every message. | | Draft | The AI writes an AI reply draft. Staff review and edit it, then send or discard it. | | Auto | The AI replies directly. | In Draft and Auto mode, the AI requests a takeover instead of replying when a message contains an escalation keyword, when a message is longer than the automatic reply limit, or after repeated delivery failures on the connection. Threads that a person is handling, or where a takeover is requested, get no AI replies. The AI can look up products and variants while it answers, but it never creates records on its own. ### Approve an AI proposal [#approve-an-ai-proposal] When a customer asks for something, the AI records a **Proposed action** card in the thread. A proposal can include a new customer with contact details, a task, an appointment and an order with product lines and the delivery method. Select **Create** to create everything in the proposal, or **Dismiss** to discard it. The created records are linked back to the conversation. ### Contextual AI actions [#contextual-ai-actions] The thread header offers **Summarize thread**, **Draft reply** and **Suggest next steps**. These open the assistant with the thread as context, and drafts are filled into the composer for review. See [Chat](/docs/product/chat). ## How inbound messages arrive [#how-inbound-messages-arrive] 1. The channel provider calls the connection's webhook URL. WhatsApp verifies the URL with a `GET` request first. 2. CentraKit stores each inbound webhook as a job and answers right away. 3. A worker processes due jobs every minute, adds the message to the right thread and runs the AI reply policy. ## Permissions [#permissions] | Permission | Allows | | --- | --- | | `chat.read` | See the Inbox and mark threads read or unread. | | `chat.create` | Reply, send AI drafts, retry deliveries and start outbound chats. | | `chat.update` | Pin, archive, take over, resume the AI, discard drafts and decide proposals. | | `chat.delete` | Delete threads. | | `integration.manage` | Create and edit channel connections. | Creating records from a proposal also needs the matching create permission: `customer.create`, plus `task.create`, `agenda.create` or `order.create` when the proposal includes a task, appointment or order. ## Automation and AI [#automation-and-ai] - The **Inbox message received** workflow trigger starts workflows for inbound messages. A system workflow waits 4 hours and notifies the assignees if the thread is still unanswered. It never replies automatically. See [Workflows](/docs/product/workflows). - The assistant can list and read Inbox threads and mark them read or unread. - MCP clients can send a staff reply with the `sendInboxThreadReply` tool. See [MCP](/docs/developers/mcp). ## API [#api] All routes live under `/api/v1/organizations/{organizationId}/`: | Resource | Routes | | --- | --- | | Threads | `GET inbox/threads` | | Messages | `GET inbox/threads/{threadId}/messages`, `POST inbox/threads/{threadId}/messages` sends a staff reply | | Reply suggestions | `POST inbox/threads/{threadId}/suggestions` | | Attachments | `POST inbox/attachments`, `GET inbox/attachments/{attachmentId}` | | Deliveries | `GET inbox/deliveries/{deliveryId}` | | Channel webhooks | `GET` and `POST channels/{connectionId}/webhook`, called by the channel provider | See [API](/docs/developers/api) and [OpenAPI](/docs/developers/openapi). ## Related pages [#related-pages] - [Integrations](/docs/product/integrations) - [Chat](/docs/product/chat) - [Customers](/docs/product/customers) - [Orders](/docs/product/orders) - [Notifications](/docs/product/notifications) # Insights (/docs/product/insights) How the Insights hub reports revenue, quotes, receivables, margin, products, customers and stock from internal CentraKit data, with deterministic forecasts, rule-based insights and CSV or Excel exports. ## Overview [#overview] Insights is the business analytics surface of an organization. It reads only the organization's own CentraKit data, such as invoices, payments, quotes, products, customers and stock, and shows key figures, trends, rankings, forecasts and rule-based insights for a date range. Insights lives under Workspace, Resources, Insights in the sidebar. | Route | Purpose | | --- | --- | | `/{orgSlug}/insights` | Opens the first tab you can use. | | `/{orgSlug}/insights/analytics` | Analytics, with the Overview and Data & exports tabs. | | `/{orgSlug}/insights/reporting` | Not yet available. | Workforce figures such as headcount and absence live on the Insights tab of [People](/docs/product/people), and hiring funnels on each job in [Hiring](/docs/product/hiring). Platform-wide analytics for system admins is a separate surface. See [Admin analytics](/docs/admin/analytics). ## Key concepts [#key-concepts] ### Filters [#filters] Every view uses the same filters, kept in the URL so a view can be shared: | Filter | Options | | --- | --- | | Date range | Last 30 days, Last 90 days, Last 365 days or a custom range of up to five years. | | Comparison | No comparison, Previous period or Previous year. | | Currency | All currencies or one currency. | ### Metrics [#metrics] Metrics cover paid, issued and total revenue, collections, receivables, overdue receivables, average collection days and overdue cash risk; quote win rate and weighted quote pipeline; gross margin, gross margin rate and margin trajectory; product revenue, gross margin, units and demand; customer revenue, customer concentration and repeat customer rate; and stock value, stock velocity, dead stock value and stock depletion. Two rules apply to every metric: - Monetary values keep currencies separate and never add unlike currencies into one total. - Gross margin is revenue less recorded direct costs. It is not net profit, because overhead, tax and payroll are outside the data model. Margin metrics show how much of the revenue has recorded costs, and an insight warns when that coverage is limited. ### Forecasts [#forecasts] Forecasts are deterministic projections with a named method (run rate, moving average, seasonal average or linear trend), a confidence band and the number of history points used. When there is not enough history, coverage is too low or data is stale, the chart says why no forecast is shown. ### Insights [#insights] Insights are generated from transparent rules, not from generative AI. Examples are elevated overdue cash risk, concentrated customer revenue, declining gross margin, limited gross margin coverage, dead stock and stock depletion risk, and material increases or declines in a metric. Each insight links to the evidence behind it. ## Use the Overview tab [#use-the-overview-tab] The Overview tab shows: - Key figures, such as paid revenue, product revenue, customer revenue and stock value, with the change against the comparison period. - A revenue trend with its forecast. - Composition rankings of the top customers and products. - Biggest movers, the figures that changed most against the comparison period. - The rule-based insights for the range. ## Use the Data & exports tab [#use-the-data--exports-tab] The Data & exports tab shows the records behind the overview as a searchable, filterable table. Choose a report: | Report | Contents | | --- | --- | | Source evidence | One row per source record, such as an invoice or quote, with its date, metric, source and value. | | Rankings | The ranked customers and products. | | Forecasts | Forecast points with method, bounds and confidence, or the reason a forecast is unavailable. | | Metric definitions | What each metric measures. | ### Export a report [#export-a-report] 1. Open the Data & exports tab and set the filters and report. 2. Select **Export**. 3. Choose **CSV** for plain text that opens in any spreadsheet, or **Excel** for a workbook with one row per record. The file is named after the report and the end date of the range. ## Permissions [#permissions] | Permission | Allows | | ------------------ | ---------------------------- | | `analytics.read` | Open Insights and Analytics. | | `analytics.export` | Export analytics reports. | ## Automation and AI [#automation-and-ai] - **Explain visible report** and **Compare visible period** ask the assistant to narrate what is on screen. The narrative is labeled as not authoritative and never produces new metrics. See [Chat](/docs/product/chat). - The assistant can read organization analytics for a date range with the same permission. ## Related pages [#related-pages] - [Dashboard](/docs/product/dashboard) - [Invoices](/docs/product/invoices) - [Quotes](/docs/product/quotes) - [Products](/docs/product/products) - [Customers](/docs/product/customers) - [Admin analytics](/docs/admin/analytics) # Integrations (/docs/product/integrations) How the integrations marketplace connects WhatsApp Business inbox channels, Google Calendar and signed incoming and outgoing webhooks to an organization. ## Overview [#overview] Integrations connect the organization to outside services. The marketplace lists what is available, and each integration has a detail page where you set up its connections. | Route | Purpose | | --- | --- | | `/{orgSlug}/integrations` | Marketplace of all integrations. | | `/{orgSlug}/integrations/{integrationSlug}` | Detail page with resources and the configuration section. | | `/{orgSlug}/integrations/whatsapp/create` | New WhatsApp connection. | | `/{orgSlug}/integrations/whatsapp/{connectionId}` | Edit a WhatsApp connection. | | `/{orgSlug}/integrations/webhooks/incoming/create` | New incoming webhook endpoint. | | `/{orgSlug}/integrations/webhooks/incoming/{endpointId}` | Edit an incoming endpoint. | | `/{orgSlug}/integrations/webhooks/outgoing/create` | New outgoing webhook destination. | | `/{orgSlug}/integrations/webhooks/outgoing/{destinationId}` | Edit an outgoing destination. | ## Key concepts [#key-concepts] ### The marketplace [#the-marketplace] The marketplace shows Featured, Recently Added, Native Integrations and All Integrations. You can search, sort by popularity and filter by category and availability. Each integration has one availability: | Availability | Meaning | | ------------ | ---------------------------------- | | Available | You can set it up now. | | Coming soon | Being built. | | Planned | On the roadmap. | | Deprecated | Still works, but will be removed. | | Unavailable | Cannot be used in this deployment. | These integrations are available today: | Integration | Slug | What it does | | --- | --- | --- | | WhatsApp Business | `whatsapp` | Receives and sends WhatsApp messages as [Inbox](/docs/product/inbox) conversations. | | Google Calendar | `google-calendar` | Imports Google calendars into the [Agenda](/docs/product/agenda). | | Webhooks | `webhooks` | Receives JSON that starts workflows and posts signed events to your own systems. | Not yet available: Slack and Resend are coming soon. Email, Stripe, Exact Online, Microsoft Teams, Google Drive and OneDrive are planned. For an integration that is not available yet, select Request this integration to help prioritize it. The detail page also lists integration resources such as the website, documentation, support, terms and privacy policy, and whether it installs for the organization or per user. ### Connections [#connections] A connection is one configured instance of an integration, such as one WhatsApp phone number. Inbox connections show a health status: Healthy, Degraded, Unhealthy, Error or Unknown. ## Connect WhatsApp Business [#connect-whatsapp-business] 1. Open Integrations and choose WhatsApp Business, then New WhatsApp connection. 2. Enter a name, the connection scope (the organization company WhatsApp or your own company phone number), the phone number ID, access token, app secret and verify token from your Meta app. 3. Choose the auto-reply mode: Off, Draft (the assistant drafts replies for a person to send) or Auto. Optionally set the reply model, escalation keywords that hand a conversation to a person, and a fallback message. 4. Select Save connection, then copy the webhook URL. In the Meta app dashboard, configure it under WhatsApp webhooks together with your verify token. Incoming messages then create or continue conversations in the inbox. To change the access token or app secret later, enter a new value; leave the field blank to keep the current one. ## Connect Google Calendar [#connect-google-calendar] Google Calendar connects per user. On the Google Calendar detail page, connect your Google account and choose which calendars to import. Imported calendars are read-only in the agenda and sync in the background. See [Agenda](/docs/product/agenda) for sync behavior and reconnecting. ## Webhooks [#webhooks] The Webhooks detail page has three sections: Incoming endpoints, Outgoing destinations and Recent deliveries. ### Incoming endpoints [#incoming-endpoints] An incoming endpoint is a public URL that receives JSON and starts matching workflows. A workflow uses it through a webhook trigger, and only published workflows start runs. 1. Select New incoming webhook, enter a name, choose the verification and create it. 2. Copy the webhook URL and the shared secret now. Neither is shown again. 3. Share the webhook URL with the external system. It posts to `POST /api/v1/public/webhooks/{token}`. The verification decides what each request must carry: a Standard Webhooks signature made with the secret (the default), the HMAC-SHA256 of the body in a signature header, the shared secret itself in a header for senders that can set a header but cannot sign, or nothing beyond the URL. For the header modes you can name the header. Verified requests are stored at once and start workflows within about a minute. The edit page renames the endpoint, changes its verification or signature header, and enables or disables it; the URL stays the same. Changing the verification to a signing mode shows a new secret once. Rotate URL issues a new public token and invalidates the previous one immediately. Rotate secret issues a new secret and keeps the URL; choose a grace period (none, 1 hour, 24 hours or 7 days) during which the previous secret still verifies requests, so the sender can switch without losing any. A workflow trigger's Rotate secret keeps the previous secret for 24 hours. ### Outgoing destinations [#outgoing-destinations] An outgoing destination is a signed HTTPS URL that receives domain events and workflow webhook actions. 1. Select New outgoing webhook and enter a name and an HTTPS URL. 2. Select the events to post automatically, or leave all unchecked to use the destination from workflows only. 3. Create it and copy the signing secret now. It is not shown again. Each delivery is a [Standard Webhooks](https://www.standardwebhooks.com) request signed with `webhook-id`, `webhook-timestamp` and `webhook-signature`, so your system can verify it with the signing secret and any Standard Webhooks library. Send test queues a delivery with event kind `webhook.test`, which does not start a workflow. Rotate secret issues a new signing secret; the previous one keeps signing for 24 hours. ### Deliveries [#deliveries] Recent deliveries lists each outgoing delivery with its status: pending, processing, completed, failed, dead lettered or canceled. Failed deliveries are retried automatically for up to eight attempts, and you can retry a finished one by hand. A destination that keeps failing for five days is disabled until you enable it again. ## Permissions [#permissions] | Key | Grants | | --- | --- | | `integration.manage` | Open the marketplace and integration detail pages, and create and edit WhatsApp connections and Google Calendar import settings. | | `webhook.read` | View incoming endpoints, outgoing destinations and deliveries. | | `webhook.create` | Create endpoints and destinations. | | `webhook.update` | Rename, rebind, reconfigure, enable or disable endpoints, edit destinations, rotate URLs and secrets, send tests and retry deliveries. | | `webhook.delete` | Delete endpoints and destinations. | Any member can request an integration. Platform administrators manage the marketplace catalog in [Admin integrations](/docs/admin/integrations). See [Access control](/docs/product/access-control) for roles. ## Automation and AI [#automation-and-ai] Workflows use webhooks in two ways: a webhook trigger starts a run when an incoming endpoint receives JSON, and a webhook action posts to an outgoing destination. See [Workflows](/docs/product/workflows). The assistant and MCP clients can manage webhooks with these tools: | MCP tool | Access | | --- | --- | | `listWebhookEndpoints`, `getWebhookEndpoint` | Read | | `createWebhookEndpoint`, `updateWebhookEndpoint`, `deleteWebhookEndpoint` | Write | | `rotateWebhookEndpointSecret`, `rotateWebhookEndpointToken` | Write | | `listWebhookDestinations`, `getWebhookDestination` | Read | | `createWebhookDestination`, `updateWebhookDestination`, `deleteWebhookDestination` | Write | | `rotateWebhookDestinationSecret` | Write | | `listWebhookDeliveries`, `getWebhookDelivery` | Read | WhatsApp connections and Google Calendar have no MCP tools. ## API [#api] Paths are relative to `/api/v1/organizations/{organizationId}`. | Method | Path | Permission | | --- | --- | --- | | GET, POST | `/webhooks/endpoints` | `webhook.read`, `webhook.create` | | GET, PATCH, DELETE | `/webhooks/endpoints/{endpointId}` | view, update, delete | | POST | `/webhooks/endpoints/{endpointId}/rotate-secret` | `webhook.update` | | POST | `/webhooks/endpoints/{endpointId}/rotate-token` | `webhook.update` | | GET, POST | `/webhooks/destinations` | `webhook.read`, `webhook.create` | | GET, PATCH, DELETE | `/webhooks/destinations/{destinationId}` | view, update, delete | | POST | `/webhooks/destinations/{destinationId}/rotate-secret` | `webhook.update` | | GET | `/webhooks/deliveries` | `webhook.read` | | GET | `/webhooks/deliveries/{deliveryId}` | `webhook.read` | Meta calls `GET` and `POST /channels/{connectionId}/webhook` under the same organization path to verify and deliver WhatsApp messages. Background jobs process channel messages and outgoing deliveries every minute. See [Webhooks for developers](/docs/developers/webhooks) for payloads and signature verification, and [OpenAPI](/docs/developers/openapi) for schemas. ## Related pages [#related-pages] - [Inbox](/docs/product/inbox) for conversations from connected channels. - [Agenda](/docs/product/agenda) for imported Google calendars. - [Workflows](/docs/product/workflows) for webhook triggers and actions. - [Admin integrations](/docs/admin/integrations) for the catalog. # Invoices (/docs/product/invoices) How invoices work at /invoices, including creation from quotes and tasks, sending, online payment through Stripe, recorded payments, credit notes, write-offs, reminders and the Payments page. ## Overview [#overview] Invoices bill customers for quotes, tasks, orders and other work. They live under **Finance > Invoices** at `/{orgSlug}/invoices`, which has two tabs: - **Pipeline** (`/invoices`): a board with the columns Suggested, Draft, Sent, Overdue, Written off, Paid and Cancelled. Every open invoice shows. Paid and cancelled columns default to invoices closed since last month; switch the period to All time for older ones. - **All invoices** (`/invoices/all`): a table with search, filters, sorting and bulk actions. New invoices start at `/invoices/create` and open at `/invoices/[invoiceId]`. The detail page is a branded document preview with the email message, payments, notes and linked records below it. Online payment is set up on the payment settings at `/{orgSlug}/payments/settings`, which the **Settings** button on **Finance > Payments** (`/{orgSlug}/payments`) opens. ## Key concepts [#key-concepts] ### Kinds [#kinds] | Kind | Meaning | | --- | --- | | Invoice | A regular invoice | | Deposit | The deposit invoice created when a quote with a deposit is accepted | | Credit note | A correcting document with its own number, inverted amounts and a reference to the original invoice | ### Statuses [#statuses] | Status | Meaning | | ----------- | ------------------------------------- | | Draft | Editable, including the customer | | Sent | Issued to the customer and immutable | | Overdue | Past the due date and still unpaid | | Written off | Uncollectible, with a required reason | | Paid | Settled in full | | Cancelled | Voided before it was paid | On the board you can drag a draft to Sent, a sent or overdue invoice to Paid or Cancelled, and a written-off invoice to Paid. ### Lines, charges and totals [#lines-charges-and-totals] Line types are section headers, products, time (hourly), fixed price and custom lines. Invoice templates can define standard costs, which you add one at a time with **Add standard cost**; each one copies an editable snapshot onto the invoice. Discounts can be a percentage or a fixed amount, or come from a [promotion](/docs/product/orders). VAT works as on quotes: organization presets plus Custom, totals per rate, and the regimes standard, reverse charge, exempt, KOR and margin scheme. An invoice can carry a customer reference, such as a purchase-order number, and a delivery date that defaults to the invoice date when it is sent. Collection costs, statutory interest and late fees are separate charges that count toward the amount due. The **Add charge** dialog shows the statutory maximum for collection costs. ### Lifecycle [#lifecycle] The **Invoice progress** strip follows the default invoice lifecycle: Sent, Viewed and Paid, with Partially paid as a flag. Its reminder ladder sends reminders 3 days before the due date and 7 and 14 days after it. The invoice becomes Overdue on the due date, a final notice goes out 21 days after the due date, and 15 days later collection costs are added and the invoice moves to In collection. Customer emails run within business hours and re-check the invoice first. Invoice templates can pick a different lifecycle or customize its reminders. See [Content](/docs/product/content). ## Create an invoice [#create-an-invoice] From the Invoices page: - **New invoice** starts an empty invoice. - **More create options > From template** starts from an invoice template. - **More create options > From quote** starts from an accepted quote. You can also select **Create invoice** on the accepted quote. From billable work: - The **Suggested** column groups billable tasks, logged time, materials and expenses per customer. Drag a suggestion to Draft, or select several, to create draft invoices. **Mark as not billable** excludes its time, materials and expenses from future invoices. - Link tasks on a draft in the **Quotes and tasks** section to bill them. - On an order, **Create invoice** bills what is ready to invoice. See [Orders](/docs/product/orders). Then select the customer and optional contact, add lines, set the invoice date, due date and options, and save. **Scan image** on the line editor looks a product up from a photo. See [Products](/docs/product/products). The **Options** panel holds the workflow, payment term, template, currency, VAT and online payment settings. Online payment is on by default; turn it off to offer bank transfer only. ## Send an invoice [#send-an-invoice] Select **Send** on a draft. Sending needs a due date, a customer billing or contact email, and a customer VAT number when reverse charge applies. When online payment is on, the organization's Stripe account must be able to receive payments. Sending does not create a payment: the customer's payment starts when they select **Pay invoice** in the portal. When an approval policy applies, approvers are notified and you send again once it is approved. See [Approvals](/docs/product/approvals). The first send snapshots the seller and buyer details, so later customer or organization edits do not change the issued invoice. The email attaches the invoice PDF, plus the terms and conditions PDF when the template sets one. Sent and overdue invoices can be sent again from the **Other** menu. Several invoices can be sent at once from **All invoices**. Sending an invoice created from a quote consumes the stock the quote held. ## Get paid [#get-paid] ### Online payment [#online-payment] The customer opens the invoice at `/{orgSlug}/portal/invoices/[invoiceId]`, including as a guest, and selects **Pay invoice**. The embedded Stripe Payment Element offers the payment methods the account supports, such as cards and iDEAL. Online payment is available for invoices in euros only; other currencies show that online payment is unavailable. A bank payment such as SEPA Direct Debit can take a few business days, and the portal shows it as processing instead of offering the payment again. Stripe webhooks are authoritative: the invoice becomes paid when Stripe confirms the payment, and a returning browser only refreshes the page. Failed payments, refunds and disputes are mirrored on the invoice. See [Customer portal](/docs/product/customer-portal). ### Refunds and lost disputes [#refunds-and-lost-disputes] When an online payment is refunded, in full or in part, from **Finance > Payments** or through the Stripe API, or when the organization loses a dispute, the invoice shows the money that went back to the customer. The amount due goes up by the refunded or lost amount, and a paid invoice moves back to Sent, or to Overdue when its due date has passed, with a status history entry that says why. A second refund on the same payment raises the amount due again. When a credit note issued for the invoice already covers the returned amount, the invoice stays paid with nothing due. An open dispute does not change the invoice, and a dispute the organization wins keeps it paid. The invoice creator gets an in-app notification, and the `invoice.refunded` and `invoice.dispute-lost` events can start workflows. Recorded payments are never touched by these steps. ### Record a payment [#record-a-payment] For bank transfers, cash and other payments, open the **Payments** section and select **Record payment**. Enter the amount, method, date received and reference. An invoice can have several payments; the section shows the total, paid and outstanding amounts, and the payment that settles the balance marks the invoice paid. Recorded payments can be removed. ### Mark paid [#mark-paid] **Mark paid** settles a sent, overdue or written-off invoice in one step. Marking paid cancels any open online payment. ## Correct or close an invoice [#correct-or-close-an-invoice] - **Credit note** creates a draft credit note for a sent, overdue, written-off or paid invoice. You can return the items to stock. Sending a credit note leaves the original invoice unchanged and cancels its open online payment when the original is unpaid. An original that credit notes cover in full shows as Credited in the Paid column, and it no longer turns overdue or gets reminders. - **Cancel invoice** voids a sent, overdue or written-off invoice that was never paid, with an optional reason and the option to return items to stock. Cancelled invoices keep their number. - **Write off** marks a sent or overdue invoice as uncollectible with a required reason. Reminders stop, and payments that still arrive are recorded. - **Pause reminders** stops reminders and fees with a reason, until a date you choose or until you select **Resume reminders**. - **Archive** hides an invoice from active lists and the pipeline. Archived invoices can be restored from the Archived filter. Drafts are archived instead of cancelled. The **Other** menu also offers **Download PDF**, **Show online** and **Duplicate**. Bulk actions in **All invoices** are send, mark paid, cancel and archive. Export the filtered list as CSV, Excel or PDFs. ## Notes and linked records [#notes-and-linked-records] Notes are an internal thread that stays off the PDF and the customer-facing invoice. Mention users and teams with `@`, or work and customer records with `#`. A duplicated invoice and a credit note start with a copy of the original's notes. Adding notes needs `comment.create`, and deleting another member's note needs `comment.moderate`. The **Quotes and tasks** section shows the quote and tasks the invoice bills for. ## Set up online payment [#set-up-online-payment] 1. Complete the address, KvK number, phone number and reply-to email address in the organization settings. They appear on the public business page, which iDEAL requires, and Stripe receives them prefilled. 2. Open **Finance > Payments** and select **Activate online payments**. The organization becomes the merchant for its invoice payments. 3. Finish the verification in the embedded Stripe form on the same page: the legal form, the representative and owners, the IBAN for payouts and the Stripe terms. Stripe may ask you to sign in with a one-time code. 4. Select **Refresh status** to update card payments, payouts and the verification deadline. The page shows Not activated, Setup required or Enabled. Once card payments and payouts are active, the page shows the account details instead of the verification form, and a banner lists anything Stripe needs later. Invoices can only be sent with online payment once the account is enabled. The page also states the platform fee per online payment, which platform admins set in [Platform settings](/docs/admin/platform-settings). See [Stripe billing](/docs/operations/stripe-billing) for the operator setup. ## Payments and payouts [#payments-and-payouts] Once online payments are enabled, **Finance > Payments** at `/{orgSlug}/payments` has two tabs. **Payments** lists the organization's online invoice payments, newest first, with the date, the invoice, the customer, the status and the amount. The status shows when a payment is processing, refunded in part or in full, disputed or lost in a dispute. Select a payment to open a side panel with the invoice lines and totals. From there, **Open invoice** goes to the invoice and **Details in Stripe** opens the Stripe payment details, where you refund the payment or respond to a dispute. Stripe itself does not show the invoice lines. **Payouts** at `/{orgSlug}/payments/payouts` lists the payouts Stripe sends to the organization's bank account and lets you edit the payout schedule. Both tabs show the data from the organization's own Stripe account. Until online payments are enabled they point to the payment settings. The **Settings** button next to the tabs opens the payment settings. All three pages need the `payment.manage` permission. ## Permissions [#permissions] | Key | Grants | | --- | --- | | `invoice.read` | See invoices and download PDFs | | `invoice.create` | Create invoices, credit notes and invoices from quotes, orders or suggestions | | `invoice.update` | Edit drafts, charges and linked tasks | | `invoice.delete` | Archive and restore invoices | | `invoice.send` | Send invoices | | `invoice.markPaid` | Mark invoices paid | | `invoice.recordPayment` | Record and remove manual payments | | `invoice.writeOff` | Write off invoices | | `invoice.pauseReminders` | Pause and resume reminders | | `invoice.cancel` | Cancel invoices | | `invoice.duplicate` | Duplicate invoices | | `payment.manage` | Open the Payments and Payouts tabs and the payment settings, and manage the Stripe account | Accepting or dismissing a suggestion also needs `task.update`. See [Access control](/docs/product/access-control). ## Automation and AI [#automation-and-ai] - Invoice events such as `invoice.sent`, `invoice.viewed`, `invoice.partially-paid`, `invoice.paid`, `invoice.overdue`, `invoice.reminder-sent`, `invoice.final-notice-sent`, `invoice.payment-failed`, `invoice.disputed`, `invoice.refunded`, `invoice.dispute-lost`, `invoice.uncollectible`, `invoice.cancelled`, `invoice.reminders-paused` and `invoice.reminders-resumed` can trigger [workflows](/docs/product/workflows) and outgoing [webhooks](/docs/developers/webhooks). - Starter automations for invoice templates include **B2C 14-day letter** (send individual customers the statutory letter once an invoice is overdue) and **Payment failed** (flag the invoice and alert the team). - The assistant can create, update, send, cancel, duplicate and archive invoices, record payments, write off, pause reminders and move lifecycle stages. See [Chat](/docs/product/chat). Not yet available: invoice tools on the MCP server. ## API [#api] All paths are under `/api/v1/organizations/{organizationId}`. | Resource | Operations | | --- | --- | | `/invoices` | [List](/docs/openapi/listInvoices), [create](/docs/openapi/createInvoice), [get](/docs/openapi/getInvoice), [update](/docs/openapi/updateInvoice), [delete](/docs/openapi/deleteInvoice) | | `/invoices/from-quote` | [Create from a quote](/docs/openapi/createInvoiceFromQuote) | | `/invoices/{invoiceId}/send` | [Send](/docs/openapi/sendInvoice) | | `/invoices/{invoiceId}/mark-paid` | [Mark paid](/docs/openapi/markInvoicePaid) | | `/invoices/{invoiceId}/payments` | [Record a payment](/docs/openapi/recordInvoicePayment) | | `/invoices/{invoiceId}/cancel` | [Cancel](/docs/openapi/cancelInvoice) | | `/invoices/{invoiceId}/write-off` | [Write off](/docs/openapi/writeOffInvoice) | | `/invoices/{invoiceId}/pause-reminders` and `/resume-reminders` | [Pause](/docs/openapi/pauseInvoiceReminders) and [resume](/docs/openapi/resumeInvoiceReminders) reminders | | `/invoices/{invoiceId}/duplicate` | [Duplicate](/docs/openapi/duplicateInvoice) | | `/invoices/{invoiceId}/notes` | [List](/docs/openapi/listInvoiceNotes), [add](/docs/openapi/addInvoiceNote), [update](/docs/openapi/updateInvoiceNote), [delete](/docs/openapi/deleteInvoiceNote) | | `/invoices/{invoiceId}/lifecycle` | [Get lifecycle](/docs/openapi/getInvoiceLifecycle) and [advance a stage](/docs/openapi/advanceInvoiceLifecycle) | ## Related pages [#related-pages] - [Quotes](/docs/product/quotes) - [Orders](/docs/product/orders) - [Tasks](/docs/product/tasks), [Time](/docs/product/time) and [Expenses](/docs/product/expenses) for billable work - [Content](/docs/product/content) for invoice templates and reminders - [Customer portal](/docs/product/customer-portal) - [Billing and usage](/docs/product/billing-and-usage) for the organization's own subscription, which is separate from customer invoices - [Stripe billing](/docs/operations/stripe-billing) # Notifications (/docs/product/notifications) How notifications reach you in the topbar and the Notifications hub, how to act on approvals and replies inline, and how watch levels and channel preferences control what you receive. ## Overview [#overview] Notifications tell you when something needs you or has happened to work you follow. They appear in two places: - The bell in the topbar shows the unread count and the latest items. It updates live while you work. - The Notifications hub at `/{orgSlug}/notifications` lists everything for the organization, with search, filters and bulk actions. It is the second item in the sidebar. Opening `/notifications` without an organization takes you to the hub of your preferred organization. ## Key concepts [#key-concepts] ### What the hub contains [#what-the-hub-contains] The hub combines several sources in one list: - Notifications stored for you, such as assignments, mentions, comments, messages and status changes. - Live AI activity: agent runs, artifacts and workflow runs that are running, waiting or failed. - Pending invitations to join another organization, which you accept or decline in place. - [Approval requests](/docs/product/approvals) that you can decide. ### Groups, subjects and types [#groups-subjects-and-types] Each notification belongs to a group: | Group | Meaning | | ------------ | ---------------------------------------------------- | | Needs action | You are expected to decide, reply or review. | | Running | AI or workflow work is in progress. | | Happened | Information about something that already took place. | The subject is the kind of record the notification is about, for example Task, Quote, Invoice, Order, Chat, Inbox, Workflow, Approval, Time off, Timesheet, Expense or Application. The type is the specific event, such as a task assignment, a mention, a quote acceptance, an overdue invoice or an escalated approval. Notifications also carry a priority (low, normal, high or urgent) and show who caused them: a user, a customer or the system. ### Watch levels [#watch-levels] You can watch a record to hear about it even when you are not assigned. Watching is available on tasks, chat threads, inbox threads, workflows, workflow runs, agenda items, customers, quotes, invoices, orders and hiring applications. | Level | What you receive | | --- | --- | | All activity | Every update to the record. | | Participating and @mentions | Only updates where you take part or are mentioned. | | Ignore | Nothing for this record. | ### Channels [#channels] Each notification type can be delivered in the app, by email or by push. You set preferences for one organization or as an account default that applies everywhere. Notifications are delivered in the app today. Email and push preferences are saved, but delivery through those channels is not yet available. Not yet available: WhatsApp notifications. ## Work with notifications [#work-with-notifications] ### Read and clear notifications [#read-and-clear-notifications] Open the bell to see the latest items. Select Mark all as read to clear the count, or Show all to open the hub. In the hub you can: - Search notifications and filter by Status (All, Unread, Read, Resolved), Group, Subject and Type. Status shows only whether you read an item or whether its request was resolved. Items in the Needs action or Running group carry that group as a badge next to their title, and the Group filter finds them. - Sort by subject, type, status or date. - Open a row, copy its link, dismiss it, or mark it as read or unread. - Select several rows and choose Mark selected read. ### Act inline [#act-inline] Notifications that need action have buttons in the row, so you can respond without opening the record: | Notification | Action | | --------------------------- | --------------- | | Approval request | Approve or Deny | | Message that needs a reply | Reply | | AI output to check | Review | | Running AI or workflow work | Stop | ### Watch or ignore a record [#watch-or-ignore-a-record] On a record that supports watching, use Watch, Unwatch or Stop ignoring to change your level. The Notification settings link opens your channel preferences. ### Choose your channels [#choose-your-channels] Open your [profile](/docs/product/profile) and go to the Communication tab. The Channels table has In-app, Email and Push toggles. ## Permissions [#permissions] Notifications are personal. Every user sees only their own notifications and subscriptions, and only for records they can open. Acting on a notification uses the permission of the record behind it, for example deciding an approval needs the approver permission set on the approval rule. ## Automation and AI [#automation-and-ai] - Workflows can notify the people involved with a quote, invoice, task or order through the Send in-app notification step. Watchers who ignore the record are skipped. See [Workflows](/docs/product/workflows). - The assistant can list your notifications, count unread items, mark items as read and dismiss them. These actions do not ask for approval. See [Chat](/docs/product/chat). ## API [#api] All routes live under `/api/v1/organizations/{organizationId}/`: | Method and path | Purpose | | --- | --- | | `GET notifications` | List your notifications. | | `PATCH notifications/{notificationId}` | Mark a notification as read or unread, or dismiss it. | | `GET notifications/inbox` | Read the combined hub list. | | `POST notifications/mark-all-read` | Mark all notifications as read. | | `GET notification-preferences` | List your channel preferences. | | `PUT notification-preferences` | Set a preference for one type and channel. | | `GET notification-subscriptions` | List the records you watch or ignore. | | `POST notification-subscriptions` | Watch or ignore a record. | | `DELETE notification-subscriptions` | Remove a watch, named by `subjectType` and `subjectId`. | See [API](/docs/developers/api) and [OpenAPI](/docs/developers/openapi). To push events to your own systems, use [webhooks](/docs/developers/webhooks). ## Related pages [#related-pages] - [Approvals](/docs/product/approvals) - [Dashboard](/docs/product/dashboard) - [Inbox](/docs/product/inbox) - [Profile](/docs/product/profile) # Orders (/docs/product/orders) Sales orders at /orders, with stock reservation, fulfillment through shipments, invoicing policies, digital delivery, returns and promotions. ## Overview [#overview] Sales orders sit between an accepted quote or an incoming request and delivery and invoicing. They live under **Finance > Orders** at `/{orgSlug}/orders`, which has three tabs: | Tab | Route | What it shows | | --- | --- | --- | | Orders | `/orders` | The order list with search and filters | | Shipments | `/orders/shipments` | Outgoing shipments, expected supplier deliveries and customer returns | | Promotions | `/orders/promotions` | Discount rules for quotes, orders and invoices | An order opens at `/orders/[orderId]` with its lines, delivery and invoicing settings, fulfillment, invoices, digital delivery and lifecycle. A promotion opens at `/orders/promotions/[promotionId]`. The Promotions tab only shows for users who can view promotions. ## Key concepts [#key-concepts] ### Order statuses [#order-statuses] | Status | Meaning | | --------- | ----------------------------------------------------------- | | Draft | Lines and settings can change | | Confirmed | Stock is reserved and the order can ship and be invoiced | | On hold | Paused; it still holds its stock | | Completed | Fulfilled and invoiced | | Cancelled | The unshipped remainder is cancelled and its stock released | Each order also shows a derived fulfillment status (not shipped, partly shipped or shipped) and payment status (not invoiced, unpaid, partly paid or paid). ### Sources [#sources] An order records where it came from: manual, quote, inbox, API or integration. Quotes create orders when their quote template has **Create an order when accepted** switched on, or when staff select **Create order** on an accepted quote. Inbox orders arrive as a proposal with an approval card. API orders are idempotent per source and external ID. ### Lines [#lines] Lines are product variants, custom lines or shipping costs. An order can have an order discount as a percentage or fixed amount, or a promotion. A line cannot go below what already shipped, and shipped lines cannot be removed. ### Delivery and invoicing [#delivery-and-invoicing] The **Delivery and invoicing** section sets the delivery method, the invoicing policy, a requested delivery date, the ship-to address, a note for the customer and internal notes. | Delivery method | Meaning | | --------------- | ---------------------------------------------------------- | | Pickup | The customer collects the goods | | Own delivery | Delivered by hand; record a shipment when it has been sent | | Carrier | Shipped with a carrier, with tracking number and link | | Digital | Delivered as files, license keys or links | | No delivery | Nothing is delivered | | Invoicing policy | Meaning | | ----------------------- | -------------------------------------- | | Invoice before shipping | Prepaid; picking waits for payment | | Invoice per shipment | Invoice what was delivered | | Invoice manually | Staff create invoices when they choose | For digital sales to a consumer in another EU country, the order offers **VAT at the buyer's rate (OSS)**. ### Lifecycle [#lifecycle] The **Order progress** strip follows the default order lifecycle: confirmed, paid, picking, shipped, delivered, invoiced and completed. Prepaid orders wait for payment before picking. ## Create and confirm an order [#create-and-confirm-an-order] 1. Open **Orders** and select **New order**, then pick the customer. You add products on the draft. 2. Add product lines by name or SKU, custom lines and shipping costs, and set the delivery and invoicing options. 3. Select **Confirm**. Confirming reserves stock for the lines; anything that is not in stock becomes a backorder. When an approval policy applies, the order is sent for approval and goes ahead once approved. See [Approvals](/docs/product/approvals). On a confirmed order: - **Reserve stock** allocates stock that arrived for backordered lines. - **Put on hold** and **Release hold** pause and resume the order. - **Create invoice** bills what is ready to invoice under the order's policy. See [Invoices](/docs/product/invoices). - **Cancel order** cancels the unshipped remainder and releases its stock, with an optional reason. Shipped goods stay on the order. Drafts and cancelled orders with nothing shipped can be deleted. The order offers two PDFs: an order confirmation and a packing slip. ## Ship an order [#ship-an-order] 1. In the order's **Fulfillment** section, select **Create shipment**. 2. Pick what goes in the shipment. Shipping takes the reserved stock; the rest stays reserved for a later shipment, so partial shipments are allowed. 3. Choose the delivery method, carrier and tracking number. Use **Create and ship** with **Ship right away**, or **The customer picks it up now** for pickups, to complete it in one step. 4. Otherwise open the shipment later and select **Ship**, or **Mark picked up** for pickups. Shipment statuses are draft, packed, ready for pickup, in transit, delivered, picked up, problem and cancelled. Each shipment holds the carrier, number of packages, tracking number and tracking link, and the customer can follow it from the portal. Shipping fails when there is not enough stock. ## Handle a return [#handle-a-return] 1. On a confirmed or completed order, select **Register return**. 2. Pick what the customer sends back and what happens to each item: restock, quarantine or write off. 3. When the return arrives, select **Receive into stock**. 4. Select **Create credit note** to credit the returned items. See [Invoices](/docs/product/invoices). Supplier deliveries are inbound shipments announced from a purchase order. See [Products](/docs/product/products). ## Deliver digital products [#deliver-digital-products] Digital lines never create shipments. When the order is confirmed or paid, according to its invoicing policy, the order issues license keys from the product's key pool and emails secure download links. The **Digital delivery** section shows how much of each line was delivered, downloads and keys left, and revoked grants. Select **Deliver now** to deliver manually. When keys run out, the order delivers what is available and the remaining lines wait for license keys. Customers find their downloads and keys on the order in the portal. ## Promotions [#promotions] Promotions are discounts you pick on quotes, orders and invoices, or that customers apply with a code. | Setting | Options | | --- | --- | | Discount | Percentage, fixed amount or free shipping | | Applies to | The whole document, or lines with specific products, variants or categories | | Code | Optional, such as `SPRING10`; must be unique | | Usable on | Quotes, orders and invoices | | Eligibility | Everyone, business customers, private customers, specific customers or customer tags; a customer qualifies when any rule matches | | Schedule and limits | Start and end dates, minimum subtotal, minimum quantity, total uses and uses per customer | Promotions start as drafts. Create one with **New promotion**, then set targets, eligibility and dates on its detail page and set the status to active. The list shows each promotion's code, discount, status (draft, scheduled, active, expired or archived) and how often it was used. The detail page lists the documents that used it. To apply a promotion on a quote, order or invoice, open **Apply a promotion** and pick one or type its code. Picking a promotion copies its discount onto the document; editing the discount by hand unlinks it. On orders, promotions are set before confirming. A use counts when a quote is accepted, an order is confirmed, or an invoice without an order is sent. Cancelling and credit notes give the use back. Deleting removes an unused promotion. A promotion that was already used is archived instead, so documents keep their history. ## Permissions [#permissions] | Key | Grants | | --- | --- | | `order.read` | See orders and shipments | | `order.create` | Create orders, including from an accepted quote | | `order.update` | Edit drafts, hold and release, and reserve stock | | `order.delete` | Delete draft or cancelled orders | | `order.confirm` | Confirm orders | | `order.fulfill` | Create and ship shipments, register returns, and deliver digital items | | `order.cancel` | Cancel orders | | `promotion.read`, `.create`, `.update`, `.delete` | Manage promotions | Creating an invoice or a return credit note also needs `invoice.create`. Applying a promotion needs update access on the quote, order or invoice. See [Access control](/docs/product/access-control). ## Automation and AI [#automation-and-ai] - Order events include `order.created`, `order.confirmed`, `order.backordered`, `order.paid`, `order.shipped`, `order.ready-for-pickup`, `order.picked-up`, `order.delivered`, `order.invoiced`, `order.completed`, `order.cancelled` and `order.returned`. Shipments emit `shipment.shipped`, `shipment.out-for-delivery`, `shipment.ready-for-pickup`, `shipment.delivered` and `shipment.exception`. They can trigger [workflows](/docs/product/workflows) and outgoing [webhooks](/docs/developers/webhooks). - Shipments follow their own lifecycle from in transit through out for delivery to delivered, and flag carrier problems. - The `commerce_daily` background job runs every morning and emits `promotion.expiring` and `promotion.limit-reached` for promotions, and `purchase-order.overdue` for late purchase orders. - Stock changes queue an `inventory_signals` background job per variant, which publishes the level change about 30 seconds later. See [Products](/docs/product/products). - Starter automations for order templates include **Prepaid order to picking**, **Ready for pickup** (message the customer and remind them after 7 days) and **Delivered, then invoice**. Add them from the template's Workflow tab under [Content](/docs/product/content). - The assistant can create, update, confirm and cancel orders, move lifecycle stages, and manage promotions. See [Chat](/docs/product/chat). - MCP tools: `listOrders`, `getOrder`, `createOrder`, `confirmOrder`, `cancelOrder`, `listShipments`, `getShipment`, `shipShipment` and `validatePromotion`. See [MCP](/docs/developers/mcp). ## API [#api] All paths are under `/api/v1/organizations/{organizationId}`. | Resource | Operations | | --- | --- | | `/orders` | [List](/docs/openapi/listOrders), [create](/docs/openapi/createOrder) (idempotent per `source` and `externalId`, with optional `confirm`), [get](/docs/openapi/getOrder) | | `/orders/{orderId}/confirm` | [Confirm](/docs/openapi/confirmOrder) | | `/orders/{orderId}/cancel` | [Cancel](/docs/openapi/cancelOrder) | | `/orders/{orderId}/lifecycle` | [Get lifecycle](/docs/openapi/getOrderLifecycle) and [advance a stage](/docs/openapi/advanceOrderLifecycle) | | `/shipments` | [List](/docs/openapi/listShipments) and [get](/docs/openapi/getShipment) | | `/shipments/{shipmentId}/ship` | [Ship](/docs/openapi/shipShipment); returns `409 APPROVAL_REQUIRED` when an approval policy applies | | `/promotions/validate` | [Validate a promotion or code](/docs/openapi/validatePromotion) for a customer without redeeming it | Webshops can combine `createOrder`, `validatePromotion` and [inventory levels](/docs/openapi/listInventoryLevels) for checkout and stock sync. ## Related pages [#related-pages] - [Quotes](/docs/product/quotes) - [Invoices](/docs/product/invoices) - [Products](/docs/product/products) for stock, suppliers and purchase orders - [Customer portal](/docs/product/customer-portal) for order and shipment tracking - [Inbox](/docs/product/inbox) for orders proposed from conversations - [Content](/docs/product/content) for order templates - [Approvals](/docs/product/approvals) # Organizations (/docs/product/organizations) How organizations work as the tenant boundary in CentraKit, and how to create, switch between, transfer and delete them. ## Overview [#overview] An organization is one business in CentraKit. Customers, work, finance, people, conversations and automation all belong to exactly one organization, and no data is shared between organizations. The organization is part of every URL as `/{orgSlug}`, for example `/tractor-service/customers`. A user can belong to several organizations and switches between them by following a link to another slug. Two pages live outside any organization: | Route | Purpose | | -------------------- | -------------------------------------------- | | `/organizations` | Overview of every organization you can open. | | `/organizations/new` | Form to create an organization. | When you sign in or open `/`, CentraKit takes you to your preferred organization. If you have none, it takes you to `/organizations/new`. ## Key concepts [#key-concepts] ### Slugs [#slugs] The slug is the short name in the URL. It is created from the organization name and can be changed in [settings](/docs/product/settings-and-branding). Old links stop working after a slug change, because an unknown slug returns a not found page. ### How you belong to an organization [#how-you-belong-to-an-organization] You reach an organization in one of two ways: - As a user with a role such as Owner, Admin, Member or Viewer. See [Access control](/docs/product/access-control). - As a customer contact. You only see the [customer portal](/docs/product/customer-portal) for that organization. The organizations overview shows your role per organization, with an Owner badge for organizations you own and a Customer badge for organizations you reach through the portal. ### Owner [#owner] Every organization has one owner. The owner holds billing, deletion and ownership transfer rights in addition to everything an admin can do. ## Work with organizations [#work-with-organizations] ### Switch organizations [#switch-organizations] Open the organization switcher at the top of the sidebar and select another organization. All organizations opens the overview, and Create organization opens the form. You can also press Cmd+K or Ctrl+K and use the [command center](/docs/product/command-center). On the overview you can search, filter by Membership, open an organization, jump to its settings or copy its slug. ### Create an organization [#create-an-organization] Any signed-in user can create an organization. 1. Open `/organizations/new`, or select New organization on the overview. 2. Enter a name. The slug fills in from the name and can be edited. 3. Optionally add a website, company information, social links and branding. 4. Select Create organization. You become the owner. CentraKit adds default document templates and VAT rates and opens the new organization's dashboard. ### Transfer ownership [#transfer-ownership] On the settings page, the last section has Transfer ownership. Pick another active user as the new owner and type the organization name to confirm. The previous owner becomes an admin. The organization needs at least one other active user. ### Delete an organization [#delete-an-organization] On the settings page, select Delete organization and type the organization name to confirm. The organization is disabled at once, so members lose access, and is permanently deleted with all of its data after 30 days. CentraKit then opens your next organization, or the create form when none is left. Until then, the owner or the person who requested the deletion can cancel it: the Info tab of your profile lists the organizations scheduled for deletion with the date their data is deleted, and Cancel deletion enables the organization again. The REST API offers the same through `DELETE /api/v1/organization-deletions/{organizationId}`. ### Export the organization's data [#export-the-organizations-data] Owners and admins can select Export data at the bottom of the settings page. The export runs in the background and writes one CSV file per table. When it is ready, select Download to get a link per file. The files stay available for seven days. Platform administrators can also disable an organization. See [Organizations](/docs/admin/organizations). ## Permissions [#permissions] | Permission | Allows | | -------------------------------- | ------------------------------------- | | `organization.read` | Open the organization. | | `organization.update` | Edit settings, including the slug. | | `organization.transferOwnership` | Transfer ownership. | | `organization.delete` | Schedule the organization's deletion. | | `organization.export` | Export the organization's data. | ## Automation and AI [#automation-and-ai] The assistant can read the active organization and list your organizations. Changing organization settings through the assistant always asks for approval. See [Chat](/docs/product/chat). ## API [#api] | Method and path | Purpose | | --- | --- | | `GET /api/v1/organizations` | List organizations you belong to. | | `POST /api/v1/organizations` | Create an organization. | | `GET /api/v1/organizations/{organizationId}` | Read one organization. | | `PATCH /api/v1/organizations/{organizationId}` | Update an organization. | | `DELETE /api/v1/organizations/{organizationId}` | Schedule an organization's deletion; the response has the purge time. | | `PATCH /api/v1/users/me/active-organization` | Set your preferred organization. | API routes use the organization ID, not the slug. See [API](/docs/developers/api) and [OpenAPI](/docs/developers/openapi). ## Related pages [#related-pages] - [Settings and branding](/docs/product/settings-and-branding) - [Access control](/docs/product/access-control) - [Billing and usage](/docs/product/billing-and-usage) - [Tenancy](/docs/architecture/tenancy) # People (/docs/product/people) How to manage employees, contracts, documents, skills, working hours, equipment, onboarding and offboarding checklists, pre-boarding links, HR settings and workforce insights in the People hub. ## Overview [#overview] People is the HR module of CentraKit. It keeps one profile per employee, contractor or intern, with their contract, documents, skills, working hours, equipment and onboarding progress. The same employee records feed time off, time tracking, expenses, planning and the assistant. People lives under Workspace, People, Employees in the sidebar. A gear icon in the hub tab bar opens HR settings. | Route | Purpose | | --- | --- | | `/{orgSlug}/people` | Employees hub with the Employees, Org chart, Onboarding, Performance, Equipment, Skills and Insights tabs. | | `/{orgSlug}/people/create` | Create an employee. | | `/{orgSlug}/people/import` | Import employees from a CSV file. | | `/{orgSlug}/people/{employeeId}` | Employee detail with the Overview, Contract, Time off, Documents, Skills, Equipment, Onboarding and History tabs. | | `/{orgSlug}/people/onboarding/templates` | Onboarding templates. | | `/{orgSlug}/people/onboarding/templates/{templateId}` | Edit one template. | | `/{orgSlug}/people/onboarding/{journeyId}` | One onboarding or offboarding checklist. | | `/{orgSlug}/people/settings` | HR settings. | | `/portal/onboarding/{token}` | Pre-boarding page for a new hire without an account. | Not yet available: the Performance tab at `/{orgSlug}/people/performance`. Not yet available: Payroll at `/{orgSlug}/payroll`. ## Key concepts [#key-concepts] ### Employees [#employees] Every organization user gets an employee profile automatically when "Create employees for new members" is on in HR settings. You can also add employees who have no app account, such as freelancers or interns, and link them to a user later from the App access section. | Field | Values | | -------------- | ----------------------------------------- | | Type | Employee, contractor or intern. | | Status | Onboarding, active, offboarding or ended. | | Reporting line | Department (a team) and manager. | | Dates | Start date and optional end date. | Private details (date of birth, personal email and phone, IBAN, emergency contact and home address) are visible only to the employee and to people with `person.readSensitive`. Employees can hide their birthday and work anniversary from colleagues. ### Contracts [#contracts] A contract records the type (permanent, fixed term, on call or freelance), period, hours per week, pay basis (hourly, monthly, annual or fixed fee), pay frequency, holiday allowance, notice period and bonuses. The current contract drives leave entitlement and FTE. Pay is only shown to people who can view compensation. ### Working hours [#working-hours] Each employee follows the organization's default weekly schedule or personal working hours, including alternating two-week schedules. Agenda, Planning, leave hour calculation and timesheets all read the same schedule. Public holidays are set per organization and are never counted as leave. ### Documents, skills and certifications [#documents-skills-and-certifications] Documents are ID copies, driver's licences, certificates, contracts and other files, each with an optional issue and expiry date. Skills and certifications are defined once on the Skills tab and assigned to employees with an optional level. Certifications have a validity period, and expired certifications are flagged. Planning and tasks use skills as requirements, and HR settings decide whether an expired certificate warns or blocks an assignment. ### Equipment [#equipment] Equipment items are company property such as laptops, phones, tools and vehicle keys, with an asset tag, serial number, category, purchase price and warranty date. An item is Available, Handed out, In repair or Retired. Handing an item out records who holds it and its condition, and the holder confirms receipt from their own profile. ### Onboarding and offboarding [#onboarding-and-offboarding] An onboarding template has two lists of steps: Onboarding for a new hire and Offboarding for a leaver. Each step has a kind (task, equipment request, document request or personal details form), an owner (the employee, their manager, HR or a named user) and a due date relative to the start or end date. Negative days fall before the date, so -7 is a week before day one. Handed-out equipment gets a return step on offboarding automatically. Starting a checklist from a template creates a journey for one employee. A journey is in progress, completed or cancelled, and shows its buddy, first or last working day and progress. The default template starts automatically for new hires and leavers. ## Manage employees [#manage-employees] ### Add an employee [#add-an-employee] 1. Open People and select **New employee**. 2. Enter the name, work email, job title, type, department, manager and start date. 3. Select **Create**. Fill in working hours, private details and contracts on the detail page. ### Import employees [#import-employees] Open **Import** on the Employees tab and upload a CSV file with a header row. Supported columns are first name, last name, email, job title, kind, start date (YYYY-MM-DD), manager email, team and hours per week. Existing employees are matched on email and updated. ### Archive or delete an employee [#archive-or-delete-an-employee] The Danger zone on the Overview tab archives an employee, which hides them from the directory and pickers but keeps their history. Only employees without an app account can be deleted. ### Review history [#review-history] The History tab shows who changed what on the employee. Sensitive values are never shown. See [Audit log](/docs/product/audit-log). ## Run onboarding [#run-onboarding] ### Edit a template [#edit-a-template] Open **Onboarding**, then **Templates**, and select a template or **New template**. Add steps to the Onboarding and Offboarding lists and select **Save**. ### Start a checklist [#start-a-checklist] Select **Start checklist** on the Onboarding tab, or **Start offboarding** on the employee's Onboarding tab. Pick the template and dates. Add extra items on the checklist page, and mark items done as they are completed. ### Send a pre-boarding link [#send-a-pre-boarding-link] New hires without an account can fill in their details before day one: 1. Open the checklist and select **Pre-boarding link**. 2. Choose how long the link stays valid (1 to 60 days, 14 by default) and optionally enter the new hire's personal email to send it. 3. The new hire opens `/portal/onboarding/{token}`, enters their private details and uploads the requested documents (up to 10 MB each). The checklist shows until when the link is active, and **Revoke link** ends it at once. An expired or revoked link shows "Link unavailable". ## HR settings [#hr-settings] `/{orgSlug}/people/settings` holds the organization-wide rules: | Section | What it controls | | --- | --- | | General & pay | Full-time hours per week, pay frequency, pay day and automatic employee profiles for new users. | | Time off | Approval requirement, allowed negative balance, leave year start, public holiday country, team capacity warning and the HR handbook Knowledge collection. | | Time tracking | Timesheet approval, clock in and out, approximate location, billable rounding, budget warning and stopping forgotten timers. | | Expenses | Mileage and per diem rates, automatic approval threshold and receipts by email. | | Compliance & retention | Expired certificate behavior and how long former employee and rejected candidate data is kept. | | Default working hours | The weekly schedule for everyone without personal hours. | | Public holidays | Organization holidays, with a button to add the official holidays for the configured country. | | Leave types, leave policies and blackout periods | See [Time](/docs/product/time). | | Expense categories and expense policies | See [Expenses](/docs/product/expenses). | ## Insights [#insights] The Insights tab shows headcount, hires, leavers, turnover, absence rate and overtime for the last 12 months, a monthly headcount chart, labor cost per team (logged hours times each employee's cost rate) and a Needs attention list. Needs attention flags sustained overtime, planning mismatches, expense outliers and frequent short sickness from the last four weeks. People with `person.readInsights` see the whole organization. Managers see the people who report to them. Sickness data is hidden without `person.readSensitive` because it is health data. ## Permissions [#permissions] | Permission | Allows | | --- | --- | | `person.read` | See the employee directory and org chart, and edit your own private details and privacy settings. | | `person.create` | Create and import employees. | | `person.update` | Edit employees, documents and skills, and see the Performance tab. | | `person.delete` | Delete employees without an app account. | | `person.readCompensation` | See contract pay. | | `person.manageCompensation` | Add, edit and delete contracts. | | `person.readSensitive` | See private details and sickness data. | | `person.readInsights` | See workforce insights for the whole organization. | | `equipment.read`, `.create`, `.update`, `.delete` | Work with equipment items and hand-outs. | | `onboarding.read`, `.create`, `.update`, `.delete` | See, start, change and archive checklists and templates. | | `hrSettings.manage` | Edit HR settings, default working hours and public holidays. | Any organization user can complete checklist items assigned to them and confirm receipt of their own equipment. ## Automation and AI [#automation-and-ai] - Saving an expiry date on a document or an end date on a contract queues one reminder for 06:07 UTC on the reminder date (the document's reminder days before expiry, or 30 days before the contract ends). Changing the date queues a new reminder, and a contract with a follow-up contract sends none. - A daily job runs leave accrual and sick-leave follow-up, stops forgotten timers and closes forgotten clock-ins. Expired candidate data is anonymized by the purge job every 15 minutes. - The dashboard shows who is out, birthdays and work anniversaries for people who share them. See [Dashboard](/docs/product/dashboard). - The assistant reads leave balances and policies and decides HR approvals. See [Time](/docs/product/time) and [Chat](/docs/product/chat). ## API [#api] All routes live under `/api/v1/organizations/{organizationId}/`: | Resource | Routes | | --- | --- | | Employees | `GET` and `POST employees`, `GET` and `PATCH employees/{employeeId}` | Employee responses never include private details. The pre-boarding page uses the public `GET` and `POST /api/v1/public/onboarding/{token}` and `POST /api/v1/public/onboarding/{token}/documents` routes. See [API](/docs/developers/api) and [OpenAPI](/docs/developers/openapi). ## Related pages [#related-pages] - [Time](/docs/product/time) - [Expenses](/docs/product/expenses) - [Hiring](/docs/product/hiring) - [Approvals](/docs/product/approvals) - [Access control](/docs/product/access-control) # Products (/docs/product/products) The product catalog at /products, with variants, inventory levels and stock movements, suppliers, purchase orders, image lookup, and public product pages. ## Overview [#overview] Products is the organization's sellable catalog and its stock. It lives under **Resources > Products** at `/{orgSlug}/products` and has four tabs: | Tab | Route | What it shows | | --- | --- | --- | | Overview | `/products` | The product list with search, filters, sorting by name or price, and bulk actions | | Inventory | `/products/inventory` | Every stock record across products and locations | | Suppliers | `/products/suppliers` | The organization's supplier masters | | Purchase orders | `/products/purchase-orders` | Supplier orders and their receiving progress | A product opens at `/products/[productId]`, where the ID in the URL is the product's slug. The detail page has the tabs Overview, Variants (`/variants`), Inventory (`/inventory`) and Suppliers (`/suppliers`). Suppliers open at `/products/suppliers/[supplierId]` and purchase orders at `/products/purchase-orders/[purchaseOrderId]`. Products are separate from customer assets. A customer asset is equipment installed at or owned by a customer and lives on the customer record (see [Customers](/docs/product/customers)). Products use the `organization.products.*` permissions, while customer assets use `organization.assets.*`. ## Key concepts [#key-concepts] ### Products and variants [#products-and-variants] A product carries its identity (name, description, brand, categories, tags and images), codes (SKU, EAN, manufacturer part number, HS code, country of origin and external catalog ID), commercial defaults (selling price, purchase price, currency and VAT percentage) and a unit of measure such as piece, box, hour, kilogram or a custom unit. Every product has at least one variant. A simple product keeps a single default variant. Add up to three options, such as Size or Color, and **Generate variants** creates one variant per combination, up to 100. Each variant can override the SKU, price and image. An empty SKU or price falls back to the product. A variant can allow backorders so it keeps selling when stock runs out. Quote, order, invoice and task material lines always reference the exact variant. A product has a status of active, discontinued or archived, and a type: - **Physical** products ship and can track inventory. - **Digital** products are delivered as a downloadable file, a license key, an access link, or manually. Digital lines never create shipments. Not yet available: uploading digital files or loading license keys onto a product from the app or the API. ### Stock records and stock status [#stock-records-and-stock-status] Stock is tracked per variant in stock records. A stock record holds a location, an optional bin or shelf, lot number, serial number and expiration date, and a status of available, quarantined, expired or archived. Quantities are shown as: - **On hand**: what is physically in stock. - **Reserved**: what quotes, orders, invoices and tasks hold. - **Available**: on hand minus reserved. - **Incoming**: open quantities on sent purchase orders. Each variant gets a derived stock status: not tracked, discontinued, out of stock, backordered, low stock (at or below the reorder point), or in stock. The inventory strip on a product shows stock health by variant. ### Inventory movements [#inventory-movements] Every quantity change writes an inventory movement, so the stock history is auditable. Movement types are initial count, purchase receipt, sale, reservation, reservation release, reservation transfer, adjustment, transfer in, transfer out, return, write-off and cycle count. The product Inventory tab shows recent movements with the reason and the on-hand quantity after each change. ### Allocations [#allocations] Reservations are inventory allocations with a state of reserved, consumed or backordered. Sending a quote holds its tracked lines, creating an invoice from that quote moves the hold to the invoice, and sending the invoice consumes it. Confirming a sales order reserves free stock per line and backorders the rest. A shortfall never blocks sending a document; the document shows which lines are short. See [Quotes](/docs/product/quotes), [Invoices](/docs/product/invoices) and [Orders](/docs/product/orders). ### Suppliers [#suppliers] A supplier has a name, status (active, inactive or archived), business identifiers (KVK and VAT number), contact details (email, ordering email, billing email, phone and website), an address, commercial defaults (currency, payment terms, default lead time and default receive location), external IDs and internal notes. The supplier detail page also lists that supplier's purchase orders. A product can link to many suppliers. Each link stores the supplier SKU, order code, unit cost, lead time, minimum order quantity and order multiple, and one supplier per variant can be the preferred supplier that reorders go to. ### Purchase orders [#purchase-orders] A purchase order moves through these statuses: | Status | Meaning | | --- | --- | | Draft | Lines can still change | | Sent | Lines are locked and open quantities count as incoming stock | | Partially received | Some lines arrived | | Received | Everything arrived | | Closed | Closed short; the open remainder was cancelled | | Cancelled | Cancelled before anything arrived | ## Create a product [#create-a-product] 1. Open **Products** and select **New product**. 2. Fill in the identity, codes and prices, then save. 3. Open the **Variants** tab to add options, or the **Inventory** tab to add stock. To start from a photo, open **More create options** and choose **From image**. Take or upload a JPEG, PNG or WebP photo of up to 8 MB. Codes on the label, such as a barcode, are read first, and AI reads the rest. Pick the matching result and select **Create from these details** to prefill the form. Each lookup runs the product lookup workflow and counts toward the organization's workflow step allowance. The same lookup is available as **Scan image** on invoice lines. ## Track stock [#track-stock] 1. Open **Products > Inventory** or a product's **Inventory** tab and select **New stock record**. 2. Choose the product and variant, type or pick a location, and enter the initial count. 3. To change a quantity later, use **Adjust/count** from the row actions. Choose a movement type and give a reason. Editing a stock record only changes its metadata; quantities always change through an adjustment. Set a reorder point and reorder quantity per variant to get low-stock signals and purchase suggestions. Select several records to move them to another location, archive them, or delete them. ## Link suppliers [#link-suppliers] 1. Create the supplier under **Products > Suppliers > New supplier**. 2. On the product, open the **Suppliers** tab and select **Link supplier**. 3. Enter the supplier SKU, unit cost and ordering details, and mark the preferred supplier. Deleting a supplier removes its links from products. Products keep their own details. ## Order stock from suppliers [#order-stock-from-suppliers] 1. Open **Products > Purchase orders** and select **New purchase order**, then pick the supplier. You add lines on the draft. 2. Or open **Reorder suggestions** to see variants whose available plus incoming stock is at or under the reorder point. Selected lines become one draft purchase order per supplier. 3. Select **Send** to email the PDF to the supplier's ordering email. When the supplier has no ordering email, use **Mark as sent** instead. 4. When goods arrive, select **Receive** and enter the quantity per line, the location, and any lot or serial numbers. Received quantities go into stock, update the average cost, and fill waiting backorders first. Receiving more than was ordered asks for confirmation. 5. If the rest will never arrive, use **Close short** to cancel the open remainder. A purchase order with nothing received can be cancelled instead. When the supplier confirms a shipment, use **Announce delivery** on the purchase order to record an inbound shipment, then receive it into stock when it arrives. ## Print and share [#print-and-share] The product actions offer: - **Print QR stickers**: a printable sticker sheet. Select several products in the list to print stickers for up to 100 at once. - **Print product page**: a one-page PDF with the image, brand, identifiers, price and a QR code. - **Open public page** and **Copy public link**: the public product page at `/{orgSlug}/portal/products/[productId]`, which anyone with the link can open, including guests. It uses a fixed layout and does not read a template. The product list and the suppliers list can be exported. Bulk actions on products are archive, delete, assign a tag and clear tags. A product with linked inventory cannot be deleted until those stock records are deleted; archive it instead. ## Permissions [#permissions] | Key | Grants | | --- | --- | | `product.read` | See products, variants, inventory and the public link actions | | `product.create` | Create products and stock records, and start an image lookup for a new product | | `product.update` | Edit products, variants, images, supplier links, reorder rules and stock adjustments | | `product.delete` | Archive or delete products and stock records | | `supplier.read`, `.create`, `.update`, `.delete` | Manage suppliers | | `purchaseOrder.read`, `.create`, `.update`, `.delete` | Manage purchase orders and drafts | | `purchaseOrder.send` | Send a purchase order or mark it as sent | | `purchaseOrder.receive` | Receive goods and close a purchase order short | Brands and categories have their own keys and are managed under [Content](/docs/product/content). Customer assets use `organization.assets.*`. See [Access control](/docs/product/access-control) for roles. ## Automation and AI [#automation-and-ai] - Stock changes publish `inventory.level-changed`, `inventory.low-stock`, `inventory.out-of-stock` and `inventory.back-in-stock` events through the `inventory_signals` background job, which a variant's first stock movement queues for 30 seconds later, so a burst of movements publishes once. Workflows and outgoing webhooks can react to them. - A daily background job flags sent purchase orders past their expected date with `purchase-order.overdue`. - Purchase orders follow the default purchase-order lifecycle: sent, confirmed, in transit, received and billed. - Starter automations include **Low stock reorder** (draft and send a purchase order when stock runs low), **Purchase order overdue** (remind the supplier) and **Back in stock** (fill backorders when stock arrives). Add them from a template's Workflow tab under [Content](/docs/product/content). - The assistant can create, update, archive and delete products, manage variants, supplier links and stock records, and work with purchase orders. See [Chat](/docs/product/chat). - MCP tools: `listProducts`, `getProduct`, `createProduct`, `updateProduct`, `listInventoryLevels`, `listPurchaseOrders`, `getPurchaseOrder`, `createPurchaseOrder` and `receivePurchaseOrder`. See [MCP](/docs/developers/mcp). Receiving a purchase order through the API returns `409 APPROVAL_REQUIRED` with an `approvalId` when an approval policy applies. See [Approvals](/docs/product/approvals). ## API [#api] All paths are under `/api/v1/organizations/{organizationId}`. | Resource | Operations | | --- | --- | | `/products` | [List](/docs/openapi/listProducts), [create](/docs/openapi/createProduct), [get](/docs/openapi/getProduct), [update](/docs/openapi/updateProduct), [delete](/docs/openapi/deleteProduct) | | `/products/{productId}/suppliers` | [Add](/docs/openapi/addProductSupplierLink), [update](/docs/openapi/updateProductSupplierLink) and [remove](/docs/openapi/removeProductSupplierLink) supplier links | | `/inventory/levels` | [List levels](/docs/openapi/listInventoryLevels) for webshop stock sync | | `/inventory/stock-records` | [List](/docs/openapi/listInventoryStockRecords), [create](/docs/openapi/createInventoryStockRecord), [update](/docs/openapi/updateInventoryStockRecord), [archive](/docs/openapi/archiveInventoryStockRecord) | | `/inventory/stock-adjustments` | [Adjust stock](/docs/openapi/adjustInventoryStock) | | `/inventory/locations` | [List](/docs/openapi/listInventoryLocations) and [create](/docs/openapi/createInventoryLocation) locations | | `/inventory/movements` | [List movements](/docs/openapi/listInventoryMovements) | | `/inventory/allocations` | [List allocations](/docs/openapi/listInventoryAllocations) | | `/suppliers` | [List](/docs/openapi/listSuppliers), [create](/docs/openapi/createSupplier), [get](/docs/openapi/getSupplier), [update](/docs/openapi/updateSupplier), [delete](/docs/openapi/deleteSupplier) | | `/purchase-orders` | [List](/docs/openapi/listPurchaseOrders), [create](/docs/openapi/createPurchaseOrder), [get](/docs/openapi/getPurchaseOrder), [receive](/docs/openapi/receivePurchaseOrder) | ## Related pages [#related-pages] - [Content](/docs/product/content) for brands, categories, tags and templates - [Orders](/docs/product/orders) for sales orders, shipments and promotions - [Quotes](/docs/product/quotes) and [Invoices](/docs/product/invoices) for stock holds on documents - [Customers](/docs/product/customers) for customer assets - [Customer portal](/docs/product/customer-portal) - [Workflows](/docs/product/workflows) - [API](/docs/developers/api) and [Webhooks](/docs/developers/webhooks) # Profile (/docs/product/profile) How to manage your personal details, language, regional formats, theme, sign-in methods and notification channels, and which account pages live outside an organization. ## Overview [#overview] Your profile holds the settings that belong to you rather than to an organization. Open it at `/profile` or through Account in the user menu at the bottom of the sidebar. The same profile applies in every organization you belong to. The profile has four tabs: Info, Authentication, Communication and Connected apps. Other pages outside an organization: | Route | Purpose | | --- | --- | | `/organizations` | Your organizations. See [Organizations](/docs/product/organizations). | | `/organizations/new` | Create an organization. | ## Key concepts [#key-concepts] ### User menu [#user-menu] The user menu has Homepage, Docs, Changelog, Account and Log out. To switch between an organization, its customer portal and platform administration, use the organization switcher at the top of the sidebar. ### Regional preferences [#regional-preferences] Your timezone and formats decide how dates and times are shown to you across the app. They do not change what other users see. ## Edit your profile [#edit-your-profile] ### Info [#info] | Section | Settings | | --- | --- | | Personal information | Photo, first name, last name and username. | | Language | English, Nederlands or Deutsch. | | Regional preferences | Timezone, week starts on Monday or Sunday, date format and time format (24-hour or 12-hour). | | Appearance | System, Light or Dark theme. The theme is saved on this device only. | | Celebrations | Show my birthday and Show my work anniversary. Only shown when you are an employee in [People](/docs/product/people). | Select your photo or its change button to upload a JPEG, PNG or WebP image under 5 MB. You can crop, rotate and zoom before saving. A username has 3 to 30 characters: lowercase letters, numbers and underscores. Sign-up picks one from your first and last name (`ada_lovelace`), or from your email address, and adds a number when the name is taken or reserved (`ada_lovelace1`). Date formats are dd/MM/yyyy, dd-MM-yyyy, dd.MM.yyyy, MM/dd/yyyy and yyyy-MM-dd. ### Authentication [#authentication] The Sign-in methods table lists Email and Google: - Email: add a password, or update it by entering your current password and the new one twice. - Google: shows Linked when your Google account is connected. Select Use Google to connect it. The API keys section below it lists your personal API keys. Select Create API key, give it a name, choose Read only or Read and write, all your organizations or one, and when it expires. The key is shown once; copy it then. Rotate replaces a key with a new one (the old one keeps working for a day), and Revoke stops it at once. A key acts as you and never has more access than you, and a read-only key cannot change data even where you can. See [Authentication](/docs/developers/authentication) for when the API accepts keys. ### Communication [#communication] The Channels table turns notification delivery on or off for In-app, Email and Push. WhatsApp is marked Coming soon. See [Notifications](/docs/product/notifications) for what each channel delivers today. ### Connected apps [#connected-apps] The Connected apps tab lists the apps and AI clients you allowed to act on your behalf through OAuth, such as an MCP client, with the scopes you granted and the date you allowed them. Select Revoke to sign an app out and remove its access; it has to ask for consent again to reconnect. See [MCP](/docs/developers/mcp). ### Export your data [#export-your-data] The Info tab has an Export your data section. Select Export data to collect your profile, memberships, comments, attachments you uploaded and other personal data as CSV files. When the export is ready, select Download to get a link per file. The files stay available for seven days. ### Organizations scheduled for deletion [#organizations-scheduled-for-deletion] When an organization you belong to is scheduled for deletion, the Info tab lists it with the date its data is deleted permanently. The owner or the person who requested the deletion can select Cancel deletion until that date, which enables the organization again. See [Organizations](/docs/product/organizations). ### Delete your account [#delete-your-account] At the bottom of the Info tab, select Delete account and type the confirmation text. Deleting your account removes your access to every organization and deletes your profile picture. While you are the owner of an organization, transfer its ownership or delete the organization first. See [Organizations](/docs/product/organizations). ## Automation and AI [#automation-and-ai] The assistant can read your contact details and update your profile, contact details and contact methods. Changes ask for approval based on their risk. See [Chat](/docs/product/chat). ## API [#api] | Method and path | Purpose | | --- | --- | | `GET /api/v1/users/me` | Read the signed-in user. Also available as an MCP tool. | | `PATCH /api/v1/users/me/active-organization` | Set your preferred organization. | See [API](/docs/developers/api), [Authentication](/docs/developers/authentication) and [MCP](/docs/developers/mcp). ## Related pages [#related-pages] - [Notifications](/docs/product/notifications) - [Organizations](/docs/product/organizations) - [Access control](/docs/product/access-control) # Quotes (/docs/product/quotes) How quotes work at /quotes, from the pipeline board and versioned sending to customer acceptance, deposits, PDFs, and turning a quote into an invoice, order or task. ## Overview [#overview] Quotes are priced offers to a customer. They live under **Finance > Quotes** at `/{orgSlug}/quotes`, which has two tabs: - **Pipeline** (`/quotes`): a board with one column per status. Open quotes always show. Accepted, rejected, expired and withdrawn quotes default to those closed since last month; switch the period to All time to see older ones. - **All quotes** (`/quotes/all`): a table with search, filters, sorting and bulk actions. A drafts-only table is also available at `/quotes/drafts`. New quotes start at `/quotes/create`, and a quote opens at `/quotes/[quoteId]`, where the ID in the URL is the quote number. The quote detail page is a branded document preview with the letterhead, dates, lines, totals and closing text. The email message, internal notes and linked records sit below the document. ## Key concepts [#key-concepts] ### Statuses [#statuses] | Status | Meaning | | --- | --- | | Draft | Not sent yet. Everything stays editable, including the customer. | | Sent | Published to the customer, who can accept, reject or request changes. | | Changes requested | The customer asked for changes with a comment. | | Expired | The valid-until date passed without a decision. | | Accepted | The customer or staff accepted the quote. It is locked. | | Rejected | The customer declined with a reason, or staff marked it rejected. | | Withdrawn | Staff withdrew a live quote so the customer can no longer accept it. | On the board you can drag a draft to Sent, and a sent quote to Accepted or Rejected. A quote with changes requested, an expired quote, or a rejected or withdrawn quote can go back to Sent, which publishes a new version; a quote with changes requested or an expired quote can also move to Rejected. A quote with changes requested cannot be marked accepted, because the customer is waiting for a revised version. Accepted quotes do not move. Before a quote that was sent earlier goes out again, a confirmation shows the recipient, the new valid-until date and an optional message to the customer, and Cancel leaves the quote where it was. ### Versions [#versions] Sending publishes version 1 on the same quote number and URL. After that, edits stay private until you select **Send new version**, which publishes the next version. Customers see the version history (v1, v2 and so on), with the latest published version as current. The customer is locked once the quote is sent. Accepting an older version than the latest published one is refused. ### Lines and pricing [#lines-and-pricing] A quote has a title, introduction, closing text and lines. Line types are section headers, products, time, fixed price, subscription (weekly, monthly, quarterly or yearly, with a number of billing periods) and custom lines. A document discount can be a percentage or a fixed amount, and a [promotion](/docs/product/orders) can supply it. Prices can be shown including or excluding VAT. VAT rates come from the organization's presets plus Custom, and totals show VAT per rate. The VAT regime can be standard, reverse charge, exempt, KOR or margin scheme. Reverse charge requires the customer's VAT number before sending. ### Options [#options] The **Options** panel holds the workflow, validity, currency, VAT and deposit settings: - **Valid until** comes from the template's default validity in days. Sending needs a valid-until date at least two days from now. - **Deposit** can be none, a percentage of the total, or a fixed amount. With **Send the deposit invoice as soon as the customer accepts online**, accepting the quote in the customer portal issues the deposit invoice right away. The document totals show the deposit amount under the total. Quote templates add **Create an order when accepted**, which turns the accepted lines of quotes from that template into a confirmed sales order, so stock is reserved and fulfillment can start. ### Lifecycle [#lifecycle] The **Quote progress** strip shows the lifecycle stages. The default quote lifecycle runs Sent, Viewed, Accepted and Invoiced, with branch stages for Changes requested, Deposit paid, Rejected, Expired and Withdrawn. It sends the quote email, records the first customer view, reminds the customer before expiry, marks the quote expired, and rejects an expired quote automatically after 30 days unless a new version is sent. An accepted quote reaches Invoiced only once an invoice for it exists: an invoice (not a deposit invoice and not cancelled) made from the quote or linked to it, or an invoice for the order made from the quote. Until then it stays at Accepted, or at Deposit paid when the quote has a deposit. Stages that the lifecycle marks as manual can be moved by hand. Templates can point a quote at a different lifecycle. See [Content](/docs/product/content). ## Create a quote [#create-a-quote] 1. Open **Quotes** and select **New quote**, or open **More create options** and choose **From template**. 2. Select the customer, or type a name to create one. Pick an optional contact. 3. Edit the title, introduction, lines and closing on the document, and set dates and options. 4. Write the email message that goes with the quote. It stays off the PDF. 5. Save the draft. Creating a quote from a customer or asset page prefills that customer and returns you to the page after saving. ## Send a quote [#send-a-quote] Select **Send** on a draft. Sending checks that the customer has a billing or contact email, a valid VAT number when reverse charge applies, and a valid-until date far enough ahead. The send email attaches the quote PDF, plus the terms and conditions PDF when the template sets one. When an approval policy applies, sending notifies the approvers and you send again once the quote is approved. See [Approvals](/docs/product/approvals). Sending holds stock for tracked product lines until the quote expires, is rejected or withdrawn, or you select **Release stock**. A shortfall never blocks sending; the dialog lists the lines that are short. See [Products](/docs/product/products). You can also select several quotes in **All quotes** and send them together. ## Handle the customer's response [#handle-the-customers-response] The customer opens the quote at `/{orgSlug}/portal/quotes/[quoteId]`, including as a guest, and can download the PDF there. From the portal they can: - **Accept**, or **Accept and pay deposit** when the quote has a deposit. - **Reject** with a reason. - **Request changes** with a comment describing what should change. Staff can also select **Mark accepted** or **Mark rejected** with an optional reason. To revise a quote after changes were requested, or after it was rejected, expired or withdrawn, edit it and select **Send new version**. To stop a sent, expired or changes-requested quote from being accepted, select **Withdraw**. A quote with changes requested shows the customer's latest comment, name and date above the document. See [Customer portal](/docs/product/customer-portal) for the customer's view. ## After acceptance [#after-acceptance] Accepting a quote emails the customer, notifies the team, creates a draft invoice and opens a follow-up task. It never sends the invoice. The draft invoice moves the quote to Invoiced; when the Quote accepted automation is off or the quote has no template, the quote moves to Invoiced when you create the invoice yourself. With a deposit, accepting creates the deposit invoice, and the final invoice deducts the deposit per VAT rate. When the quote template has **Create an order when accepted** switched on, a confirmed sales order is created instead and invoicing follows the order's policy. On an accepted quote you can also: - **Create task**, once per quote, for the work the quote agreed. - **Create invoice**, which opens a new invoice prefilled from the quote. See [Invoices](/docs/product/invoices). - **Create order** or **Open order**. See [Orders](/docs/product/orders). ## Notes and linked records [#notes-and-linked-records] Notes are an internal thread that stays off the PDF and the customer-facing quote. Mention users and teams with `@`, or work and customer records with `#`. Notes stay editable after sending, and a new version of the quote or an invoice made from it starts with a copy of the thread. Adding notes needs `comment.create`, only the author edits a note, and deleting another member's note needs `comment.moderate`. The **Tasks and invoices** section shows the work that carries out the quote and what has been billed for it. Use **Link tasks** to connect existing tasks. Customer assets can be linked to a quote through the API or from the asset on the customer record. ## Other actions [#other-actions] The **Other** menu and the row actions offer **Show online** (the customer view), **Duplicate**, **Download PDF** and **Delete**. Duplicate creates a new draft with the same lines, template, deposit and workflow settings, so the template's automations run for the copy. Deleting a draft removes it permanently; other quotes are hidden from lists. Bulk actions in **All quotes** are send, mark accepted, mark rejected and delete. Export the filtered list as CSV, Excel or PDFs. ## Permissions [#permissions] | Key | Grants | | --- | --- | | `quote.read` | See quotes and download PDFs | | `quote.create` | Create quotes | | `quote.update` | Edit quotes, linked tasks and stock holds, and create a task from a quote | | `quote.delete` | Delete or hide quotes | | `quote.send` | Send a quote or a new version | | `quote.markAccepted` | Mark a quote accepted | | `quote.markRejected` | Mark a quote rejected | | `quote.withdraw` | Withdraw a sent or expired quote | | `quote.duplicate` | Duplicate a quote | | `quote.approve` | Decide on quote approval requests | Creating a task from a quote also needs `task.create`, and creating a customer inline needs `customer.create`. See [Access control](/docs/product/access-control). ## Automation and AI [#automation-and-ai] - Quote events such as `quote.sent`, `quote.viewed`, `quote.accepted`, `quote.rejected`, `quote.changes-requested`, `quote.expired`, `quote.withdrawn`, `quote.deposit-paid` and `quote.invoiced` can trigger [workflows](/docs/product/workflows) and outgoing [webhooks](/docs/developers/webhooks). - Starter automations for quote templates include **Big-ticket quote** (tell the team when a quote over 10,000 goes out) and **Viewed, not accepted** (follow up when a viewed quote has no decision after 3 days). - The assistant can create, update, send, accept, reject, duplicate and archive quotes, manage notes and links, and move lifecycle stages. See [Chat](/docs/product/chat). - MCP tools: `listQuotes`, `getQuote`, `createQuote` and `updateQuote`. See [MCP](/docs/developers/mcp). ## API [#api] All paths are under `/api/v1/organizations/{organizationId}`. | Resource | Operations | | --- | --- | | `/quotes` | [List](/docs/openapi/listQuotes), [create](/docs/openapi/createQuote), [get](/docs/openapi/getQuote), [update](/docs/openapi/updateQuote), [delete](/docs/openapi/deleteQuote) | | `/quotes/{quoteId}/send` | [Send](/docs/openapi/sendQuote) | | `/quotes/{quoteId}/accept` and `/reject` | [Accept](/docs/openapi/acceptQuote) and [reject](/docs/openapi/rejectQuote) | | `/quotes/{quoteId}/duplicate` | [Duplicate](/docs/openapi/duplicateQuote) | | `/quotes/{quoteId}/notes` | [List](/docs/openapi/listQuoteNotes), [add](/docs/openapi/addQuoteNote), [update](/docs/openapi/updateQuoteNote), [delete](/docs/openapi/deleteQuoteNote) | | `/quotes/{quoteId}/tasks` and `/assets` | [Set linked tasks](/docs/openapi/setQuoteTasks) and [set linked assets](/docs/openapi/setQuoteAssets) | | `/quotes/{quoteId}/lifecycle` | [Get lifecycle](/docs/openapi/getQuoteLifecycle) and [advance a stage](/docs/openapi/advanceQuoteLifecycle) | The customer's change request goes through the public portal endpoint [`requestPortalQuoteChanges`](/docs/openapi/requestPortalQuoteChanges). ## Related pages [#related-pages] - [Invoices](/docs/product/invoices) - [Orders](/docs/product/orders) - [Products](/docs/product/products) - [Content](/docs/product/content) for quote templates and lifecycles - [Customer portal](/docs/product/customer-portal) - [Approvals](/docs/product/approvals) - [Customers](/docs/product/customers) and [Tasks](/docs/product/tasks) # Settings and branding (/docs/product/settings-and-branding) The organization settings page, covering general details, company information, social links, finance defaults, email addresses, export formats, lifecycles, logo, colors and brand images. ## Overview [#overview] Organization settings live at `/{orgSlug}/settings`, under Organization, Account, Settings in the sidebar. One page holds every organization-wide default, in sections you save one at a time. The organization's AI settings have their own page at `/{orgSlug}/ai/settings`. See [Chat](/docs/product/chat). ## Key concepts [#key-concepts] Settings belong to the organization, not to a team or a user. Teams are collaboration groups and do not carry their own finance or inbox defaults. Logos and brand images are stored as uploaded files and shown wherever the organization appears, such as the sidebar, quote and invoice documents, emails and the [customer portal](/docs/product/customer-portal). ## Settings sections [#settings-sections] ### General [#general] Name, slug and website. Changing the slug changes every URL of the organization, and old links stop working. ### Company information [#company-information] Address, Chamber of Commerce (KVK) number, VAT number (BTW), IBAN and phone. These details are printed on invoices and official correspondence. Once online payments are activated, the name, address, KVK number, VAT number, phone, Reply-to address and website also appear on the organization's public business page at `/{orgSlug}/portal/about`. Before that, the page returns not found. The IBAN does not. The page needs no sign-in and shows the details that iDEAL requires on a business website. Select **View business page** under the fields to open it. ### Socials [#socials] Facebook, Instagram, LinkedIn, X and YouTube links. They are shown to users in the portal. ### Finance [#finance] | Setting | Purpose | | --- | --- | | Base currency | The currency for quotes, invoices and reports. | | Default hourly rate | Used for time and labor lines. | | Quote validity days | How long a new quote stays valid. | | Fiscal year start | The month and day the fiscal year starts. | | Task, quote and invoice number prefix | The prefix of each document number. | | Invoice number format | The pattern for invoice numbers. | | VAT rates | The rates available on documents. Exactly one rate is the default. | ### Email [#email] The Inbox address, the Forwarding address and the Reply-to address that the [Inbox](/docs/product/inbox) uses for customer email. ### Other settings [#other-settings] Operating country, default export format (CSV or Excel workbook) and export date format (ISO or Localized). The operating country also sets the language of customer emails, such as quotes, invoices, reminders and portal emails, and of the PDFs attached to them. An organization in Germany, Austria or Liechtenstein sends them in German. An organization in the Netherlands, Belgium or another Dutch-speaking country sends them in Dutch. Switzerland and Luxembourg stay in English. Every other organization sends them in English. Without an operating country, the country of the organization address is used. The same language applies to the starter invoice and quote templates and to the default message of the invoice email that the invoice lifecycle sends. A message you wrote yourself is sent as written. ### Lifecycles [#lifecycles] The lifecycle used for invoices, quotes, tasks, orders, purchase orders and shipments. Each one uses the Platform default unless you choose another lifecycle. ### Branding [#branding] | Item | Rules | | --------------- | ----------------------------------- | | Logo | PNG, JPEG or WebP, up to 5 MB. | | Primary color | Hex color. | | Secondary color | Hex color. | | Brand images | PNG, JPEG or WebP, up to 8 MB each. | Organizations have a primary and a secondary color only. Each uploaded image is stored once, and the app resizes it when it is shown. ### Ownership and deletion [#ownership-and-deletion] The last section has Transfer ownership and Delete organization, followed by Export organization data. See [Organizations](/docs/product/organizations). ## Edit settings [#edit-settings] 1. Open Settings in the sidebar. 2. Change the fields in a section. 3. Save that section. Other sections keep their saved values. When you leave the KVK or VAT number field, CentraKit looks the number up in the official registry (KvK for KVK numbers, VIES for VAT numbers) and shows Verified or Not found next to the field. The check never blocks saving. ## Permissions [#permissions] | Permission | Allows | | --- | --- | | `organization.update` | Open and edit the settings page, including branding. | | `branding.manage` | Upload and remove the logo and brand images through the API. | | `organization.transferOwnership` | Transfer ownership. | | `organization.delete` | Schedule the organization's deletion. | | `organization.export` | Export the organization's data. | ## Automation and AI [#automation-and-ai] The assistant can read the organization and propose changes to its settings. Every change asks for approval first. See [Chat](/docs/product/chat). ## API [#api] | Method and path | Purpose | | --- | --- | | `GET /api/v1/organizations/{organizationId}` | Read the organization with its settings. | | `PATCH /api/v1/organizations/{organizationId}` | Update settings. | | `POST /api/v1/organizations/{organizationId}/branding/logo` | Upload the logo. | | `DELETE /api/v1/organizations/{organizationId}/branding/logo` | Remove the logo. | | `POST /api/v1/organizations/{organizationId}/branding/brand-images` | Upload a brand image. | | `DELETE /api/v1/organizations/{organizationId}/branding/brand-images/{brandImageId}` | Remove a brand image. | See [API](/docs/developers/api) and [OpenAPI](/docs/developers/openapi). ## Related pages [#related-pages] - [Organizations](/docs/product/organizations) - [Quotes](/docs/product/quotes) - [Invoices](/docs/product/invoices) - [Content](/docs/product/content) - [Customer portal](/docs/product/customer-portal) # Tasks (/docs/product/tasks) How to plan, track, bill and finish work with tasks, including the board, subtasks, materials, time entries, comments and the task lifecycle. ## Overview [#overview] Tasks are the organization's units of work. A task can belong to a customer and one of their assets, link to quotes and invoices, carry the materials and hours spent on it, and end in an invoice through the finish flow. Tasks live under Workspace, Work, Tasks in the sidebar. The routes are: | Route | Purpose | | --- | --- | | `/{orgSlug}/tasks` | Board: a Kanban board with one column per status. | | `/{orgSlug}/tasks/all` | All tasks: a searchable, filterable table. | | `/{orgSlug}/tasks/mine` | My tasks: open tasks assigned to you, and the top-level tasks of subtasks assigned to you. | | `/{orgSlug}/tasks/create` | Create a task. | | `/{orgSlug}/tasks/{taskId}` | Task detail. | ## Key concepts [#key-concepts] ### Status and priority [#status-and-priority] | Status | Meaning | | ----------- | ---------------------------------------------------- | | Todo | Not started. | | In progress | Work has started. | | On hold | Paused. Putting a task on hold pauses its lifecycle. | | Done | Finished. | Priority is Low, Medium, High or Urgent. Lists sort open work before done work, then by nearest due date and highest priority, unless you sort on a column yourself. Tasks can also be archived. Archived tasks stay available for reference but disappear from active lists. The All tasks filters can include done and archived tasks again. ### Task fields [#task-fields] - Title, description, status, priority and tags. - Customer and, optionally, one of that customer's assets. You can create a customer or asset inline from the pickers. - Assignees, start and end time, and due date. The form warns when the planned end is after the due date. - Required skills, which Planning uses to flag people who lack them. - Linked quotes and invoices for the same customer. Each task gets a per-organization number, shown as `#{number}`. ### Subtasks [#subtasks] Subtasks are child tasks of a root task. They have their own status and can be reordered, edited and deleted from the Subtasks section. Boards and lists show root tasks only, with a `{done}/{total} done` progress count. Generate subtasks asks AI for suggestions based on the task, and you accept or discard each one. ### Materials [#materials] Materials are products used on the task. Each line has a quantity, a unit that follows the product, a read-only unit price, a billable flag and a stock status: | Stock status | When it applies | | --- | --- | | Reserved | The product tracks stock and the quantity is held for this task. Reserving more than is available is allowed; the shortfall is backordered. | | Consumed | The quantity has been taken out of stock. | | Untracked | The product does not track stock. | A new material starts as Reserved for tracked products and Untracked for others. Removing a material releases any reserved stock. When the task links a sent or accepted quote, you can add the quote's products and take over the stock the quote still holds. The section flags products that are over the quoted quantity or not on the quote, and offers Order at supplier for products with a preferred supplier. Suggest materials asks AI to propose lines from your product catalog. See [Products](/docs/product/products) for stock tracking and [Quotes](/docs/product/quotes) for quote holds. ### Time entries [#time-entries] The Time tracking section logs hours and minutes per user with an optional note and a billable flag. Time reserved on [Planning](/docs/product/agenda) appears as a draft entry until you confirm or dismiss it. When a quote is linked, the section compares logged hours with quoted hours. Task time also appears in [Time](/docs/product/time). Time entries and materials are billed per line. Once an invoice bills a line, it is locked and shows a billed icon that names the invoice. ### Comments [#comments] Comments use a rich text composer. Type `@` to mention users or teams and `#` to reference work and customer records. Mentioned users, and active members of mentioned teams, receive a notification. See [Notifications](/docs/product/notifications). ## Work with tasks [#work-with-tasks] ### Create a task [#create-a-task] Select New task on the board or the All tasks page, or pick a task template from the From template menu next to it. Templates are managed under [Content](/docs/product/content). Assistants and integrations can also create tasks through the API and MCP. ### Use the board [#use-the-board] The board shows every open task plus tasks completed since the start of last month. Switch the period to All time to see older completed work. Drag a card to another column to change its status, or within a column to change its manual order. Select several cards to change status, priority, assignees or tags in bulk, or to archive or delete them. ### Finish a task [#finish-a-task] Select Finish task on the detail page, choose Done as the status in the task form, or drag the card to Done on the board. All three open the same checks dialog: - Hours logged, with a warning when they exceed the hours on a linked accepted quote. - Materials added, with an option to add the quote materials that are still waiting on the task. - Quote lines the task cannot bill, such as transport, so you can add them to the invoice yourself. - All subtasks done. - A customer linked, which is required to invoice. - Invoices already linked. Depending on what is still unbilled, the dialog offers one of these actions: - Finish and create invoice opens a new draft invoice for the customer with the unbilled hours and materials of the task and its done subtasks. - Finish and add to invoice appends them to a linked draft invoice. - Finish and invoice remaining items bills what linked sent invoices left over. - Finish anyway closes the task without invoicing. For a task linked to an accepted quote, the invoice bills quoted hours and products at the quote's price and discount, and anything beyond the quote at the standard price. Hours added after a task is reopened join its existing line on the same draft invoice. Finishing marks the task Done and consumes its materials. A done task accepts no new materials or time until it is reopened. Remaining quoted hours show as draft time entries on the task until you confirm or dismiss them. When a quote that was invoiced has no open tasks left, it is archived off the quotes board. See [Invoices](/docs/product/invoices). ### Archive or delete [#archive-or-delete] The Danger zone section archives a task or deletes it permanently with its assignees and comments. Deleting a subtask also deletes its related data. ## Permissions [#permissions] | Key | Grants | | --- | --- | | `task.read` | View the board, lists, task detail and the lifecycle. | | `task.create` | Create tasks. | | `task.update` | Edit tasks, status, order, assignees, subtasks, materials and time entries, and finish tasks. | | `task.delete` | Archive and delete tasks. | | `comment.read` | Read the comments on tasks the member can see. | | `comment.create` | Add comments, and edit or delete their own. | | `comment.moderate` | Delete other members' comments. | Finishing with a new invoice also requires `invoice.create`, and appending to a draft requires `invoice.update`. Creating a customer or asset from the task form requires `customer.create` or `asset.create`. See [Access control](/docs/product/access-control). ## Automation and AI [#automation-and-ai] Every root task runs the Task lifecycle from the moment it is created. It tracks In progress, On hold and Done, and the progress shows in the status section on the detail page. The task Options popover shows the lifecycle workflow, which you can change only while the task is Todo. See [Workflows](/docs/product/workflows). Automatic workflows email assignees when they are added to a task, remind them when a task is due within 24 hours, and remind assignees and the reporter when it is overdue. MCP tools for tasks: | Tool | Kind | | --------------- | ----- | | `listTasks` | Read | | `getTask` | Read | | `createTask` | Write | | `updateTask` | Write | | `setTaskStatus` | Write | AI also powers Generate subtasks and Suggest materials on the detail page. See [MCP](/docs/developers/mcp) and [AI agents](/docs/developers/ai-agents). ## API [#api] All paths are relative to `/api/v1/organizations/{organizationId}`. | Resource | Operations | | --- | --- | | `tasks` | `listTasks` (GET), `createTask` (POST) | | `tasks/{taskId}` | `getTask` (GET), `updateTask` (PATCH), `deleteTask` (DELETE) | | `tasks/{taskId}/status` | `setTaskStatus` (PATCH) | | `tasks/{taskId}/assignees` | `setTaskAssignees` (PUT) | | `tasks/{taskId}/comments` | `listTaskComments` (GET), `addTaskComment` (POST) | | `tasks/{taskId}/comments/{commentId}` | `updateTaskComment` (PATCH), `deleteTaskComment` (DELETE) | | `tasks/{taskId}/time-entries` | `listTaskTimeEntries` (GET), `addTaskTimeEntry` (POST) | | `tasks/{taskId}/time-entries/{entryId}` | `deleteTaskTimeEntry` (DELETE) | | `tasks/{taskId}/lifecycle` | `getTaskLifecycle` (GET) | | `tasks/{taskId}/lifecycle/advance` | `advanceTaskLifecycle` (POST) | | `tasks/reorder` | `reorderTasks` (POST) | | `quotes/{quoteId}/tasks` | `setQuoteTasks` (PUT) | `listTasks` filters by status, priority, due date bucket, assignee, customer and tag, and supports `q`, `limit` and `offset`. Materials and subtask editing are not exposed through the REST API. See [API](/docs/developers/api) and [OpenAPI](/docs/developers/openapi) for request and response schemas. ## Related pages [#related-pages] - [Agenda](/docs/product/agenda) for Planning and scheduled work. - [Quotes](/docs/product/quotes) and [Invoices](/docs/product/invoices) for the commercial side of a task. - [Customers](/docs/product/customers) for customers and their assets. - [Workflows](/docs/product/workflows) for the Task lifecycle. # Time (/docs/product/time) How to log hours with the weekly timesheet, timer and clock in and out, submit and approve timesheets, request and approve time off, report sick, and set up leave types, policies and blackout periods. ## Overview [#overview] Time covers the hours people work and the hours they are away. Employees log work on a weekly timesheet, run a timer, clock in and out, request time off and report sick. Managers approve timesheets and leave for the people who report to them. Everything is counted in hours against each employee's working hours from [People](/docs/product/people). Time lives under Workspace, People, Time in the sidebar. | Route | Purpose | | --- | --- | | `/{orgSlug}/time` | My timesheet tab: your week, timer, attendance and suggestions. | | `/{orgSlug}/time/team` | Team tab: timesheets awaiting your approval and the team week. | | `/{orgSlug}/time/time-off` | My time off tab: your balances and requests. | | `/{orgSlug}/time/time-off/calendar` | Team calendar of approved and pending leave. | | `/{orgSlug}/time/time-off/requests` | Requests tab: leave awaiting your decision, sick reports and all requests. | | `/{orgSlug}/time/time-off/new` | Request time off. | | `/{orgSlug}/time/time-off/{requestId}` | One time-off request. | | `/{orgSlug}/time/timesheets/{timesheetId}` | One employee's timesheet week. | You need an employee profile to track time or request time off. ## Key concepts [#key-concepts] ### Time entries [#time-entries] A time entry is a number of hours on a day, logged against a task, a customer or general internal work. Entries come from the timesheet grid, the timer, agenda and planning items, or accepted suggestions. Billable time is rounded according to the rounding rule in HR settings. ### Timesheets [#timesheets] A timesheet is one employee's week. Its status is Open, Submitted, Approved or Rejected. The week shows logged hours against expected hours from the working schedule, the running overtime balance and clocked attendance hours. Submitted and approved weeks are locked, so new entries for those days are rejected. When timesheet approval is off in HR settings, employees close their week instead of submitting it. ### Time off [#time-off] Time off is booked in hours from leave types and policies: | Concept | Meaning | | --- | --- | | Leave type | A kind of leave, such as vacation, sick leave, special leave, unpaid leave, parental leave or compensatory time. A type is paid or unpaid and does or does not use a balance. | | Leave policy | A yearly entitlement in hours for a leave type, granted up front or monthly. Part-timers get a prorated share from their contract hours. Carried-over hours can expire after a set number of months. | | Balance | Entitled, taken, planned and awaiting-approval hours per leave type, with a ledger of every grant, accrual, booking, correction, carry-over and expiry. | | Blackout period | A busy period, for all teams or one team, that warns about or blocks leave requests. | A request is Pending, Approved, Rejected or Cancelled. Colleagues only see that someone is absent, never the reason. ### Sick leave [#sick-leave] Reporting sick opens a sick report with the start date and an expected return date. A report is Sick, Partially back (with the percentage worked) or closed when the person is fully back. Long reports are marked long-term. ## Track time [#track-time] ### Fill in your timesheet [#fill-in-your-timesheet] 1. Open Time. The My timesheet tab shows the current week. 2. Select **Add a task row** to add a task, or use the General row for internal work. 3. Type hours into each day. The footer shows logged against expected hours per day. 4. Select **Submit for approval** when the week is complete, or **Close week** when approval is off. Use **Withdraw** to pull back a submitted week before it is decided. A rejected week is sent back for changes. ### Use the timer [#use-the-timer] Start the timer from the Timer card, from the top bar, or from a task. Pick a task or describe the work, then stop the timer to turn the elapsed time into a time entry. Forgotten timers stop automatically after the number of minutes set in HR settings. ### Clock in and out [#clock-in-and-out] When clock in and out is on, the Attendance card records the start and end of the working day. With location recording on, a coarse location is stored and employees are told about it. A missing clock-out is closed at the scheduled end of the day. ### Accept suggestions [#accept-suggestions] "Suggested from your week" lists hours from your agenda and planning. Untick anything you did not work on and add the rest to your timesheet, or dismiss them all. ### Approve timesheets [#approve-timesheets] The Team tab lists timesheets awaiting your approval and the status of everyone in your team this week. Select several and choose **Approve** or **Deny**, or open one week to review it line by line. A decided week stays locked until it is reopened with **Reopen** on the timesheet page. Task pages show hours logged against the hours quoted on linked quotes, with a warning at the budget percentage from HR settings. See [Tasks](/docs/product/tasks). ## Take time off [#take-time-off] ### Request time off [#request-time-off] 1. Select **Request time off** on the My time off tab. 2. Choose the leave type and the first and last day. Each end can be a full day, morning, afternoon or specific hours. 3. Add an optional note for your manager and select **Send request**. The summary shows how many hours the request takes and warns when it falls in a blackout period, when a teammate is also off, when too many people in the team would be away, when planned work overlaps, when you already have leave on those dates or when the balance is too low. People with HR rights can book time off for someone else. Requests go to the employee's manager. Without a manager, they go to people with `timeOff.approve`. Whether a request needs approval follows the leave type, or the Leave requires approval setting in HR settings when the type does not override it. Sick leave never needs approval. ### Decide on requests [#decide-on-requests] The Requests tab lists leave awaiting your decision. Select several to approve or deny them at once, or open one request to add a note to the employee. Approvers are also notified in [Notifications](/docs/product/notifications). Cancelling an approved request returns its hours to the balance. ### Report sick and register a return [#report-sick-and-register-a-return] Select **Report sick** on the My time off tab, or report sick for someone else as HR. You can turn booked vacation in the sick period into sick days. On the Requests tab, **Register return** records a full return, which closes the report, or a partial return with the percentage worked. ### Subscribe to the team calendar [#subscribe-to-the-team-calendar] The Team calendar tab shows approved and pending leave per person for a month and can be filtered by team. **Subscribe** creates a private link that you can add to Google Calendar, Outlook or Apple Calendar. Anyone with the link can see the leave, so reset it if it leaks. ## Set up leave [#set-up-leave] Leave types, leave policies and blackout periods live in [HR settings](/docs/product/people) at `/{orgSlug}/people/settings`. **Grant entitlements now** adds the yearly entitlements to balances at once. **Correct balance** on an employee's Time off tab adds or removes hours with a note that is kept in the balance history. ## Permissions [#permissions] | Permission | Allows | | --- | --- | | `timeTracking.read` | Open Time and log your own hours. | | `timeTracking.create`, `.update`, `.delete` | Work with time entries through the API and AI tools. | | `timeTracking.approve` | Approve and deny timesheets. | | `timeOff.read` | Request time off, report sick and see the team calendar. Managers decide requests for their reports. | | `timeOff.create`, `.update`, `.delete` | Book, change and cancel time off through the API and AI tools. | | `timeOff.approve` | Decide requests for employees without a manager. | | `timeOff.managePolicies` | Manage leave types, policies and blackout periods, grant entitlements and correct balances. | Approvals can be delegated. See [Approvals](/docs/product/approvals). ## Automation and AI [#automation-and-ai] - A daily job runs scheduled leave accrual, follows up on sick reports, stops forgotten timers and closes forgotten clock-ins. - The assistant reads balances, leave types, requests, who is off and leave policy passages from the HR handbook Knowledge collection set in HR settings. It can request, change or cancel your own time off and report you sick. - The assistant lists HR approvals waiting for you, including leave, timesheets and expense claims, and decides them one by one after you confirm. See [Chat](/docs/product/chat). ## API [#api] All routes live under `/api/v1/organizations/{organizationId}/`: | Resource | Routes | | --- | --- | | Time entries | `GET` and `POST time-entries` | | Task time entries | `GET` and `POST tasks/{taskId}/time-entries`, `DELETE tasks/{taskId}/time-entries/{entryId}` | | Time-off requests | `GET` and `POST time-off-requests` | | HR approvals | `GET approvals` lists approvals waiting for you, `POST approvals` decides up to 100 at once. | The team leave feed is served from `GET /api/v1/public/time-off/{token}`. See [API](/docs/developers/api) and [OpenAPI](/docs/developers/openapi). ## Related pages [#related-pages] - [People](/docs/product/people) - [Expenses](/docs/product/expenses) - [Approvals](/docs/product/approvals) - [Agenda](/docs/product/agenda) - [Tasks](/docs/product/tasks) # Workflows (/docs/product/workflows) How lifecycles move quotes, invoices, tasks, orders, purchase orders and shipments through their stages, and how automations react to events, schedules and webhooks. ## Overview [#overview] Workflows run work for you. A workflow is a graph of steps that starts from a trigger, such as an event, a schedule or a webhook, and runs in the background. Events are recorded with the change that caused them and start their automations and lifecycle steps within about a minute. Page actions, such as a customer lookup, start their own workflow right away. Every run is recorded step by step, so you can see what happened and why. | Route | Purpose | | --- | --- | | `/{orgSlug}/workflows` | List of lifecycles and automations, with search and filters by status and type. | | `/{orgSlug}/workflows/create` | New workflow form. | | `/{orgSlug}/workflows/{workflowId}` | Editor tab with the canvas. | | `/{orgSlug}/workflows/{workflowId}/details` | Details tab with name, description and access. | | `/{orgSlug}/workflows/{workflowId}/runs` | Runs tab with the run history. | | `/{orgSlug}/workflows/{workflowId}/runs/{runId}` | One run with its graph, steps, input and output. | ## Key concepts [#key-concepts] ### Lifecycles and automations [#lifecycles-and-automations] A workflow has one of two purposes: - A **lifecycle** owns the status of a record. It defines the stages a quote, invoice, task, order, purchase order or shipment goes through, and the automatic steps between them, such as sending reminders. - An **automation** reacts to what happens. It runs when its trigger fires and does not own the record status. Platform workflows are marked System and Maintained by the platform. You cannot edit them directly. Select Duplicate to customize to make an organization copy; your copy replaces the platform version once you publish it. When the platform version changes later, the list shows Upstream updated and the editor offers View update. Nothing is merged automatically, so you compare and apply the changes you want. You can also Turn off for this organization any platform automation you do not want, and Turn on for this organization to restore it. ### Lifecycles [#lifecycles] Each lifecycle starts when its record reaches a starting event: | Record | Starts on | Main stages | | --- | --- | --- | | Quote | Quote sent | Sent, Viewed, Accepted, Invoiced once an invoice for the quote exists. Also changes requested, deposit paid, rejected, withdrawn and expired. | | Invoice | Invoice sent | Sent, Viewed, Paid. Also partially paid, overdue, final notice, in collection, cancelled and written off. | | Task | Task created | In progress, Done. On hold is a flag. | | Order | Order confirmed | Confirmed, Paid, Picking, Shipped or Ready for pickup, Delivered or Picked up, Invoiced, Completed. Backordered is a flag; Cancelled and Returned end the lifecycle. | | Purchase order | Purchase order sent | Sent, Confirmed, In transit, Received, Billed. Partially received is a sub-stage, Overdue is a flag and Cancelled ends the lifecycle. | | Shipment | Shipment shipped | In transit, Out for delivery or Ready for pickup, Delivered. Exception is a flag. | The platform invoice lifecycle sends payment reminders 3 days before and 7 and 14 days after the due date, and a final notice 21 days after. The platform quote lifecycle reminds the customer and rejects a quote 30 days after it expires. Emails to customers go out only during business hours, 09:00 to 17:00 on weekdays. A stage is shown as a main step, a sub-stage or a flag. Stages that end the lifecycle are marked Ends the lifecycle. Some stages advance on their own and others wait for a person. ### Which lifecycle a record uses [#which-lifecycle-a-record-uses] A record uses the first lifecycle it finds in this order: 1. The lifecycle chosen on the record itself, for tasks, quotes and invoices. 2. The lifecycle chosen on its template. 3. The organization default, set with Set as default in the workflow list or in the Lifecycles section of the organization settings. 4. The platform default. A running lifecycle can be paused, either indefinitely or until a date, with an optional reason. Delays and customer follow-ups wait while it is paused. Putting a task on hold pauses its lifecycle, and leaving hold resumes it. ### Templates and automations [#templates-and-automations] Open a template in [Content](/docs/product/content) and choose its Workflow tab to connect it to workflows: - **Lifecycle** shows the lifecycle that records from this template follow and where it comes from (chosen for this template, organization default or platform default). Customize copies it for editing, and Open in full editor opens the workflow. - **Payment reminders** on invoice templates lets you adjust the reminder days. Saving publishes the lifecycle change. - **Automations** lists automations attached to this template. Attach existing, Add from starter or New automation add one, and Detach removes it. Under Also applies you see automations that run for every template, and you can switch one off for this template. Starter automations are ready-made automations you add from a template: | Starter | What it does | | --- | --- | | Big-ticket quote | Tells the team when a quote over 10,000 goes out. | | Viewed, not accepted | Reminds the team to follow up when a quote was viewed but not decided in 3 days. | | B2C 14-day letter | Sends individual customers the 14-day letter once an invoice is overdue. | | Payment failed | Flags the invoice and alerts the team when an online payment fails. | | Low stock reorder | Drafts a purchase order when stock runs low and sends it to the supplier. Runs for all products. | | Purchase order overdue | Reminds the supplier and tells the purchaser when a purchase order is late. | | Back in stock | Fills backorders when stock arrives and tells customers their order is complete. Runs for all orders. | | Prepaid order to picking | Releases a prepaid order to picking as soon as it is paid. | | Ready for pickup | Messages the customer when an order is ready for pickup, and reminds them after 7 days. | | Delivered, then invoice | Invoices what was delivered and sends it, for orders invoiced on fulfillment. | ### Platform automations [#platform-automations] The platform also runs automations for every organization. Among them: confirming an accepted quote with a draft invoice and a follow-up task, notifying assignees when a task is assigned, due soon or overdue, following up on agenda items, welcoming new customers, flagging inbox conversations that are unanswered after 4 hours, weekly digests of overdue invoices and stale drafts, looking up customer details in the KvK register, VIES and on the web, reading product details from a photo, and alerting you when a workflow fails. ### Steps [#steps] The editor palette groups steps as follows: | Group | Steps | | --- | --- | | Triggers | Domain event, schedule, manual, workflow failed and webhook. | | Flow | Condition, delay, merge, stage, clear flag and wait for event. | | Actions | Send email, AI agent, call tool, HTTP request, webhook, approval and start workflow. | | Domain actions | Record-specific steps such as rendering a PDF, sending a reminder, creating an invoice or order, notifying assignees or looking up a company. | An approval step pauses the run until people decide. It can require any one approver, all approvers or a fixed order. ### Status and runs [#status-and-runs] A workflow is Draft, Published or Archived. Only published workflows run, and only published workflows can be archived. A run is Queued, Running, Waiting, Completed, Failed or Cancelled. Runs start from a trigger, by hand, from the API or MCP, or from a channel message. ## Create and edit a workflow [#create-and-edit-a-workflow] 1. Open Workflows and select New workflow. 2. Enter a name, a description and a key, then create it. 3. On the Editor tab, drag a trigger from the palette onto the canvas and connect steps after it. Select a step to configure it. 4. Changes save as you go. Undo, Redo, Copy, Paste, Tidy up, Import and Export are in the toolbar, and Show stages only hides everything but stages. 5. Select Publish. Problems are listed under Fix these before publishing. If someone else changes the workflow at the same time, you can Keep my changes or Load server version. To edit with the assistant, open the side panel on a workflow and describe the change. The assistant proposes graph changes that you review before they are applied. On the Details tab, Access controls who can use the workflow. Automatic means anyone with workflow permissions in this organization can view and run it. Restricted means only the teams and users you grant. ## Test and run [#test-and-run] - **Test run** runs the saved graph with every action simulated. Nothing is sent and no records change. Choose where to start from and enter a JSON trigger payload. Test runs are not billed. - **Run now** on the Runs tab starts a manual run. - **Cancel run** stops a queued, running or waiting run. It cannot be resumed. - **Restart workflow** starts a failed, cancelled or stuck run again as a new run. Steps that already sent something or recorded a stage, such as emails, notifications and stages, are not repeated. Wait steps pick up the record's events since the first run started, so a quote that was accepted in the meantime continues from accepted. The run detail page shows the graph, the list of steps with their status and attempts, and an inspector with the input and output of the selected step. When a run waits on an approval, approvers see Approve and Deny with an optional comment. ## Permissions [#permissions] | Key | Grants | | --- | --- | | `workflow.read` | View workflows, runs and run status. | | `workflow.create` | Create and duplicate workflows, and customize platform workflows. | | `workflow.update` | Edit details, access and the graph, publish, archive, turn platform automations off or on and set the default lifecycle. | | `workflow.delete` | Delete workflows and their run history. | | `workflow.run` | Start manual and test runs, cancel and restart runs and decide approvals. | Adding a starter automation to a template also needs `template.update`. Platform administrators manage platform workflows with `platform.workflow.manage`; see [Admin workflows](/docs/admin/workflows). See [Access control](/docs/product/access-control) for roles. ## Automation and AI [#automation-and-ai] The assistant can read workflows and runs and start runs through the same tools as MCP: | MCP tool | Access | Purpose | | ------------------------- | ------ | -------------------------------- | | `listWorkflowDefinitions` | Read | List workflows. | | `getWorkflowDefinition` | Read | Get one workflow with its graph. | | `listWorkflowRuns` | Read | List runs. | | `getWorkflowRun` | Read | Get one run with its steps. | | `startWorkflowRun` | Write | Start a run. | Workflow steps count toward the `workflow.steps` usage meter. See [Billing and usage](/docs/product/billing-and-usage). ## API [#api] Paths are relative to `/api/v1/organizations/{organizationId}`. | Method | Path | Operation | Permission | | --- | --- | --- | --- | | GET | `/workflows/definitions` | `listWorkflowDefinitions` | `workflow.read` | | GET | `/workflows/definitions/{definitionId}` | `getWorkflowDefinition` | `workflow.read` | | POST | `/workflows/definitions/{definitionId}/publish` | `publishWorkflowDefinition` | `workflow.update` | | POST | `/workflows/definitions/{definitionId}/runs` | `startWorkflowRun` | `workflow.run` | | GET | `/workflows/runs` | `listWorkflowRuns` | `workflow.read` | | GET | `/workflows/runs/{runId}` | `getWorkflowRun` | `workflow.read` | | POST | `/workflows/runs/{runId}/cancel` | `cancelWorkflowRun` | `workflow.run` | | POST | `/workflows/runs/{runId}/restart` | `restartWorkflowRun` | `workflow.run` | | POST | `/workflows/runs/{runId}/approval` | `decideWorkflowRunApproval` | `workflow.run` | Tasks, quotes, invoices and orders also have `GET .../lifecycle` and `POST .../lifecycle/advance` routes on the record. Purchase orders and shipments do not have lifecycle routes yet. Incoming webhooks start workflows from outside; see [Integrations](/docs/product/integrations). See [OpenAPI](/docs/developers/openapi) for schemas. ## Related pages [#related-pages] - [Operations workflows](/docs/operations/workflows) for the runtime, scheduler and retries. - [Integrations](/docs/product/integrations) for webhook endpoints and destinations. - [Quotes](/docs/product/quotes), [Invoices](/docs/product/invoices), [Tasks](/docs/product/tasks) and [Orders](/docs/product/orders). - [Approvals](/docs/product/approvals) for approval requests. # Start Google Calendar OAuth. (/docs/openapi/startGoogleCalendarOAuth) Authorize an organization integration through Vercel Connect. ## GET /v1/agenda/google-calendar/oauth/start ```json { "operationId": "startGoogleCalendarOAuth", "summary": "Start Google Calendar OAuth.", "description": "Authorize an organization integration through Vercel Connect.", "tags": [ "Agenda" ], "parameters": [ { "in": "query", "name": "orgSlug", "required": true, "schema": { "$ref": "#/components/schemas/organizationSlugSchema" }, "example": "example" } ], "responses": { "302": { "description": "Redirects to Vercel Connect / Google consent or the integration settings page.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/googleCalendarOAuthRedirectResponseSchema" } } } } } } ``` ## Referenced schemas ```json { "googleCalendarOAuthRedirectResponseSchema": { "type": "string", "format": "uri" }, "organizationSlugSchema": { "type": "string", "minLength": 2, "maxLength": 64, "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$" } } ``` # List agenda items. (/docs/openapi/listAgendaItems) Return agenda items that overlap the requested range and match the optional filters. ## GET /v1/organizations/{organizationId}/agenda/items ```json { "operationId": "listAgendaItems", "summary": "List agenda items.", "description": "Return agenda items that overlap the requested range and match the optional filters.", "tags": [ "Agenda" ], "parameters": [ { "in": "query", "name": "endAt", "required": true, "schema": { "$ref": "#/components/schemas/agendaIsoDateTime" }, "example": "example" }, { "in": "query", "name": "startAt", "required": true, "schema": { "$ref": "#/components/schemas/agendaIsoDateTime" }, "example": "example" }, { "in": "query", "name": "calendarIds", "required": true, "schema": { "type": "array", "items": { "type": "string", "format": "uuid" }, "maxItems": 100, "default": [] }, "example": "example" }, { "in": "query", "name": "q", "required": false, "schema": { "type": "string", "maxLength": 240 }, "example": "example" }, { "in": "query", "name": "userIds", "required": true, "schema": { "type": "array", "items": { "type": "string", "format": "uuid" }, "maxItems": 100, "default": [] }, "example": "example" }, { "in": "query", "name": "visibility", "required": true, "schema": { "type": "string", "enum": [ "all", "all-day", "timed" ], "default": "all" }, "example": "all" }, { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "responses": { "200": { "description": "Returns matching agenda items.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/agendaItemsHttpResponseSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "agendaAttendeeRoleSchema": { "type": "string", "enum": [ "required", "optional", "resource", "chair" ] }, "agendaAttendeeSchema": { "type": "object", "properties": { "customerContactId": { "type": [ "string", "null" ], "format": "uuid" }, "customerId": { "type": [ "string", "null" ], "format": "uuid" }, "displayName": { "type": [ "string", "null" ], "minLength": 1, "maxLength": 240 }, "email": { "type": [ "string", "null" ], "format": "email" }, "id": { "type": [ "string", "null" ], "format": "uuid" }, "isOptional": { "type": "boolean" }, "isOrganizer": { "type": "boolean" }, "participationStatus": { "$ref": "#/components/schemas/agendaParticipationStatusSchema" }, "role": { "$ref": "#/components/schemas/agendaAttendeeRoleSchema" }, "userId": { "type": [ "string", "null" ], "format": "uuid" } }, "required": [ "customerContactId", "customerId", "displayName", "email", "id", "isOptional", "isOrganizer", "participationStatus", "role", "userId" ] }, "agendaBlockSourceSchema": { "type": "string", "enum": [ "item", "google-calendar" ] }, "agendaBlockStateSchema": { "type": "string", "enum": [ "confirmed", "tentative", "canceled" ] }, "agendaIsoDateTime": { "type": "string", "format": "date-time" }, "agendaItemHttpSchema": { "type": "object", "properties": { "assignees": { "type": "array", "items": { "$ref": "#/components/schemas/agendaItemUserSchema" } }, "attendees": { "type": "array", "items": { "$ref": "#/components/schemas/agendaAttendeeSchema" } }, "calendarId": { "type": "string", "format": "uuid" }, "confidence": { "type": [ "number", "null" ], "minimum": 0, "maximum": 1 }, "createdAt": { "$ref": "#/components/schemas/agendaIsoDateTime" }, "createdBy": { "type": [ "string", "null" ], "format": "uuid" }, "description": { "type": [ "string", "null" ] }, "descriptionHtml": { "$ref": "#/components/schemas/agendaRichDescriptionSchema" }, "endAt": { "$ref": "#/components/schemas/agendaIsoDateTime" }, "id": { "type": "string", "format": "uuid" }, "isAllDay": { "type": "boolean" }, "isReadOnly": { "type": "boolean" }, "location": { "type": [ "string", "null" ] }, "onlineMeetingUrl": { "type": [ "string", "null" ], "format": "uri" }, "organizationId": { "type": "string", "format": "uuid" }, "organizerDisplayName": { "type": [ "string", "null" ] }, "organizerEmail": { "type": [ "string", "null" ], "format": "email" }, "organizerUserId": { "type": [ "string", "null" ], "format": "uuid" }, "ownerUserId": { "type": [ "string", "null" ], "format": "uuid" }, "priority": { "anyOf": [ { "$ref": "#/components/schemas/taskPrioritySchema" }, { "type": "null" } ] }, "reason": { "anyOf": [ { "$ref": "#/components/schemas/agendaSuggestionReasonSchema" }, { "type": "null" } ] }, "recurrenceDates": { "type": "array", "items": { "$ref": "#/components/schemas/agendaRecurrenceDateSchema" } }, "recurrenceIdAt": { "anyOf": [ { "$ref": "#/components/schemas/agendaIsoDateTime" }, { "type": "null" } ] }, "recurrenceParentItemId": { "type": [ "string", "null" ], "format": "uuid" }, "recurrenceRule": { "anyOf": [ { "$ref": "#/components/schemas/agendaRecurrenceRuleSchema" }, { "type": "null" } ] }, "reminders": { "type": "array", "items": { "$ref": "#/components/schemas/agendaReminderSchema" } }, "source": { "$ref": "#/components/schemas/agendaBlockSourceSchema" }, "startAt": { "$ref": "#/components/schemas/agendaIsoDateTime" }, "state": { "$ref": "#/components/schemas/agendaBlockStateSchema" }, "taskDisplayNumber": { "type": [ "string", "null" ], "minLength": 1 }, "taskId": { "type": [ "string", "null" ], "format": "uuid" }, "taskNumber": { "type": [ "integer", "null" ], "exclusiveMinimum": 0 }, "taskStatus": { "anyOf": [ { "$ref": "#/components/schemas/taskStatusSchema" }, { "type": "null" } ] }, "taskTitle": { "type": [ "string", "null" ], "minLength": 1 }, "timeZone": { "anyOf": [ { "$ref": "#/components/schemas/agendaTimeZoneSchema" }, { "type": "null" } ] }, "title": { "type": "string", "minLength": 1 }, "transparency": { "$ref": "#/components/schemas/agendaTransparencySchema" }, "updatedAt": { "$ref": "#/components/schemas/agendaIsoDateTime" }, "users": { "type": "array", "items": { "$ref": "#/components/schemas/agendaItemUserSchema" } }, "visibility": { "$ref": "#/components/schemas/agendaItemVisibilitySchema" } }, "required": [ "assignees", "attendees", "calendarId", "confidence", "createdAt", "createdBy", "description", "descriptionHtml", "endAt", "id", "isAllDay", "isReadOnly", "location", "onlineMeetingUrl", "organizationId", "organizerDisplayName", "organizerEmail", "organizerUserId", "ownerUserId", "priority", "reason", "recurrenceDates", "recurrenceIdAt", "recurrenceParentItemId", "recurrenceRule", "reminders", "source", "startAt", "state", "taskDisplayNumber", "taskId", "taskNumber", "taskStatus", "taskTitle", "timeZone", "title", "transparency", "updatedAt", "users", "visibility" ] }, "agendaItemUserSchema": { "$ref": "#/components/schemas/taskAssigneeSchema" }, "agendaItemVisibilitySchema": { "type": "string", "enum": [ "public", "private" ] }, "agendaItemsHttpResponseSchema": { "type": "array", "items": { "$ref": "#/components/schemas/agendaItemHttpSchema" } }, "agendaParticipationStatusSchema": { "type": "string", "enum": [ "needs-action", "accepted", "declined", "tentative", "delegated" ] }, "agendaRecurrenceDateKindSchema": { "type": "string", "enum": [ "rdate", "exdate" ] }, "agendaRecurrenceDateSchema": { "type": "object", "properties": { "id": { "type": [ "string", "null" ], "format": "uuid" }, "kind": { "$ref": "#/components/schemas/agendaRecurrenceDateKindSchema" }, "occursAt": { "$ref": "#/components/schemas/agendaIsoDateTime" } }, "required": [ "id", "kind", "occursAt" ] }, "agendaRecurrenceFrequencySchema": { "type": "string", "enum": [ "daily", "weekly", "monthly", "yearly" ] }, "agendaRecurrenceRuleSchema": { "type": "object", "properties": { "count": { "type": [ "integer", "null" ], "exclusiveMinimum": 0 }, "frequency": { "$ref": "#/components/schemas/agendaRecurrenceFrequencySchema" }, "intervalCount": { "type": "integer", "exclusiveMinimum": 0 }, "monthDay": { "type": [ "integer", "null" ], "minimum": 1, "maximum": 31 }, "monthOrdinal": { "anyOf": [ { "anyOf": [ { "type": "string" }, { "type": "integer", "enum": [ 1 ] }, { "type": "integer", "enum": [ 2 ] }, { "type": "integer", "enum": [ 3 ] }, { "type": "integer", "enum": [ 4 ] } ] }, { "type": "null" } ] }, "monthWeekday": { "anyOf": [ { "$ref": "#/components/schemas/agendaWeekdaySchema" }, { "type": "null" } ] }, "untilAt": { "anyOf": [ { "$ref": "#/components/schemas/agendaIsoDateTime" }, { "type": "null" } ] }, "weekDays": { "type": "array", "items": { "$ref": "#/components/schemas/agendaWeekdaySchema" }, "maxItems": 7 } }, "required": [ "count", "frequency", "intervalCount", "monthDay", "monthOrdinal", "monthWeekday", "untilAt", "weekDays" ] }, "agendaReminderMethodSchema": { "type": "string", "enum": [ "display", "email" ] }, "agendaReminderSchema": { "type": "object", "properties": { "id": { "type": [ "string", "null" ], "format": "uuid" }, "method": { "$ref": "#/components/schemas/agendaReminderMethodSchema" }, "offsetMinutes": { "type": "integer", "minimum": -40320, "maximum": 40320 } }, "required": [ "id", "method", "offsetMinutes" ] }, "agendaRichDescriptionSchema": { "type": [ "string", "null" ], "maxLength": 50000 }, "agendaSuggestionReasonSchema": { "type": "object", "properties": { "description": { "type": "string", "minLength": 1 }, "signals": { "type": "array", "items": { "type": "string", "minLength": 1 }, "maxItems": 8 }, "title": { "type": "string", "minLength": 1 } }, "required": [ "description", "signals", "title" ] }, "agendaTimeZoneSchema": { "type": "string", "minLength": 1, "maxLength": 120 }, "agendaTransparencySchema": { "type": "string", "enum": [ "opaque", "transparent" ] }, "agendaWeekdaySchema": { "type": "string", "enum": [ "mo", "tu", "we", "th", "fr", "sa", "su" ] }, "organizationIdSchema": { "type": "string", "format": "uuid" }, "taskAssigneeSchema": { "type": "object", "properties": { "avatarUrl": { "type": [ "string", "null" ], "format": "uri" }, "disabledAt": { "type": [ "string", "null" ] }, "displayName": { "type": "string", "minLength": 1 }, "userId": { "type": "string", "format": "uuid" } }, "required": [ "avatarUrl", "disabledAt", "displayName", "userId" ] }, "taskPrioritySchema": { "type": "string", "enum": [ "low", "medium", "high", "urgent" ] }, "taskStatusSchema": { "type": "string", "enum": [ "todo", "in_progress", "on_hold", "done" ] } } ``` # Create an agenda item. (/docs/openapi/createAgendaItem) Create an agenda item in an editable calendar for the specified organization. ## POST /v1/organizations/{organizationId}/agenda/items ```json { "operationId": "createAgendaItem", "summary": "Create an agenda item.", "description": "Create an agenda item in an editable calendar for the specified organization.", "tags": [ "Agenda" ], "parameters": [ { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/agendaItemInputSchema" } } }, "description": "The agenda item payload.", "required": true }, "responses": { "201": { "description": "Returns the created agenda item.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/agendaItemHttpSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "agendaAttendeeInputSchema": { "type": "object", "properties": { "customerContactId": { "type": [ "string", "null" ], "format": "uuid", "default": null }, "customerId": { "type": [ "string", "null" ], "format": "uuid", "default": null }, "displayName": { "$ref": "#/components/schemas/agendaOptionalShortText" }, "email": { "type": [ "string", "null" ], "format": "email", "default": null }, "isOptional": { "type": "boolean", "default": false }, "isOrganizer": { "type": "boolean", "default": false }, "participationStatus": { "allOf": [ { "$ref": "#/components/schemas/agendaParticipationStatusSchema" } ], "default": "needs-action" }, "role": { "allOf": [ { "$ref": "#/components/schemas/agendaAttendeeRoleSchema" } ], "default": "required" }, "userId": { "type": [ "string", "null" ], "format": "uuid", "default": null } }, "required": [ "customerContactId", "customerId", "displayName", "email", "isOptional", "isOrganizer", "participationStatus", "role", "userId" ] }, "agendaAttendeeRoleSchema": { "type": "string", "enum": [ "required", "optional", "resource", "chair" ] }, "agendaAttendeeSchema": { "type": "object", "properties": { "customerContactId": { "type": [ "string", "null" ], "format": "uuid" }, "customerId": { "type": [ "string", "null" ], "format": "uuid" }, "displayName": { "type": [ "string", "null" ], "minLength": 1, "maxLength": 240 }, "email": { "type": [ "string", "null" ], "format": "email" }, "id": { "type": [ "string", "null" ], "format": "uuid" }, "isOptional": { "type": "boolean" }, "isOrganizer": { "type": "boolean" }, "participationStatus": { "$ref": "#/components/schemas/agendaParticipationStatusSchema" }, "role": { "$ref": "#/components/schemas/agendaAttendeeRoleSchema" }, "userId": { "type": [ "string", "null" ], "format": "uuid" } }, "required": [ "customerContactId", "customerId", "displayName", "email", "id", "isOptional", "isOrganizer", "participationStatus", "role", "userId" ] }, "agendaBlockSourceSchema": { "type": "string", "enum": [ "item", "google-calendar" ] }, "agendaBlockStateSchema": { "type": "string", "enum": [ "confirmed", "tentative", "canceled" ] }, "agendaIsoDateTime": { "type": "string", "format": "date-time" }, "agendaItemHttpSchema": { "type": "object", "properties": { "assignees": { "type": "array", "items": { "$ref": "#/components/schemas/agendaItemUserSchema" } }, "attendees": { "type": "array", "items": { "$ref": "#/components/schemas/agendaAttendeeSchema" } }, "calendarId": { "type": "string", "format": "uuid" }, "confidence": { "type": [ "number", "null" ], "minimum": 0, "maximum": 1 }, "createdAt": { "$ref": "#/components/schemas/agendaIsoDateTime" }, "createdBy": { "type": [ "string", "null" ], "format": "uuid" }, "description": { "type": [ "string", "null" ] }, "descriptionHtml": { "$ref": "#/components/schemas/agendaRichDescriptionSchema" }, "endAt": { "$ref": "#/components/schemas/agendaIsoDateTime" }, "id": { "type": "string", "format": "uuid" }, "isAllDay": { "type": "boolean" }, "isReadOnly": { "type": "boolean" }, "location": { "type": [ "string", "null" ] }, "onlineMeetingUrl": { "type": [ "string", "null" ], "format": "uri" }, "organizationId": { "type": "string", "format": "uuid" }, "organizerDisplayName": { "type": [ "string", "null" ] }, "organizerEmail": { "type": [ "string", "null" ], "format": "email" }, "organizerUserId": { "type": [ "string", "null" ], "format": "uuid" }, "ownerUserId": { "type": [ "string", "null" ], "format": "uuid" }, "priority": { "anyOf": [ { "$ref": "#/components/schemas/taskPrioritySchema" }, { "type": "null" } ] }, "reason": { "anyOf": [ { "$ref": "#/components/schemas/agendaSuggestionReasonSchema" }, { "type": "null" } ] }, "recurrenceDates": { "type": "array", "items": { "$ref": "#/components/schemas/agendaRecurrenceDateSchema" } }, "recurrenceIdAt": { "anyOf": [ { "$ref": "#/components/schemas/agendaIsoDateTime" }, { "type": "null" } ] }, "recurrenceParentItemId": { "type": [ "string", "null" ], "format": "uuid" }, "recurrenceRule": { "anyOf": [ { "$ref": "#/components/schemas/agendaRecurrenceRuleSchema" }, { "type": "null" } ] }, "reminders": { "type": "array", "items": { "$ref": "#/components/schemas/agendaReminderSchema" } }, "source": { "$ref": "#/components/schemas/agendaBlockSourceSchema" }, "startAt": { "$ref": "#/components/schemas/agendaIsoDateTime" }, "state": { "$ref": "#/components/schemas/agendaBlockStateSchema" }, "taskDisplayNumber": { "type": [ "string", "null" ], "minLength": 1 }, "taskId": { "type": [ "string", "null" ], "format": "uuid" }, "taskNumber": { "type": [ "integer", "null" ], "exclusiveMinimum": 0 }, "taskStatus": { "anyOf": [ { "$ref": "#/components/schemas/taskStatusSchema" }, { "type": "null" } ] }, "taskTitle": { "type": [ "string", "null" ], "minLength": 1 }, "timeZone": { "anyOf": [ { "$ref": "#/components/schemas/agendaTimeZoneSchema" }, { "type": "null" } ] }, "title": { "type": "string", "minLength": 1 }, "transparency": { "$ref": "#/components/schemas/agendaTransparencySchema" }, "updatedAt": { "$ref": "#/components/schemas/agendaIsoDateTime" }, "users": { "type": "array", "items": { "$ref": "#/components/schemas/agendaItemUserSchema" } }, "visibility": { "$ref": "#/components/schemas/agendaItemVisibilitySchema" } }, "required": [ "assignees", "attendees", "calendarId", "confidence", "createdAt", "createdBy", "description", "descriptionHtml", "endAt", "id", "isAllDay", "isReadOnly", "location", "onlineMeetingUrl", "organizationId", "organizerDisplayName", "organizerEmail", "organizerUserId", "ownerUserId", "priority", "reason", "recurrenceDates", "recurrenceIdAt", "recurrenceParentItemId", "recurrenceRule", "reminders", "source", "startAt", "state", "taskDisplayNumber", "taskId", "taskNumber", "taskStatus", "taskTitle", "timeZone", "title", "transparency", "updatedAt", "users", "visibility" ] }, "agendaItemInputSchema": { "type": "object", "properties": { "attendees": { "type": "array", "items": { "$ref": "#/components/schemas/agendaAttendeeInputSchema" }, "maxItems": 200 }, "calendarId": { "type": "string", "format": "uuid" }, "description": { "$ref": "#/components/schemas/agendaOptionalText" }, "descriptionHtml": { "allOf": [ { "$ref": "#/components/schemas/agendaRichDescriptionSchema" } ], "default": null }, "endAt": { "$ref": "#/components/schemas/agendaIsoDateTime" }, "isAllDay": { "type": "boolean", "default": false }, "location": { "$ref": "#/components/schemas/agendaOptionalShortText" }, "onlineMeetingUrl": { "type": [ "string", "null" ], "format": "uri", "default": null }, "recurrenceDates": { "type": "array", "items": { "$ref": "#/components/schemas/agendaRecurrenceDateInputSchema" }, "maxItems": 366 }, "recurrenceRule": { "anyOf": [ { "$ref": "#/components/schemas/agendaRecurrenceRuleInputSchema" }, { "type": "null" } ], "default": null }, "reminders": { "type": "array", "items": { "$ref": "#/components/schemas/agendaReminderInputSchema" }, "maxItems": 20 }, "startAt": { "$ref": "#/components/schemas/agendaIsoDateTime" }, "status": { "type": "string", "enum": [ "confirmed", "tentative", "canceled" ], "default": "confirmed" }, "taskId": { "type": [ "string", "null" ], "format": "uuid", "default": null }, "timeZone": { "anyOf": [ { "$ref": "#/components/schemas/agendaTimeZoneSchema" }, { "type": "null" } ], "default": null }, "title": { "type": "string", "minLength": 1, "maxLength": 240 }, "transparency": { "type": "string", "enum": [ "opaque", "transparent" ], "default": "opaque" }, "userIds": { "type": "array", "items": { "type": "string", "format": "uuid" }, "maxItems": 100 } }, "required": [ "attendees", "description", "endAt", "isAllDay", "location", "recurrenceDates", "recurrenceRule", "reminders", "startAt", "status", "taskId", "title", "transparency", "userIds" ] }, "agendaItemUserSchema": { "$ref": "#/components/schemas/taskAssigneeSchema" }, "agendaItemVisibilitySchema": { "type": "string", "enum": [ "public", "private" ] }, "agendaOptionalShortText": { "type": [ "string", "null" ], "maxLength": 500 }, "agendaOptionalText": { "type": [ "string", "null" ], "maxLength": 5000 }, "agendaParticipationStatusSchema": { "type": "string", "enum": [ "needs-action", "accepted", "declined", "tentative", "delegated" ] }, "agendaRecurrenceDateInputSchema": { "type": "object", "properties": { "kind": { "$ref": "#/components/schemas/agendaRecurrenceDateKindSchema" }, "occursAt": { "$ref": "#/components/schemas/agendaIsoDateTime" } }, "required": [ "kind", "occursAt" ] }, "agendaRecurrenceDateKindSchema": { "type": "string", "enum": [ "rdate", "exdate" ] }, "agendaRecurrenceDateSchema": { "type": "object", "properties": { "id": { "type": [ "string", "null" ], "format": "uuid" }, "kind": { "$ref": "#/components/schemas/agendaRecurrenceDateKindSchema" }, "occursAt": { "$ref": "#/components/schemas/agendaIsoDateTime" } }, "required": [ "id", "kind", "occursAt" ] }, "agendaRecurrenceFrequencySchema": { "type": "string", "enum": [ "daily", "weekly", "monthly", "yearly" ] }, "agendaRecurrenceRuleInputSchema": { "type": "object", "properties": { "count": { "type": [ "integer", "null" ], "exclusiveMinimum": 0, "default": null }, "frequency": { "$ref": "#/components/schemas/agendaRecurrenceFrequencySchema" }, "intervalCount": { "type": "integer", "exclusiveMinimum": 0, "default": 1 }, "monthDay": { "type": [ "integer", "null" ], "minimum": 1, "maximum": 31, "default": null }, "monthOrdinal": { "anyOf": [ { "anyOf": [ { "type": "string" }, { "type": "integer", "enum": [ 1 ] }, { "type": "integer", "enum": [ 2 ] }, { "type": "integer", "enum": [ 3 ] }, { "type": "integer", "enum": [ 4 ] } ], "default": null }, { "type": "null" } ] }, "monthWeekday": { "anyOf": [ { "$ref": "#/components/schemas/agendaWeekdaySchema" }, { "type": "null" } ], "default": null }, "untilAt": { "anyOf": [ { "$ref": "#/components/schemas/agendaIsoDateTime" }, { "type": "null" } ], "default": null }, "weekDays": { "type": "array", "items": { "$ref": "#/components/schemas/agendaWeekdaySchema" }, "maxItems": 7 } }, "required": [ "count", "frequency", "intervalCount", "monthDay", "monthOrdinal", "monthWeekday", "untilAt", "weekDays" ] }, "agendaRecurrenceRuleSchema": { "type": "object", "properties": { "count": { "type": [ "integer", "null" ], "exclusiveMinimum": 0 }, "frequency": { "$ref": "#/components/schemas/agendaRecurrenceFrequencySchema" }, "intervalCount": { "type": "integer", "exclusiveMinimum": 0 }, "monthDay": { "type": [ "integer", "null" ], "minimum": 1, "maximum": 31 }, "monthOrdinal": { "anyOf": [ { "anyOf": [ { "type": "string" }, { "type": "integer", "enum": [ 1 ] }, { "type": "integer", "enum": [ 2 ] }, { "type": "integer", "enum": [ 3 ] }, { "type": "integer", "enum": [ 4 ] } ] }, { "type": "null" } ] }, "monthWeekday": { "anyOf": [ { "$ref": "#/components/schemas/agendaWeekdaySchema" }, { "type": "null" } ] }, "untilAt": { "anyOf": [ { "$ref": "#/components/schemas/agendaIsoDateTime" }, { "type": "null" } ] }, "weekDays": { "type": "array", "items": { "$ref": "#/components/schemas/agendaWeekdaySchema" }, "maxItems": 7 } }, "required": [ "count", "frequency", "intervalCount", "monthDay", "monthOrdinal", "monthWeekday", "untilAt", "weekDays" ] }, "agendaReminderInputSchema": { "type": "object", "properties": { "method": { "$ref": "#/components/schemas/agendaReminderMethodSchema" }, "offsetMinutes": { "type": "integer", "minimum": -40320, "maximum": 40320 } }, "required": [ "method", "offsetMinutes" ] }, "agendaReminderMethodSchema": { "type": "string", "enum": [ "display", "email" ] }, "agendaReminderSchema": { "type": "object", "properties": { "id": { "type": [ "string", "null" ], "format": "uuid" }, "method": { "$ref": "#/components/schemas/agendaReminderMethodSchema" }, "offsetMinutes": { "type": "integer", "minimum": -40320, "maximum": 40320 } }, "required": [ "id", "method", "offsetMinutes" ] }, "agendaRichDescriptionSchema": { "type": [ "string", "null" ], "maxLength": 50000 }, "agendaSuggestionReasonSchema": { "type": "object", "properties": { "description": { "type": "string", "minLength": 1 }, "signals": { "type": "array", "items": { "type": "string", "minLength": 1 }, "maxItems": 8 }, "title": { "type": "string", "minLength": 1 } }, "required": [ "description", "signals", "title" ] }, "agendaTimeZoneSchema": { "type": "string", "minLength": 1, "maxLength": 120 }, "agendaTransparencySchema": { "type": "string", "enum": [ "opaque", "transparent" ] }, "agendaWeekdaySchema": { "type": "string", "enum": [ "mo", "tu", "we", "th", "fr", "sa", "su" ] }, "organizationIdSchema": { "type": "string", "format": "uuid" }, "taskAssigneeSchema": { "type": "object", "properties": { "avatarUrl": { "type": [ "string", "null" ], "format": "uri" }, "disabledAt": { "type": [ "string", "null" ] }, "displayName": { "type": "string", "minLength": 1 }, "userId": { "type": "string", "format": "uuid" } }, "required": [ "avatarUrl", "disabledAt", "displayName", "userId" ] }, "taskPrioritySchema": { "type": "string", "enum": [ "low", "medium", "high", "urgent" ] }, "taskStatusSchema": { "type": "string", "enum": [ "todo", "in_progress", "on_hold", "done" ] } } ``` # Get an agenda item. (/docs/openapi/getAgendaItem) Return one agenda item the authenticated user can view. ## GET /v1/organizations/{organizationId}/agenda/items/{agendaItemId} ```json { "operationId": "getAgendaItem", "summary": "Get an agenda item.", "description": "Return one agenda item the authenticated user can view.", "tags": [ "Agenda" ], "parameters": [ { "in": "path", "name": "agendaItemId", "required": true, "schema": { "type": "string", "format": "uuid" }, "example": "123e4567-e89b-12d3-a456-426614174000" }, { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "responses": { "200": { "description": "Returns the requested agenda item.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/agendaItemHttpSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "agendaAttendeeRoleSchema": { "type": "string", "enum": [ "required", "optional", "resource", "chair" ] }, "agendaAttendeeSchema": { "type": "object", "properties": { "customerContactId": { "type": [ "string", "null" ], "format": "uuid" }, "customerId": { "type": [ "string", "null" ], "format": "uuid" }, "displayName": { "type": [ "string", "null" ], "minLength": 1, "maxLength": 240 }, "email": { "type": [ "string", "null" ], "format": "email" }, "id": { "type": [ "string", "null" ], "format": "uuid" }, "isOptional": { "type": "boolean" }, "isOrganizer": { "type": "boolean" }, "participationStatus": { "$ref": "#/components/schemas/agendaParticipationStatusSchema" }, "role": { "$ref": "#/components/schemas/agendaAttendeeRoleSchema" }, "userId": { "type": [ "string", "null" ], "format": "uuid" } }, "required": [ "customerContactId", "customerId", "displayName", "email", "id", "isOptional", "isOrganizer", "participationStatus", "role", "userId" ] }, "agendaBlockSourceSchema": { "type": "string", "enum": [ "item", "google-calendar" ] }, "agendaBlockStateSchema": { "type": "string", "enum": [ "confirmed", "tentative", "canceled" ] }, "agendaIsoDateTime": { "type": "string", "format": "date-time" }, "agendaItemHttpSchema": { "type": "object", "properties": { "assignees": { "type": "array", "items": { "$ref": "#/components/schemas/agendaItemUserSchema" } }, "attendees": { "type": "array", "items": { "$ref": "#/components/schemas/agendaAttendeeSchema" } }, "calendarId": { "type": "string", "format": "uuid" }, "confidence": { "type": [ "number", "null" ], "minimum": 0, "maximum": 1 }, "createdAt": { "$ref": "#/components/schemas/agendaIsoDateTime" }, "createdBy": { "type": [ "string", "null" ], "format": "uuid" }, "description": { "type": [ "string", "null" ] }, "descriptionHtml": { "$ref": "#/components/schemas/agendaRichDescriptionSchema" }, "endAt": { "$ref": "#/components/schemas/agendaIsoDateTime" }, "id": { "type": "string", "format": "uuid" }, "isAllDay": { "type": "boolean" }, "isReadOnly": { "type": "boolean" }, "location": { "type": [ "string", "null" ] }, "onlineMeetingUrl": { "type": [ "string", "null" ], "format": "uri" }, "organizationId": { "type": "string", "format": "uuid" }, "organizerDisplayName": { "type": [ "string", "null" ] }, "organizerEmail": { "type": [ "string", "null" ], "format": "email" }, "organizerUserId": { "type": [ "string", "null" ], "format": "uuid" }, "ownerUserId": { "type": [ "string", "null" ], "format": "uuid" }, "priority": { "anyOf": [ { "$ref": "#/components/schemas/taskPrioritySchema" }, { "type": "null" } ] }, "reason": { "anyOf": [ { "$ref": "#/components/schemas/agendaSuggestionReasonSchema" }, { "type": "null" } ] }, "recurrenceDates": { "type": "array", "items": { "$ref": "#/components/schemas/agendaRecurrenceDateSchema" } }, "recurrenceIdAt": { "anyOf": [ { "$ref": "#/components/schemas/agendaIsoDateTime" }, { "type": "null" } ] }, "recurrenceParentItemId": { "type": [ "string", "null" ], "format": "uuid" }, "recurrenceRule": { "anyOf": [ { "$ref": "#/components/schemas/agendaRecurrenceRuleSchema" }, { "type": "null" } ] }, "reminders": { "type": "array", "items": { "$ref": "#/components/schemas/agendaReminderSchema" } }, "source": { "$ref": "#/components/schemas/agendaBlockSourceSchema" }, "startAt": { "$ref": "#/components/schemas/agendaIsoDateTime" }, "state": { "$ref": "#/components/schemas/agendaBlockStateSchema" }, "taskDisplayNumber": { "type": [ "string", "null" ], "minLength": 1 }, "taskId": { "type": [ "string", "null" ], "format": "uuid" }, "taskNumber": { "type": [ "integer", "null" ], "exclusiveMinimum": 0 }, "taskStatus": { "anyOf": [ { "$ref": "#/components/schemas/taskStatusSchema" }, { "type": "null" } ] }, "taskTitle": { "type": [ "string", "null" ], "minLength": 1 }, "timeZone": { "anyOf": [ { "$ref": "#/components/schemas/agendaTimeZoneSchema" }, { "type": "null" } ] }, "title": { "type": "string", "minLength": 1 }, "transparency": { "$ref": "#/components/schemas/agendaTransparencySchema" }, "updatedAt": { "$ref": "#/components/schemas/agendaIsoDateTime" }, "users": { "type": "array", "items": { "$ref": "#/components/schemas/agendaItemUserSchema" } }, "visibility": { "$ref": "#/components/schemas/agendaItemVisibilitySchema" } }, "required": [ "assignees", "attendees", "calendarId", "confidence", "createdAt", "createdBy", "description", "descriptionHtml", "endAt", "id", "isAllDay", "isReadOnly", "location", "onlineMeetingUrl", "organizationId", "organizerDisplayName", "organizerEmail", "organizerUserId", "ownerUserId", "priority", "reason", "recurrenceDates", "recurrenceIdAt", "recurrenceParentItemId", "recurrenceRule", "reminders", "source", "startAt", "state", "taskDisplayNumber", "taskId", "taskNumber", "taskStatus", "taskTitle", "timeZone", "title", "transparency", "updatedAt", "users", "visibility" ] }, "agendaItemUserSchema": { "$ref": "#/components/schemas/taskAssigneeSchema" }, "agendaItemVisibilitySchema": { "type": "string", "enum": [ "public", "private" ] }, "agendaParticipationStatusSchema": { "type": "string", "enum": [ "needs-action", "accepted", "declined", "tentative", "delegated" ] }, "agendaRecurrenceDateKindSchema": { "type": "string", "enum": [ "rdate", "exdate" ] }, "agendaRecurrenceDateSchema": { "type": "object", "properties": { "id": { "type": [ "string", "null" ], "format": "uuid" }, "kind": { "$ref": "#/components/schemas/agendaRecurrenceDateKindSchema" }, "occursAt": { "$ref": "#/components/schemas/agendaIsoDateTime" } }, "required": [ "id", "kind", "occursAt" ] }, "agendaRecurrenceFrequencySchema": { "type": "string", "enum": [ "daily", "weekly", "monthly", "yearly" ] }, "agendaRecurrenceRuleSchema": { "type": "object", "properties": { "count": { "type": [ "integer", "null" ], "exclusiveMinimum": 0 }, "frequency": { "$ref": "#/components/schemas/agendaRecurrenceFrequencySchema" }, "intervalCount": { "type": "integer", "exclusiveMinimum": 0 }, "monthDay": { "type": [ "integer", "null" ], "minimum": 1, "maximum": 31 }, "monthOrdinal": { "anyOf": [ { "anyOf": [ { "type": "string" }, { "type": "integer", "enum": [ 1 ] }, { "type": "integer", "enum": [ 2 ] }, { "type": "integer", "enum": [ 3 ] }, { "type": "integer", "enum": [ 4 ] } ] }, { "type": "null" } ] }, "monthWeekday": { "anyOf": [ { "$ref": "#/components/schemas/agendaWeekdaySchema" }, { "type": "null" } ] }, "untilAt": { "anyOf": [ { "$ref": "#/components/schemas/agendaIsoDateTime" }, { "type": "null" } ] }, "weekDays": { "type": "array", "items": { "$ref": "#/components/schemas/agendaWeekdaySchema" }, "maxItems": 7 } }, "required": [ "count", "frequency", "intervalCount", "monthDay", "monthOrdinal", "monthWeekday", "untilAt", "weekDays" ] }, "agendaReminderMethodSchema": { "type": "string", "enum": [ "display", "email" ] }, "agendaReminderSchema": { "type": "object", "properties": { "id": { "type": [ "string", "null" ], "format": "uuid" }, "method": { "$ref": "#/components/schemas/agendaReminderMethodSchema" }, "offsetMinutes": { "type": "integer", "minimum": -40320, "maximum": 40320 } }, "required": [ "id", "method", "offsetMinutes" ] }, "agendaRichDescriptionSchema": { "type": [ "string", "null" ], "maxLength": 50000 }, "agendaSuggestionReasonSchema": { "type": "object", "properties": { "description": { "type": "string", "minLength": 1 }, "signals": { "type": "array", "items": { "type": "string", "minLength": 1 }, "maxItems": 8 }, "title": { "type": "string", "minLength": 1 } }, "required": [ "description", "signals", "title" ] }, "agendaTimeZoneSchema": { "type": "string", "minLength": 1, "maxLength": 120 }, "agendaTransparencySchema": { "type": "string", "enum": [ "opaque", "transparent" ] }, "agendaWeekdaySchema": { "type": "string", "enum": [ "mo", "tu", "we", "th", "fr", "sa", "su" ] }, "organizationIdSchema": { "type": "string", "format": "uuid" }, "taskAssigneeSchema": { "type": "object", "properties": { "avatarUrl": { "type": [ "string", "null" ], "format": "uri" }, "disabledAt": { "type": [ "string", "null" ] }, "displayName": { "type": "string", "minLength": 1 }, "userId": { "type": "string", "format": "uuid" } }, "required": [ "avatarUrl", "disabledAt", "displayName", "userId" ] }, "taskPrioritySchema": { "type": "string", "enum": [ "low", "medium", "high", "urgent" ] }, "taskStatusSchema": { "type": "string", "enum": [ "todo", "in_progress", "on_hold", "done" ] } } ``` # Update an agenda item. (/docs/openapi/updateAgendaItem) Replace an agenda item or one recurring occurrence in an editable calendar. ## PATCH /v1/organizations/{organizationId}/agenda/items/{agendaItemId} ```json { "operationId": "updateAgendaItem", "summary": "Update an agenda item.", "description": "Replace an agenda item or one recurring occurrence in an editable calendar.", "tags": [ "Agenda" ], "parameters": [ { "in": "path", "name": "agendaItemId", "required": true, "schema": { "type": "string", "format": "uuid" }, "example": "123e4567-e89b-12d3-a456-426614174000" }, { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/agendaItemUpdateHttpInputSchema" } } }, "description": "The complete agenda item payload plus the optional recurrence mutation scope.", "required": true }, "responses": { "200": { "description": "Returns the updated agenda item.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/agendaItemHttpSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "agendaAttendeeInputSchema": { "type": "object", "properties": { "customerContactId": { "type": [ "string", "null" ], "format": "uuid", "default": null }, "customerId": { "type": [ "string", "null" ], "format": "uuid", "default": null }, "displayName": { "$ref": "#/components/schemas/agendaOptionalShortText" }, "email": { "type": [ "string", "null" ], "format": "email", "default": null }, "isOptional": { "type": "boolean", "default": false }, "isOrganizer": { "type": "boolean", "default": false }, "participationStatus": { "allOf": [ { "$ref": "#/components/schemas/agendaParticipationStatusSchema" } ], "default": "needs-action" }, "role": { "allOf": [ { "$ref": "#/components/schemas/agendaAttendeeRoleSchema" } ], "default": "required" }, "userId": { "type": [ "string", "null" ], "format": "uuid", "default": null } }, "required": [ "customerContactId", "customerId", "displayName", "email", "isOptional", "isOrganizer", "participationStatus", "role", "userId" ] }, "agendaAttendeeRoleSchema": { "type": "string", "enum": [ "required", "optional", "resource", "chair" ] }, "agendaAttendeeSchema": { "type": "object", "properties": { "customerContactId": { "type": [ "string", "null" ], "format": "uuid" }, "customerId": { "type": [ "string", "null" ], "format": "uuid" }, "displayName": { "type": [ "string", "null" ], "minLength": 1, "maxLength": 240 }, "email": { "type": [ "string", "null" ], "format": "email" }, "id": { "type": [ "string", "null" ], "format": "uuid" }, "isOptional": { "type": "boolean" }, "isOrganizer": { "type": "boolean" }, "participationStatus": { "$ref": "#/components/schemas/agendaParticipationStatusSchema" }, "role": { "$ref": "#/components/schemas/agendaAttendeeRoleSchema" }, "userId": { "type": [ "string", "null" ], "format": "uuid" } }, "required": [ "customerContactId", "customerId", "displayName", "email", "id", "isOptional", "isOrganizer", "participationStatus", "role", "userId" ] }, "agendaBlockSourceSchema": { "type": "string", "enum": [ "item", "google-calendar" ] }, "agendaBlockStateSchema": { "type": "string", "enum": [ "confirmed", "tentative", "canceled" ] }, "agendaIsoDateTime": { "type": "string", "format": "date-time" }, "agendaItemHttpSchema": { "type": "object", "properties": { "assignees": { "type": "array", "items": { "$ref": "#/components/schemas/agendaItemUserSchema" } }, "attendees": { "type": "array", "items": { "$ref": "#/components/schemas/agendaAttendeeSchema" } }, "calendarId": { "type": "string", "format": "uuid" }, "confidence": { "type": [ "number", "null" ], "minimum": 0, "maximum": 1 }, "createdAt": { "$ref": "#/components/schemas/agendaIsoDateTime" }, "createdBy": { "type": [ "string", "null" ], "format": "uuid" }, "description": { "type": [ "string", "null" ] }, "descriptionHtml": { "$ref": "#/components/schemas/agendaRichDescriptionSchema" }, "endAt": { "$ref": "#/components/schemas/agendaIsoDateTime" }, "id": { "type": "string", "format": "uuid" }, "isAllDay": { "type": "boolean" }, "isReadOnly": { "type": "boolean" }, "location": { "type": [ "string", "null" ] }, "onlineMeetingUrl": { "type": [ "string", "null" ], "format": "uri" }, "organizationId": { "type": "string", "format": "uuid" }, "organizerDisplayName": { "type": [ "string", "null" ] }, "organizerEmail": { "type": [ "string", "null" ], "format": "email" }, "organizerUserId": { "type": [ "string", "null" ], "format": "uuid" }, "ownerUserId": { "type": [ "string", "null" ], "format": "uuid" }, "priority": { "anyOf": [ { "$ref": "#/components/schemas/taskPrioritySchema" }, { "type": "null" } ] }, "reason": { "anyOf": [ { "$ref": "#/components/schemas/agendaSuggestionReasonSchema" }, { "type": "null" } ] }, "recurrenceDates": { "type": "array", "items": { "$ref": "#/components/schemas/agendaRecurrenceDateSchema" } }, "recurrenceIdAt": { "anyOf": [ { "$ref": "#/components/schemas/agendaIsoDateTime" }, { "type": "null" } ] }, "recurrenceParentItemId": { "type": [ "string", "null" ], "format": "uuid" }, "recurrenceRule": { "anyOf": [ { "$ref": "#/components/schemas/agendaRecurrenceRuleSchema" }, { "type": "null" } ] }, "reminders": { "type": "array", "items": { "$ref": "#/components/schemas/agendaReminderSchema" } }, "source": { "$ref": "#/components/schemas/agendaBlockSourceSchema" }, "startAt": { "$ref": "#/components/schemas/agendaIsoDateTime" }, "state": { "$ref": "#/components/schemas/agendaBlockStateSchema" }, "taskDisplayNumber": { "type": [ "string", "null" ], "minLength": 1 }, "taskId": { "type": [ "string", "null" ], "format": "uuid" }, "taskNumber": { "type": [ "integer", "null" ], "exclusiveMinimum": 0 }, "taskStatus": { "anyOf": [ { "$ref": "#/components/schemas/taskStatusSchema" }, { "type": "null" } ] }, "taskTitle": { "type": [ "string", "null" ], "minLength": 1 }, "timeZone": { "anyOf": [ { "$ref": "#/components/schemas/agendaTimeZoneSchema" }, { "type": "null" } ] }, "title": { "type": "string", "minLength": 1 }, "transparency": { "$ref": "#/components/schemas/agendaTransparencySchema" }, "updatedAt": { "$ref": "#/components/schemas/agendaIsoDateTime" }, "users": { "type": "array", "items": { "$ref": "#/components/schemas/agendaItemUserSchema" } }, "visibility": { "$ref": "#/components/schemas/agendaItemVisibilitySchema" } }, "required": [ "assignees", "attendees", "calendarId", "confidence", "createdAt", "createdBy", "description", "descriptionHtml", "endAt", "id", "isAllDay", "isReadOnly", "location", "onlineMeetingUrl", "organizationId", "organizerDisplayName", "organizerEmail", "organizerUserId", "ownerUserId", "priority", "reason", "recurrenceDates", "recurrenceIdAt", "recurrenceParentItemId", "recurrenceRule", "reminders", "source", "startAt", "state", "taskDisplayNumber", "taskId", "taskNumber", "taskStatus", "taskTitle", "timeZone", "title", "transparency", "updatedAt", "users", "visibility" ] }, "agendaItemUpdateHttpInputSchema": { "type": "object", "properties": { "attendees": { "type": "array", "items": { "$ref": "#/components/schemas/agendaAttendeeInputSchema" }, "maxItems": 200 }, "calendarId": { "type": "string", "format": "uuid" }, "description": { "$ref": "#/components/schemas/agendaOptionalText" }, "descriptionHtml": { "allOf": [ { "$ref": "#/components/schemas/agendaRichDescriptionSchema" } ], "default": null }, "endAt": { "$ref": "#/components/schemas/agendaIsoDateTime" }, "isAllDay": { "type": "boolean", "default": false }, "location": { "$ref": "#/components/schemas/agendaOptionalShortText" }, "onlineMeetingUrl": { "type": [ "string", "null" ], "format": "uri", "default": null }, "recurrenceDates": { "type": "array", "items": { "$ref": "#/components/schemas/agendaRecurrenceDateInputSchema" }, "maxItems": 366 }, "recurrenceRule": { "anyOf": [ { "$ref": "#/components/schemas/agendaRecurrenceRuleInputSchema" }, { "type": "null" } ], "default": null }, "reminders": { "type": "array", "items": { "$ref": "#/components/schemas/agendaReminderInputSchema" }, "maxItems": 20 }, "startAt": { "$ref": "#/components/schemas/agendaIsoDateTime" }, "status": { "type": "string", "enum": [ "confirmed", "tentative", "canceled" ], "default": "confirmed" }, "taskId": { "type": [ "string", "null" ], "format": "uuid", "default": null }, "timeZone": { "anyOf": [ { "$ref": "#/components/schemas/agendaTimeZoneSchema" }, { "type": "null" } ], "default": null }, "title": { "type": "string", "minLength": 1, "maxLength": 240 }, "transparency": { "type": "string", "enum": [ "opaque", "transparent" ], "default": "opaque" }, "userIds": { "type": "array", "items": { "type": "string", "format": "uuid" }, "maxItems": 100 }, "occurrenceStartAt": { "allOf": [ { "$ref": "#/components/schemas/agendaIsoDateTime" } ] }, "scope": { "type": "string", "enum": [ "occurrence", "series" ], "default": "series" } }, "required": [ "attendees", "description", "endAt", "isAllDay", "location", "recurrenceDates", "recurrenceRule", "reminders", "startAt", "status", "taskId", "title", "transparency", "userIds", "scope" ] }, "agendaItemUserSchema": { "$ref": "#/components/schemas/taskAssigneeSchema" }, "agendaItemVisibilitySchema": { "type": "string", "enum": [ "public", "private" ] }, "agendaOptionalShortText": { "type": [ "string", "null" ], "maxLength": 500 }, "agendaOptionalText": { "type": [ "string", "null" ], "maxLength": 5000 }, "agendaParticipationStatusSchema": { "type": "string", "enum": [ "needs-action", "accepted", "declined", "tentative", "delegated" ] }, "agendaRecurrenceDateInputSchema": { "type": "object", "properties": { "kind": { "$ref": "#/components/schemas/agendaRecurrenceDateKindSchema" }, "occursAt": { "$ref": "#/components/schemas/agendaIsoDateTime" } }, "required": [ "kind", "occursAt" ] }, "agendaRecurrenceDateKindSchema": { "type": "string", "enum": [ "rdate", "exdate" ] }, "agendaRecurrenceDateSchema": { "type": "object", "properties": { "id": { "type": [ "string", "null" ], "format": "uuid" }, "kind": { "$ref": "#/components/schemas/agendaRecurrenceDateKindSchema" }, "occursAt": { "$ref": "#/components/schemas/agendaIsoDateTime" } }, "required": [ "id", "kind", "occursAt" ] }, "agendaRecurrenceFrequencySchema": { "type": "string", "enum": [ "daily", "weekly", "monthly", "yearly" ] }, "agendaRecurrenceRuleInputSchema": { "type": "object", "properties": { "count": { "type": [ "integer", "null" ], "exclusiveMinimum": 0, "default": null }, "frequency": { "$ref": "#/components/schemas/agendaRecurrenceFrequencySchema" }, "intervalCount": { "type": "integer", "exclusiveMinimum": 0, "default": 1 }, "monthDay": { "type": [ "integer", "null" ], "minimum": 1, "maximum": 31, "default": null }, "monthOrdinal": { "anyOf": [ { "anyOf": [ { "type": "string" }, { "type": "integer", "enum": [ 1 ] }, { "type": "integer", "enum": [ 2 ] }, { "type": "integer", "enum": [ 3 ] }, { "type": "integer", "enum": [ 4 ] } ], "default": null }, { "type": "null" } ] }, "monthWeekday": { "anyOf": [ { "$ref": "#/components/schemas/agendaWeekdaySchema" }, { "type": "null" } ], "default": null }, "untilAt": { "anyOf": [ { "$ref": "#/components/schemas/agendaIsoDateTime" }, { "type": "null" } ], "default": null }, "weekDays": { "type": "array", "items": { "$ref": "#/components/schemas/agendaWeekdaySchema" }, "maxItems": 7 } }, "required": [ "count", "frequency", "intervalCount", "monthDay", "monthOrdinal", "monthWeekday", "untilAt", "weekDays" ] }, "agendaRecurrenceRuleSchema": { "type": "object", "properties": { "count": { "type": [ "integer", "null" ], "exclusiveMinimum": 0 }, "frequency": { "$ref": "#/components/schemas/agendaRecurrenceFrequencySchema" }, "intervalCount": { "type": "integer", "exclusiveMinimum": 0 }, "monthDay": { "type": [ "integer", "null" ], "minimum": 1, "maximum": 31 }, "monthOrdinal": { "anyOf": [ { "anyOf": [ { "type": "string" }, { "type": "integer", "enum": [ 1 ] }, { "type": "integer", "enum": [ 2 ] }, { "type": "integer", "enum": [ 3 ] }, { "type": "integer", "enum": [ 4 ] } ] }, { "type": "null" } ] }, "monthWeekday": { "anyOf": [ { "$ref": "#/components/schemas/agendaWeekdaySchema" }, { "type": "null" } ] }, "untilAt": { "anyOf": [ { "$ref": "#/components/schemas/agendaIsoDateTime" }, { "type": "null" } ] }, "weekDays": { "type": "array", "items": { "$ref": "#/components/schemas/agendaWeekdaySchema" }, "maxItems": 7 } }, "required": [ "count", "frequency", "intervalCount", "monthDay", "monthOrdinal", "monthWeekday", "untilAt", "weekDays" ] }, "agendaReminderInputSchema": { "type": "object", "properties": { "method": { "$ref": "#/components/schemas/agendaReminderMethodSchema" }, "offsetMinutes": { "type": "integer", "minimum": -40320, "maximum": 40320 } }, "required": [ "method", "offsetMinutes" ] }, "agendaReminderMethodSchema": { "type": "string", "enum": [ "display", "email" ] }, "agendaReminderSchema": { "type": "object", "properties": { "id": { "type": [ "string", "null" ], "format": "uuid" }, "method": { "$ref": "#/components/schemas/agendaReminderMethodSchema" }, "offsetMinutes": { "type": "integer", "minimum": -40320, "maximum": 40320 } }, "required": [ "id", "method", "offsetMinutes" ] }, "agendaRichDescriptionSchema": { "type": [ "string", "null" ], "maxLength": 50000 }, "agendaSuggestionReasonSchema": { "type": "object", "properties": { "description": { "type": "string", "minLength": 1 }, "signals": { "type": "array", "items": { "type": "string", "minLength": 1 }, "maxItems": 8 }, "title": { "type": "string", "minLength": 1 } }, "required": [ "description", "signals", "title" ] }, "agendaTimeZoneSchema": { "type": "string", "minLength": 1, "maxLength": 120 }, "agendaTransparencySchema": { "type": "string", "enum": [ "opaque", "transparent" ] }, "agendaWeekdaySchema": { "type": "string", "enum": [ "mo", "tu", "we", "th", "fr", "sa", "su" ] }, "organizationIdSchema": { "type": "string", "format": "uuid" }, "taskAssigneeSchema": { "type": "object", "properties": { "avatarUrl": { "type": [ "string", "null" ], "format": "uri" }, "disabledAt": { "type": [ "string", "null" ] }, "displayName": { "type": "string", "minLength": 1 }, "userId": { "type": "string", "format": "uuid" } }, "required": [ "avatarUrl", "disabledAt", "displayName", "userId" ] }, "taskPrioritySchema": { "type": "string", "enum": [ "low", "medium", "high", "urgent" ] }, "taskStatusSchema": { "type": "string", "enum": [ "todo", "in_progress", "on_hold", "done" ] } } ``` # Delete an agenda item. (/docs/openapi/deleteAgendaItem) Delete an agenda series or one recurring occurrence from an editable calendar. ## DELETE /v1/organizations/{organizationId}/agenda/items/{agendaItemId} ```json { "operationId": "deleteAgendaItem", "summary": "Delete an agenda item.", "description": "Delete an agenda series or one recurring occurrence from an editable calendar.", "tags": [ "Agenda" ], "parameters": [ { "in": "query", "name": "occurrenceStartAt", "required": false, "schema": { "allOf": [ { "$ref": "#/components/schemas/agendaIsoDateTime" } ] }, "example": "example" }, { "in": "query", "name": "scope", "required": true, "schema": { "type": "string", "enum": [ "occurrence", "series" ], "default": "series" }, "example": "occurrence" }, { "in": "path", "name": "agendaItemId", "required": true, "schema": { "type": "string", "format": "uuid" }, "example": "123e4567-e89b-12d3-a456-426614174000" }, { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "responses": { "200": { "description": "Returns a success payload when the agenda item is deleted.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/publicApiSuccessResponseSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "agendaIsoDateTime": { "type": "string", "format": "date-time" }, "organizationIdSchema": { "type": "string", "format": "uuid" }, "publicApiSuccessResponseSchema": { "type": "object", "properties": { "success": { "type": "boolean", "enum": [ true ] } }, "required": [ "success" ] } } ``` # Move an agenda item. (/docs/openapi/moveAgendaItem) Move or resize an agenda item or one recurring occurrence in an editable calendar. ## POST /v1/organizations/{organizationId}/agenda/items/{agendaItemId}/move ```json { "operationId": "moveAgendaItem", "summary": "Move an agenda item.", "description": "Move or resize an agenda item or one recurring occurrence in an editable calendar.", "tags": [ "Agenda" ], "parameters": [ { "in": "path", "name": "agendaItemId", "required": true, "schema": { "type": "string", "format": "uuid" }, "example": "123e4567-e89b-12d3-a456-426614174000" }, { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/agendaItemMoveInputSchema" } } }, "description": "The new temporal range and optional recurrence mutation scope.", "required": true }, "responses": { "200": { "description": "Returns the moved agenda item.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/agendaItemHttpSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "agendaAttendeeRoleSchema": { "type": "string", "enum": [ "required", "optional", "resource", "chair" ] }, "agendaAttendeeSchema": { "type": "object", "properties": { "customerContactId": { "type": [ "string", "null" ], "format": "uuid" }, "customerId": { "type": [ "string", "null" ], "format": "uuid" }, "displayName": { "type": [ "string", "null" ], "minLength": 1, "maxLength": 240 }, "email": { "type": [ "string", "null" ], "format": "email" }, "id": { "type": [ "string", "null" ], "format": "uuid" }, "isOptional": { "type": "boolean" }, "isOrganizer": { "type": "boolean" }, "participationStatus": { "$ref": "#/components/schemas/agendaParticipationStatusSchema" }, "role": { "$ref": "#/components/schemas/agendaAttendeeRoleSchema" }, "userId": { "type": [ "string", "null" ], "format": "uuid" } }, "required": [ "customerContactId", "customerId", "displayName", "email", "id", "isOptional", "isOrganizer", "participationStatus", "role", "userId" ] }, "agendaBlockSourceSchema": { "type": "string", "enum": [ "item", "google-calendar" ] }, "agendaBlockStateSchema": { "type": "string", "enum": [ "confirmed", "tentative", "canceled" ] }, "agendaIsoDateTime": { "type": "string", "format": "date-time" }, "agendaItemHttpSchema": { "type": "object", "properties": { "assignees": { "type": "array", "items": { "$ref": "#/components/schemas/agendaItemUserSchema" } }, "attendees": { "type": "array", "items": { "$ref": "#/components/schemas/agendaAttendeeSchema" } }, "calendarId": { "type": "string", "format": "uuid" }, "confidence": { "type": [ "number", "null" ], "minimum": 0, "maximum": 1 }, "createdAt": { "$ref": "#/components/schemas/agendaIsoDateTime" }, "createdBy": { "type": [ "string", "null" ], "format": "uuid" }, "description": { "type": [ "string", "null" ] }, "descriptionHtml": { "$ref": "#/components/schemas/agendaRichDescriptionSchema" }, "endAt": { "$ref": "#/components/schemas/agendaIsoDateTime" }, "id": { "type": "string", "format": "uuid" }, "isAllDay": { "type": "boolean" }, "isReadOnly": { "type": "boolean" }, "location": { "type": [ "string", "null" ] }, "onlineMeetingUrl": { "type": [ "string", "null" ], "format": "uri" }, "organizationId": { "type": "string", "format": "uuid" }, "organizerDisplayName": { "type": [ "string", "null" ] }, "organizerEmail": { "type": [ "string", "null" ], "format": "email" }, "organizerUserId": { "type": [ "string", "null" ], "format": "uuid" }, "ownerUserId": { "type": [ "string", "null" ], "format": "uuid" }, "priority": { "anyOf": [ { "$ref": "#/components/schemas/taskPrioritySchema" }, { "type": "null" } ] }, "reason": { "anyOf": [ { "$ref": "#/components/schemas/agendaSuggestionReasonSchema" }, { "type": "null" } ] }, "recurrenceDates": { "type": "array", "items": { "$ref": "#/components/schemas/agendaRecurrenceDateSchema" } }, "recurrenceIdAt": { "anyOf": [ { "$ref": "#/components/schemas/agendaIsoDateTime" }, { "type": "null" } ] }, "recurrenceParentItemId": { "type": [ "string", "null" ], "format": "uuid" }, "recurrenceRule": { "anyOf": [ { "$ref": "#/components/schemas/agendaRecurrenceRuleSchema" }, { "type": "null" } ] }, "reminders": { "type": "array", "items": { "$ref": "#/components/schemas/agendaReminderSchema" } }, "source": { "$ref": "#/components/schemas/agendaBlockSourceSchema" }, "startAt": { "$ref": "#/components/schemas/agendaIsoDateTime" }, "state": { "$ref": "#/components/schemas/agendaBlockStateSchema" }, "taskDisplayNumber": { "type": [ "string", "null" ], "minLength": 1 }, "taskId": { "type": [ "string", "null" ], "format": "uuid" }, "taskNumber": { "type": [ "integer", "null" ], "exclusiveMinimum": 0 }, "taskStatus": { "anyOf": [ { "$ref": "#/components/schemas/taskStatusSchema" }, { "type": "null" } ] }, "taskTitle": { "type": [ "string", "null" ], "minLength": 1 }, "timeZone": { "anyOf": [ { "$ref": "#/components/schemas/agendaTimeZoneSchema" }, { "type": "null" } ] }, "title": { "type": "string", "minLength": 1 }, "transparency": { "$ref": "#/components/schemas/agendaTransparencySchema" }, "updatedAt": { "$ref": "#/components/schemas/agendaIsoDateTime" }, "users": { "type": "array", "items": { "$ref": "#/components/schemas/agendaItemUserSchema" } }, "visibility": { "$ref": "#/components/schemas/agendaItemVisibilitySchema" } }, "required": [ "assignees", "attendees", "calendarId", "confidence", "createdAt", "createdBy", "description", "descriptionHtml", "endAt", "id", "isAllDay", "isReadOnly", "location", "onlineMeetingUrl", "organizationId", "organizerDisplayName", "organizerEmail", "organizerUserId", "ownerUserId", "priority", "reason", "recurrenceDates", "recurrenceIdAt", "recurrenceParentItemId", "recurrenceRule", "reminders", "source", "startAt", "state", "taskDisplayNumber", "taskId", "taskNumber", "taskStatus", "taskTitle", "timeZone", "title", "transparency", "updatedAt", "users", "visibility" ] }, "agendaItemMoveInputSchema": { "type": "object", "properties": { "endAt": { "$ref": "#/components/schemas/agendaIsoDateTime" }, "isAllDay": { "type": "boolean", "default": false }, "occurrenceStartAt": { "allOf": [ { "$ref": "#/components/schemas/agendaIsoDateTime" } ] }, "scope": { "type": "string", "enum": [ "occurrence", "series" ], "default": "series" }, "startAt": { "$ref": "#/components/schemas/agendaIsoDateTime" } }, "required": [ "endAt", "isAllDay", "scope", "startAt" ] }, "agendaItemUserSchema": { "$ref": "#/components/schemas/taskAssigneeSchema" }, "agendaItemVisibilitySchema": { "type": "string", "enum": [ "public", "private" ] }, "agendaParticipationStatusSchema": { "type": "string", "enum": [ "needs-action", "accepted", "declined", "tentative", "delegated" ] }, "agendaRecurrenceDateKindSchema": { "type": "string", "enum": [ "rdate", "exdate" ] }, "agendaRecurrenceDateSchema": { "type": "object", "properties": { "id": { "type": [ "string", "null" ], "format": "uuid" }, "kind": { "$ref": "#/components/schemas/agendaRecurrenceDateKindSchema" }, "occursAt": { "$ref": "#/components/schemas/agendaIsoDateTime" } }, "required": [ "id", "kind", "occursAt" ] }, "agendaRecurrenceFrequencySchema": { "type": "string", "enum": [ "daily", "weekly", "monthly", "yearly" ] }, "agendaRecurrenceRuleSchema": { "type": "object", "properties": { "count": { "type": [ "integer", "null" ], "exclusiveMinimum": 0 }, "frequency": { "$ref": "#/components/schemas/agendaRecurrenceFrequencySchema" }, "intervalCount": { "type": "integer", "exclusiveMinimum": 0 }, "monthDay": { "type": [ "integer", "null" ], "minimum": 1, "maximum": 31 }, "monthOrdinal": { "anyOf": [ { "anyOf": [ { "type": "string" }, { "type": "integer", "enum": [ 1 ] }, { "type": "integer", "enum": [ 2 ] }, { "type": "integer", "enum": [ 3 ] }, { "type": "integer", "enum": [ 4 ] } ] }, { "type": "null" } ] }, "monthWeekday": { "anyOf": [ { "$ref": "#/components/schemas/agendaWeekdaySchema" }, { "type": "null" } ] }, "untilAt": { "anyOf": [ { "$ref": "#/components/schemas/agendaIsoDateTime" }, { "type": "null" } ] }, "weekDays": { "type": "array", "items": { "$ref": "#/components/schemas/agendaWeekdaySchema" }, "maxItems": 7 } }, "required": [ "count", "frequency", "intervalCount", "monthDay", "monthOrdinal", "monthWeekday", "untilAt", "weekDays" ] }, "agendaReminderMethodSchema": { "type": "string", "enum": [ "display", "email" ] }, "agendaReminderSchema": { "type": "object", "properties": { "id": { "type": [ "string", "null" ], "format": "uuid" }, "method": { "$ref": "#/components/schemas/agendaReminderMethodSchema" }, "offsetMinutes": { "type": "integer", "minimum": -40320, "maximum": 40320 } }, "required": [ "id", "method", "offsetMinutes" ] }, "agendaRichDescriptionSchema": { "type": [ "string", "null" ], "maxLength": 50000 }, "agendaSuggestionReasonSchema": { "type": "object", "properties": { "description": { "type": "string", "minLength": 1 }, "signals": { "type": "array", "items": { "type": "string", "minLength": 1 }, "maxItems": 8 }, "title": { "type": "string", "minLength": 1 } }, "required": [ "description", "signals", "title" ] }, "agendaTimeZoneSchema": { "type": "string", "minLength": 1, "maxLength": 120 }, "agendaTransparencySchema": { "type": "string", "enum": [ "opaque", "transparent" ] }, "agendaWeekdaySchema": { "type": "string", "enum": [ "mo", "tu", "we", "th", "fr", "sa", "su" ] }, "organizationIdSchema": { "type": "string", "format": "uuid" }, "taskAssigneeSchema": { "type": "object", "properties": { "avatarUrl": { "type": [ "string", "null" ], "format": "uri" }, "disabledAt": { "type": [ "string", "null" ] }, "displayName": { "type": "string", "minLength": 1 }, "userId": { "type": "string", "format": "uuid" } }, "required": [ "avatarUrl", "disabledAt", "displayName", "userId" ] }, "taskPrioritySchema": { "type": "string", "enum": [ "low", "medium", "high", "urgent" ] }, "taskStatusSchema": { "type": "string", "enum": [ "todo", "in_progress", "on_hold", "done" ] } } ``` # List chat messages. (/docs/openapi/listChatMessages) Return a latest-relative page of messages. Offset zero is the newest window; each page is chronological and increasing offsets retrieve older windows. ## GET /v1/organizations/{organizationId}/ai/threads/{threadId}/messages ```json { "operationId": "listChatMessages", "summary": "List chat messages.", "description": "Return a latest-relative page of messages. Offset zero is the newest window; each page is chronological and increasing offsets retrieve older windows.", "tags": [ "AI" ], "parameters": [ { "in": "query", "name": "limit", "required": true, "schema": { "type": "integer", "minimum": 1, "maximum": 100, "default": 50 }, "example": 1 }, { "in": "query", "name": "offset", "required": true, "schema": { "type": "integer", "minimum": 0, "default": 0 }, "example": 1 }, { "in": "path", "name": "organizationId", "required": true, "schema": { "type": "string", "format": "uuid" }, "example": "123e4567-e89b-12d3-a456-426614174000" }, { "in": "path", "name": "threadId", "required": true, "schema": { "type": "string", "format": "uuid" }, "example": "123e4567-e89b-12d3-a456-426614174000" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "responses": { "200": { "description": "Returns a paginated chronological message window.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/chatMessagesListResponseSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "chatMessageItemSchema": { "type": "object", "properties": { "clientMessageId": { "type": [ "string", "null" ], "minLength": 1, "maxLength": 240 }, "createdAt": { "type": "string", "format": "date-time" }, "createdBy": { "type": [ "string", "null" ], "format": "uuid" }, "id": { "type": "string", "format": "uuid" }, "metadata": { "type": "object", "additionalProperties": { "$ref": "#/components/schemas/jsonValueSchema" }, "propertyNames": { "type": "string" } }, "organizationId": { "type": [ "string", "null" ], "format": "uuid" }, "parts": { "type": "array", "items": {} }, "role": { "type": "string", "enum": [ "system", "user", "assistant", "tool" ] }, "status": { "type": "string", "enum": [ "complete", "partial", "failed" ] }, "threadId": { "type": "string", "format": "uuid" } }, "required": [ "clientMessageId", "createdAt", "createdBy", "id", "metadata", "organizationId", "parts", "role", "status", "threadId" ] }, "chatMessagesListResponseSchema": { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/components/schemas/chatMessageItemSchema" } }, "pagination": { "type": "object", "properties": { "hasMore": { "type": "boolean" }, "limit": { "type": "integer", "minimum": 1, "maximum": 100 }, "offset": { "type": "integer", "minimum": 0 }, "total": { "type": "integer", "minimum": 0 } }, "required": [ "hasMore", "limit", "offset", "total" ] } }, "required": [ "data", "pagination" ] }, "jsonValueSchema": { "anyOf": [ { "type": "string" }, { "type": "number" }, { "type": "boolean" }, { "type": "null" }, { "type": "array", "items": { "$ref": "#/components/schemas/jsonValueSchema" } }, { "type": "object", "additionalProperties": { "$ref": "#/components/schemas/jsonValueSchema" }, "propertyNames": { "type": "string" } } ] } } ``` # Append a chat message. (/docs/openapi/appendChatMessage) Append a message to an organization chat thread. ## POST /v1/organizations/{organizationId}/ai/threads/{threadId}/messages ```json { "operationId": "appendChatMessage", "summary": "Append a chat message.", "description": "Append a message to an organization chat thread.", "tags": [ "AI" ], "parameters": [ { "in": "path", "name": "organizationId", "required": true, "schema": { "type": "string", "format": "uuid" }, "example": "123e4567-e89b-12d3-a456-426614174000" }, { "in": "path", "name": "threadId", "required": true, "schema": { "type": "string", "format": "uuid" }, "example": "123e4567-e89b-12d3-a456-426614174000" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/appendChatMessageBodySchema" } } }, "description": "The chat message payload to append.", "required": true }, "responses": { "201": { "description": "Returns the created chat message.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/appendChatMessageResponseSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "appendChatMessageBodySchema": { "type": "object", "properties": { "clientMessageId": { "type": [ "string", "null" ], "minLength": 1, "maxLength": 240, "default": null }, "metadata": { "type": "object", "additionalProperties": { "$ref": "#/components/schemas/jsonValueSchema" }, "propertyNames": { "type": "string" }, "default": {} }, "parts": { "type": "array", "items": {}, "minItems": 1 }, "role": { "type": "string", "enum": [ "system", "user", "assistant", "tool" ] }, "status": { "type": "string", "enum": [ "complete", "partial", "failed" ], "default": "complete" } }, "required": [ "clientMessageId", "metadata", "parts", "role", "status" ] }, "appendChatMessageResponseSchema": { "type": "object", "properties": { "data": { "$ref": "#/components/schemas/chatMessageItemSchema" } }, "required": [ "data" ] }, "chatMessageItemSchema": { "type": "object", "properties": { "clientMessageId": { "type": [ "string", "null" ], "minLength": 1, "maxLength": 240 }, "createdAt": { "type": "string", "format": "date-time" }, "createdBy": { "type": [ "string", "null" ], "format": "uuid" }, "id": { "type": "string", "format": "uuid" }, "metadata": { "type": "object", "additionalProperties": { "$ref": "#/components/schemas/jsonValueSchema" }, "propertyNames": { "type": "string" } }, "organizationId": { "type": [ "string", "null" ], "format": "uuid" }, "parts": { "type": "array", "items": {} }, "role": { "type": "string", "enum": [ "system", "user", "assistant", "tool" ] }, "status": { "type": "string", "enum": [ "complete", "partial", "failed" ] }, "threadId": { "type": "string", "format": "uuid" } }, "required": [ "clientMessageId", "createdAt", "createdBy", "id", "metadata", "organizationId", "parts", "role", "status", "threadId" ] }, "jsonValueSchema": { "anyOf": [ { "type": "string" }, { "type": "number" }, { "type": "boolean" }, { "type": "null" }, { "type": "array", "items": { "$ref": "#/components/schemas/jsonValueSchema" } }, { "type": "object", "additionalProperties": { "$ref": "#/components/schemas/jsonValueSchema" }, "propertyNames": { "type": "string" } } ] } } ``` # List brands. (/docs/openapi/listBrands) Return brands for an organization the authenticated user can access. ## GET /v1/organizations/{organizationId}/brands ```json { "operationId": "listBrands", "summary": "List brands.", "description": "Return brands for an organization the authenticated user can access.", "tags": [ "Assets" ], "parameters": [ { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "responses": { "200": { "description": "Returns paginated brands.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/brandsListResponseSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "brandsListResponseSchema": { "type": "object" }, "organizationIdSchema": { "type": "string", "format": "uuid" } } ``` # Create a brand. (/docs/openapi/createBrand) Create a brand for the specified organization. ## POST /v1/organizations/{organizationId}/brands ```json { "operationId": "createBrand", "summary": "Create a brand.", "description": "Create a brand for the specified organization.", "tags": [ "Assets" ], "parameters": [ { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/brandInputSchema" } } }, "description": "The brand payload." }, "responses": { "201": { "description": "Returns the created brand.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/brandSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "brandIdSchema": { "type": "string", "enum": [ "centrakit", "mechup", "stabledock", "caresly", "propdock", "marryduck", "signaldock", "spacelio", "legaldock" ] }, "brandImageIdSchema": { "type": "string", "format": "uuid" }, "brandImageSchema": { "type": "object", "properties": { "altText": { "type": [ "string", "null" ] }, "brandId": { "$ref": "#/components/schemas/brandIdSchema" }, "createdAt": { "type": "string", "format": "date-time" }, "id": { "$ref": "#/components/schemas/brandImageIdSchema" }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "position": { "type": "integer", "minimum": 0 }, "updatedAt": { "type": "string", "format": "date-time" }, "url": { "type": "string", "format": "uri" } }, "required": [ "altText", "brandId", "createdAt", "id", "organizationId", "position", "updatedAt", "url" ] }, "brandInputSchema": { "type": "object", "properties": { "description": { "allOf": [ { "$ref": "#/components/schemas/nullableLongText" } ] }, "name": { "type": "string", "minLength": 1, "maxLength": 240 }, "slug": { "type": "string", "minLength": 1, "maxLength": 160, "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$" }, "website": { "allOf": [ { "$ref": "#/components/schemas/nullableUrlInputSchema" } ] } }, "required": [ "name" ] }, "brandSchema": { "type": "object", "properties": { "createdAt": { "type": "string", "format": "date-time" }, "description": { "type": [ "string", "null" ] }, "id": { "$ref": "#/components/schemas/brandIdSchema" }, "images": { "type": "array", "items": { "$ref": "#/components/schemas/brandImageSchema" } }, "logoUrl": { "type": [ "string", "null" ], "format": "uri" }, "name": { "type": "string", "minLength": 1, "maxLength": 240 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "slug": { "type": "string", "minLength": 1, "maxLength": 160, "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$" }, "tags": { "type": "array", "items": { "$ref": "#/components/schemas/organizationTagSchema" } }, "updatedAt": { "type": "string", "format": "date-time" }, "website": { "type": [ "string", "null" ] } }, "required": [ "createdAt", "description", "id", "images", "logoUrl", "name", "organizationId", "slug", "tags", "updatedAt", "website" ] }, "nullableLongText": { "type": [ "string", "null" ], "maxLength": 4000 }, "nullableUrlInputSchema": { "anyOf": [ { "type": "string", "maxLength": 400, "format": "uri" }, { "type": "null" } ] }, "organizationIdSchema": { "type": "string", "format": "uuid" }, "organizationTagSchema": { "type": "object", "properties": { "color": { "anyOf": [ { "$ref": "#/components/schemas/tagColorSchema" }, { "type": "null" } ] }, "createdAt": { "type": "string", "format": "date-time" }, "description": { "type": [ "string", "null" ] }, "id": { "$ref": "#/components/schemas/tagIdSchema" }, "kind": { "$ref": "#/components/schemas/tagKindSchema" }, "name": { "type": "string", "minLength": 1, "maxLength": 80 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "position": { "type": "integer", "minimum": 0 }, "slug": { "$ref": "#/components/schemas/tagSlugSchema" }, "updatedAt": { "type": "string", "format": "date-time" } }, "required": [ "color", "createdAt", "description", "id", "kind", "name", "organizationId", "position", "slug", "updatedAt" ] }, "tagColorSchema": { "type": "string", "pattern": "^#(?:[0-9a-f]{3}|[0-9a-f]{6})$" }, "tagIdSchema": { "type": "string", "format": "uuid" }, "tagKindSchema": { "type": "string", "enum": [ "task", "customer", "product" ] }, "tagSlugSchema": { "type": "string", "minLength": 1, "maxLength": 160, "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$" } } ``` # List customer assets. (/docs/openapi/listCustomerAssets) Return customer assets for an organization. ## GET /v1/organizations/{organizationId}/customer-assets ```json { "operationId": "listCustomerAssets", "summary": "List customer assets.", "description": "Return customer assets for an organization.", "tags": [ "Assets" ], "parameters": [ { "in": "query", "name": "customerId", "required": false, "schema": { "type": "string", "format": "uuid" }, "example": "123e4567-e89b-12d3-a456-426614174000" }, { "in": "query", "name": "limit", "required": false, "schema": { "type": "string" }, "example": "example" }, { "in": "query", "name": "offset", "required": false, "schema": { "type": "string" }, "example": "example" }, { "in": "query", "name": "q", "required": false, "schema": { "type": "string" }, "example": "example" }, { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "responses": { "200": { "description": "Returns paginated customer assets.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/customerAssetsListResponseSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "customerAssetsListResponseSchema": { "type": "object" }, "organizationIdSchema": { "type": "string", "format": "uuid" } } ``` # Create a customer asset. (/docs/openapi/createCustomerAsset) Create a customer asset for the specified organization. ## POST /v1/organizations/{organizationId}/customer-assets ```json { "operationId": "createCustomerAsset", "summary": "Create a customer asset.", "description": "Create a customer asset for the specified organization.", "tags": [ "Assets" ], "parameters": [ { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/customerAssetInputSchema" } } }, "description": "The customer asset payload." }, "responses": { "201": { "description": "Returns the created customer asset.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/customerAssetSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "assetCustomAttributeSchema": { "type": "object", "properties": { "booleanValue": { "type": [ "boolean", "null" ] }, "dateValue": { "$ref": "#/components/schemas/nullableDate" }, "numberValue": { "type": [ "number", "null" ] }, "textValue": { "type": [ "string", "null" ] }, "urlValue": { "type": [ "string", "null" ] }, "valueType": { "$ref": "#/components/schemas/customAttributeValueTypeSchema" }, "createdAt": { "type": "string", "format": "date-time" }, "customerAssetId": { "anyOf": [ { "$ref": "#/components/schemas/customerAssetIdSchema" }, { "type": "null" } ] }, "id": { "type": "string", "format": "uuid" }, "key": { "type": "string", "minLength": 1, "maxLength": 120 }, "label": { "type": "string", "minLength": 1, "maxLength": 160 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "position": { "type": "integer", "minimum": 0 }, "productId": { "anyOf": [ { "$ref": "#/components/schemas/productIdSchema" }, { "type": "null" } ] }, "updatedAt": { "type": "string", "format": "date-time" } }, "required": [ "booleanValue", "dateValue", "numberValue", "textValue", "urlValue", "valueType", "createdAt", "customerAssetId", "id", "key", "label", "organizationId", "position", "productId", "updatedAt" ] }, "assetImageIdSchema": { "type": "string", "format": "uuid" }, "assetImageSchema": { "type": "object", "properties": { "altText": { "type": [ "string", "null" ] }, "createdAt": { "type": "string", "format": "date-time" }, "id": { "$ref": "#/components/schemas/assetImageIdSchema" }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "position": { "type": "integer", "minimum": 0 }, "productId": { "anyOf": [ { "$ref": "#/components/schemas/productIdSchema" }, { "type": "null" } ] }, "updatedAt": { "type": "string", "format": "date-time" }, "url": { "type": "string", "format": "uri" } }, "required": [ "altText", "createdAt", "id", "organizationId", "position", "productId", "updatedAt", "url" ] }, "brandIdSchema": { "type": "string", "enum": [ "centrakit", "mechup", "stabledock", "caresly", "propdock", "marryduck", "signaldock", "spacelio", "legaldock" ] }, "currencyCodeSchema": { "type": "string", "pattern": "^[A-Z]{3}$" }, "customAttributeValueTypeSchema": { "type": "string", "enum": [ "text", "number", "boolean", "date", "url" ] }, "customerAddressSchema": { "type": "object", "properties": { "city": { "$ref": "#/components/schemas/nullableShortText" }, "country": { "$ref": "#/components/schemas/nullableShortText" }, "line1": { "$ref": "#/components/schemas/nullableShortText" }, "line2": { "$ref": "#/components/schemas/nullableShortText" }, "postalCode": { "$ref": "#/components/schemas/nullableShortText" }, "state": { "$ref": "#/components/schemas/nullableShortText" } }, "required": [ "city", "country", "line1", "line2", "postalCode", "state" ] }, "customerAssetConditionSchema": { "type": "string", "enum": [ "new", "good", "fair", "poor", "unknown" ] }, "customerAssetFileIdSchema": { "type": "string", "format": "uuid" }, "customerAssetFileSchema": { "type": "object", "properties": { "altText": { "type": [ "string", "null" ] }, "byteSize": { "type": "integer", "exclusiveMinimum": 0 }, "contentType": { "type": "string", "minLength": 1 }, "createdAt": { "type": "string", "format": "date-time" }, "createdBy": { "type": [ "string", "null" ], "format": "uuid" }, "customerAssetId": { "$ref": "#/components/schemas/customerAssetIdSchema" }, "fileName": { "type": "string", "minLength": 1 }, "id": { "$ref": "#/components/schemas/customerAssetFileIdSchema" }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "position": { "type": "integer", "minimum": 0 }, "updatedAt": { "type": "string", "format": "date-time" }, "url": { "type": "string", "format": "uri" } }, "required": [ "altText", "byteSize", "contentType", "createdAt", "createdBy", "customerAssetId", "fileName", "id", "organizationId", "position", "updatedAt", "url" ] }, "customerAssetIdSchema": { "type": "string", "format": "uuid" }, "customerAssetInputSchema": { "type": "object", "properties": { "assetTag": { "allOf": [ { "$ref": "#/components/schemas/nullableShortText" } ] }, "barcode": { "allOf": [ { "$ref": "#/components/schemas/nullableShortText" } ] }, "condition": { "allOf": [ { "$ref": "#/components/schemas/customerAssetConditionSchema" } ] }, "currency": { "allOf": [ { "$ref": "#/components/schemas/nullableCurrency" } ] }, "customerId": { "$ref": "#/components/schemas/customerIdSchema" }, "customerLocationId": { "type": [ "string", "null" ], "format": "uuid" }, "description": { "allOf": [ { "$ref": "#/components/schemas/nullableLongText" } ] }, "installedOn": { "allOf": [ { "$ref": "#/components/schemas/nullableDate" } ] }, "invoiceId": { "type": [ "string", "null" ], "format": "uuid" }, "productId": { "$ref": "#/components/schemas/productIdSchema" }, "purchasePrice": { "allOf": [ { "$ref": "#/components/schemas/nullableMoney" } ] }, "purchasedOn": { "allOf": [ { "$ref": "#/components/schemas/nullableDate" } ] }, "quoteId": { "type": [ "string", "null" ], "format": "uuid" }, "serialNumber": { "allOf": [ { "$ref": "#/components/schemas/nullableShortText" } ] }, "serviceIntervalDays": { "type": [ "integer", "null" ], "minimum": 1 }, "status": { "allOf": [ { "$ref": "#/components/schemas/customerAssetStatusSchema" } ] }, "warrantyExpiresOn": { "allOf": [ { "$ref": "#/components/schemas/nullableDate" } ] }, "warrantyProvider": { "allOf": [ { "$ref": "#/components/schemas/nullableShortText" } ] }, "warrantyReference": { "allOf": [ { "$ref": "#/components/schemas/nullableShortText" } ] } }, "required": [ "customerId", "productId" ] }, "customerAssetSchema": { "type": "object", "properties": { "assetTag": { "type": [ "string", "null" ] }, "barcode": { "type": [ "string", "null" ] }, "condition": { "$ref": "#/components/schemas/customerAssetConditionSchema" }, "createdAt": { "type": "string", "format": "date-time" }, "createdBy": { "type": [ "string", "null" ], "format": "uuid" }, "currency": { "type": [ "string", "null" ] }, "customAttributes": { "type": "array", "items": { "$ref": "#/components/schemas/assetCustomAttributeSchema" } }, "customerId": { "$ref": "#/components/schemas/customerIdSchema" }, "customerLocation": { "anyOf": [ { "$ref": "#/components/schemas/customerLocationSchema" }, { "type": "null" } ] }, "customerLocationId": { "type": [ "string", "null" ], "format": "uuid" }, "customerName": { "type": [ "string", "null" ] }, "description": { "type": [ "string", "null" ] }, "files": { "type": "array", "items": { "$ref": "#/components/schemas/customerAssetFileSchema" } }, "id": { "$ref": "#/components/schemas/customerAssetIdSchema" }, "installedOn": { "$ref": "#/components/schemas/nullableDate" }, "invoiceId": { "type": [ "string", "null" ], "format": "uuid" }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "product": { "anyOf": [ { "$ref": "#/components/schemas/productListItemSchema" }, { "type": "null" } ] }, "productId": { "$ref": "#/components/schemas/productIdSchema" }, "purchasePrice": { "type": [ "number", "null" ] }, "purchasedOn": { "$ref": "#/components/schemas/nullableDate" }, "quoteId": { "type": [ "string", "null" ], "format": "uuid" }, "serialNumber": { "type": [ "string", "null" ] }, "serviceIntervalDays": { "type": [ "integer", "null" ], "minimum": 1 }, "status": { "$ref": "#/components/schemas/customerAssetStatusSchema" }, "tags": { "type": "array", "items": { "$ref": "#/components/schemas/organizationTagSchema" } }, "updatedAt": { "type": "string", "format": "date-time" }, "updatedBy": { "type": [ "string", "null" ], "format": "uuid" }, "warrantyExpiresOn": { "$ref": "#/components/schemas/nullableDate" }, "warrantyProvider": { "type": [ "string", "null" ] }, "warrantyReference": { "type": [ "string", "null" ] } }, "required": [ "assetTag", "barcode", "condition", "createdAt", "createdBy", "currency", "customAttributes", "customerId", "customerLocation", "customerLocationId", "customerName", "description", "files", "id", "installedOn", "invoiceId", "organizationId", "product", "productId", "purchasePrice", "purchasedOn", "quoteId", "serialNumber", "serviceIntervalDays", "status", "tags", "updatedAt", "updatedBy", "warrantyExpiresOn", "warrantyProvider", "warrantyReference" ] }, "customerAssetStatusSchema": { "type": "string", "enum": [ "active", "inactive", "maintenance", "retired", "archived" ] }, "customerIdSchema": { "type": "string", "format": "uuid" }, "customerLocationSchema": { "type": "object", "properties": { "address": { "$ref": "#/components/schemas/customerAddressSchema" }, "createdAt": { "type": "string", "format": "date-time" }, "customerId": { "$ref": "#/components/schemas/customerIdSchema" }, "id": { "type": "string", "format": "uuid" }, "isPrimary": { "type": "boolean" }, "name": { "type": "string", "minLength": 1, "maxLength": 120 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "updatedAt": { "type": "string", "format": "date-time" } }, "required": [ "address", "createdAt", "customerId", "id", "isPrimary", "name", "organizationId", "updatedAt" ] }, "iconNameSchema": { "type": "string", "minLength": 1, "maxLength": 120, "pattern": "^[A-Za-z0-9]+Icon$" }, "inventoryLocationIdSchema": { "type": "string", "format": "uuid" }, "nullableCurrency": { "anyOf": [ { "$ref": "#/components/schemas/currencyCodeSchema" }, { "type": "null" } ] }, "nullableDate": { "type": [ "string", "null" ], "format": "date" }, "nullableLongText": { "type": [ "string", "null" ], "maxLength": 4000 }, "nullableMoney": { "type": [ "number", "null" ], "minimum": 0 }, "nullableShortText": { "type": [ "string", "null" ], "maxLength": 240 }, "organizationIdSchema": { "type": "string", "format": "uuid" }, "organizationTagSchema": { "type": "object", "properties": { "color": { "anyOf": [ { "$ref": "#/components/schemas/tagColorSchema" }, { "type": "null" } ] }, "createdAt": { "type": "string", "format": "date-time" }, "description": { "type": [ "string", "null" ] }, "id": { "$ref": "#/components/schemas/tagIdSchema" }, "kind": { "$ref": "#/components/schemas/tagKindSchema" }, "name": { "type": "string", "minLength": 1, "maxLength": 80 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "position": { "type": "integer", "minimum": 0 }, "slug": { "$ref": "#/components/schemas/tagSlugSchema" }, "updatedAt": { "type": "string", "format": "date-time" } }, "required": [ "color", "createdAt", "description", "id", "kind", "name", "organizationId", "position", "slug", "updatedAt" ] }, "productCategorySchema": { "type": "object", "properties": { "createdAt": { "type": "string", "format": "date-time" }, "description": { "type": [ "string", "null" ] }, "icon": { "$ref": "#/components/schemas/iconNameSchema" }, "id": { "type": "string", "format": "uuid" }, "name": { "type": "string", "minLength": 1, "maxLength": 120 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "parentId": { "type": [ "string", "null" ], "format": "uuid" }, "position": { "type": "integer", "minimum": 0 }, "slug": { "$ref": "#/components/schemas/slugSchema" }, "updatedAt": { "type": "string", "format": "date-time" } }, "required": [ "createdAt", "description", "icon", "id", "name", "organizationId", "parentId", "position", "slug", "updatedAt" ] }, "productIdSchema": { "type": "string", "format": "uuid" }, "productListItemSchema": { "type": "object", "properties": { "brand": { "type": [ "object", "null" ], "properties": { "createdAt": { "type": "string", "format": "date-time" }, "description": { "type": [ "string", "null" ] }, "id": { "$ref": "#/components/schemas/brandIdSchema" }, "logoUrl": { "type": [ "string", "null" ], "format": "uri" }, "name": { "type": "string", "minLength": 1, "maxLength": 240 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "slug": { "type": "string", "minLength": 1, "maxLength": 160, "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$" }, "updatedAt": { "type": "string", "format": "date-time" }, "website": { "type": [ "string", "null" ] } }, "required": [ "createdAt", "description", "id", "logoUrl", "name", "organizationId", "slug", "updatedAt", "website" ] }, "categories": { "type": "array", "items": { "$ref": "#/components/schemas/productCategorySchema" } }, "categoryIds": { "type": "array", "items": { "type": "string", "format": "uuid" } }, "currency": { "type": [ "string", "null" ] }, "defaultSellingPrice": { "type": [ "number", "null" ] }, "ean": { "type": [ "string", "null" ] }, "id": { "$ref": "#/components/schemas/productIdSchema" }, "images": { "type": "array", "items": { "$ref": "#/components/schemas/assetImageSchema" } }, "manufacturer": { "type": [ "string", "null" ] }, "manufacturerPartNumber": { "type": [ "string", "null" ] }, "name": { "type": "string", "minLength": 1 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "sku": { "type": [ "string", "null" ] }, "slug": { "$ref": "#/components/schemas/slugSchema" }, "status": { "$ref": "#/components/schemas/productStatusSchema" }, "suppliers": { "type": "array", "items": { "$ref": "#/components/schemas/productSupplierLinkSchema" } }, "tags": { "type": "array", "items": { "$ref": "#/components/schemas/organizationTagSchema" } }, "unitOfMeasure": { "type": [ "string", "null" ] }, "updatedAt": { "type": "string", "format": "date-time" }, "vatPercentage": { "type": [ "number", "null" ] }, "stockStatus": { "anyOf": [ { "$ref": "#/components/schemas/stockStatusSchema" }, { "type": "null" } ] } }, "required": [ "brand", "categories", "categoryIds", "currency", "defaultSellingPrice", "ean", "id", "images", "manufacturer", "manufacturerPartNumber", "name", "organizationId", "sku", "slug", "status", "suppliers", "tags", "unitOfMeasure", "updatedAt", "vatPercentage" ] }, "productStatusSchema": { "type": "string", "enum": [ "active", "discontinued", "archived" ] }, "productSupplierLinkSchema": { "type": "object", "properties": { "createdAt": { "type": "string", "format": "date-time" }, "currency": { "type": [ "string", "null" ] }, "externalId": { "type": [ "string", "null" ] }, "id": { "type": "string", "format": "uuid" }, "isActive": { "type": "boolean" }, "isPreferred": { "type": "boolean" }, "leadTimeDays": { "type": [ "integer", "null" ] }, "minOrderQuantity": { "type": [ "number", "null" ] }, "orderCode": { "type": [ "string", "null" ] }, "orderMultiple": { "type": [ "number", "null" ] }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "productId": { "$ref": "#/components/schemas/productIdSchema" }, "supplier": { "anyOf": [ { "$ref": "#/components/schemas/supplierSchema" }, { "type": "null" } ] }, "supplierId": { "$ref": "#/components/schemas/supplierIdSchema" }, "supplierSku": { "type": [ "string", "null" ] }, "unitCost": { "type": [ "number", "null" ] }, "updatedAt": { "type": "string", "format": "date-time" } }, "required": [ "createdAt", "currency", "externalId", "id", "isActive", "isPreferred", "leadTimeDays", "minOrderQuantity", "orderCode", "orderMultiple", "organizationId", "productId", "supplier", "supplierId", "supplierSku", "unitCost", "updatedAt" ] }, "slugSchema": { "type": "string", "minLength": 1, "maxLength": 160, "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$" }, "stockStatusSchema": { "type": "string", "enum": [ "not_tracked", "discontinued", "out_of_stock", "backordered", "low_stock", "in_stock" ] }, "supplierIdSchema": { "type": "string", "format": "uuid" }, "supplierSchema": { "type": "object", "properties": { "accountNumber": { "type": [ "string", "null" ] }, "addressCity": { "type": [ "string", "null" ] }, "addressCountry": { "type": [ "string", "null" ] }, "addressLine1": { "type": [ "string", "null" ] }, "addressLine2": { "type": [ "string", "null" ] }, "addressPostalCode": { "type": [ "string", "null" ] }, "addressState": { "type": [ "string", "null" ] }, "billingEmail": { "type": [ "string", "null" ] }, "btw": { "type": [ "string", "null" ] }, "code": { "type": [ "string", "null" ] }, "createdAt": { "type": "string", "format": "date-time" }, "currency": { "type": [ "string", "null" ] }, "defaultInventoryLocationId": { "anyOf": [ { "$ref": "#/components/schemas/inventoryLocationIdSchema" }, { "type": "null" } ] }, "defaultLeadTimeDays": { "type": [ "integer", "null" ] }, "email": { "type": [ "string", "null" ] }, "externalId": { "type": [ "string", "null" ] }, "id": { "$ref": "#/components/schemas/supplierIdSchema" }, "kvk": { "type": [ "string", "null" ] }, "name": { "type": "string", "minLength": 1, "maxLength": 240 }, "notes": { "type": [ "string", "null" ] }, "orderingEmail": { "type": [ "string", "null" ] }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "paymentTermsDays": { "type": [ "integer", "null" ] }, "phone": { "type": [ "string", "null" ] }, "source": { "type": [ "string", "null" ] }, "status": { "$ref": "#/components/schemas/supplierStatusSchema" }, "updatedAt": { "type": "string", "format": "date-time" }, "website": { "type": [ "string", "null" ] } }, "required": [ "accountNumber", "addressCity", "addressCountry", "addressLine1", "addressLine2", "addressPostalCode", "addressState", "billingEmail", "btw", "code", "createdAt", "currency", "defaultInventoryLocationId", "defaultLeadTimeDays", "email", "externalId", "id", "kvk", "name", "notes", "orderingEmail", "organizationId", "paymentTermsDays", "phone", "source", "status", "updatedAt", "website" ] }, "supplierStatusSchema": { "type": "string", "enum": [ "active", "inactive", "archived" ] }, "tagColorSchema": { "type": "string", "pattern": "^#(?:[0-9a-f]{3}|[0-9a-f]{6})$" }, "tagIdSchema": { "type": "string", "format": "uuid" }, "tagKindSchema": { "type": "string", "enum": [ "task", "customer", "product" ] }, "tagSlugSchema": { "type": "string", "minLength": 1, "maxLength": 160, "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$" } } ``` # List product categories. (/docs/openapi/listProductCategories) Return product categories for an organization. ## GET /v1/organizations/{organizationId}/product-categories ```json { "operationId": "listProductCategories", "summary": "List product categories.", "description": "Return product categories for an organization.", "tags": [ "Assets" ], "parameters": [ { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "responses": { "200": { "description": "Returns paginated product categories.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/productCategoriesListResponseSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "organizationIdSchema": { "type": "string", "format": "uuid" }, "productCategoriesListResponseSchema": { "type": "object" } } ``` # Create a product category. (/docs/openapi/createProductCategory) Create a product category for the specified organization. ## POST /v1/organizations/{organizationId}/product-categories ```json { "operationId": "createProductCategory", "summary": "Create a product category.", "description": "Create a product category for the specified organization.", "tags": [ "Assets" ], "parameters": [ { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/productCategoryInputSchema" } } }, "description": "The product category payload." }, "responses": { "201": { "description": "Returns the created product category.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/productCategorySchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "iconNameInputSchema": { "type": "string", "minLength": 1, "maxLength": 120, "pattern": "^[A-Za-z0-9]+Icon$" }, "iconNameSchema": { "type": "string", "minLength": 1, "maxLength": 120, "pattern": "^[A-Za-z0-9]+Icon$" }, "nullableLongText": { "type": [ "string", "null" ], "maxLength": 4000 }, "organizationIdSchema": { "type": "string", "format": "uuid" }, "productCategoryInputSchema": { "type": "object", "properties": { "description": { "allOf": [ { "$ref": "#/components/schemas/nullableLongText" } ] }, "icon": { "allOf": [ { "$ref": "#/components/schemas/iconNameInputSchema" } ] }, "name": { "type": "string", "minLength": 1, "maxLength": 120 }, "parentId": { "type": [ "string", "null" ], "format": "uuid" }, "position": { "type": "integer", "minimum": 0 }, "slug": { "allOf": [ { "$ref": "#/components/schemas/slugInputSchema" } ] } }, "required": [ "name" ] }, "productCategorySchema": { "type": "object", "properties": { "createdAt": { "type": "string", "format": "date-time" }, "description": { "type": [ "string", "null" ] }, "icon": { "$ref": "#/components/schemas/iconNameSchema" }, "id": { "type": "string", "format": "uuid" }, "name": { "type": "string", "minLength": 1, "maxLength": 120 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "parentId": { "type": [ "string", "null" ], "format": "uuid" }, "position": { "type": "integer", "minimum": 0 }, "slug": { "$ref": "#/components/schemas/slugSchema" }, "updatedAt": { "type": "string", "format": "date-time" } }, "required": [ "createdAt", "description", "icon", "id", "name", "organizationId", "parentId", "position", "slug", "updatedAt" ] }, "slugInputSchema": { "type": "string", "minLength": 1, "maxLength": 160, "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$" }, "slugSchema": { "type": "string", "minLength": 1, "maxLength": 160, "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$" } } ``` # List products. (/docs/openapi/listProducts) Return products for an organization the authenticated user can access. ## GET /v1/organizations/{organizationId}/products ```json { "operationId": "listProducts", "summary": "List products.", "description": "Return products for an organization the authenticated user can access.", "tags": [ "Assets" ], "parameters": [ { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "responses": { "200": { "description": "Returns paginated products.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/productsListResponseSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "organizationIdSchema": { "type": "string", "format": "uuid" }, "productsListResponseSchema": { "type": "object" } } ``` # Create a product. (/docs/openapi/createProduct) Create a product for the specified organization. ## POST /v1/organizations/{organizationId}/products ```json { "operationId": "createProduct", "summary": "Create a product.", "description": "Create a product for the specified organization.", "tags": [ "Assets" ], "parameters": [ { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/productInputSchema" } } }, "description": "The product payload.", "required": true }, "responses": { "201": { "description": "Returns the created product.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/productSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "assetCustomAttributeSchema": { "type": "object", "properties": { "booleanValue": { "type": [ "boolean", "null" ] }, "dateValue": { "$ref": "#/components/schemas/nullableDate" }, "numberValue": { "type": [ "number", "null" ] }, "textValue": { "type": [ "string", "null" ] }, "urlValue": { "type": [ "string", "null" ] }, "valueType": { "$ref": "#/components/schemas/customAttributeValueTypeSchema" }, "createdAt": { "type": "string", "format": "date-time" }, "customerAssetId": { "anyOf": [ { "$ref": "#/components/schemas/customerAssetIdSchema" }, { "type": "null" } ] }, "id": { "type": "string", "format": "uuid" }, "key": { "type": "string", "minLength": 1, "maxLength": 120 }, "label": { "type": "string", "minLength": 1, "maxLength": 160 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "position": { "type": "integer", "minimum": 0 }, "productId": { "anyOf": [ { "$ref": "#/components/schemas/productIdSchema" }, { "type": "null" } ] }, "updatedAt": { "type": "string", "format": "date-time" } }, "required": [ "booleanValue", "dateValue", "numberValue", "textValue", "urlValue", "valueType", "createdAt", "customerAssetId", "id", "key", "label", "organizationId", "position", "productId", "updatedAt" ] }, "assetImageIdSchema": { "type": "string", "format": "uuid" }, "assetImageSchema": { "type": "object", "properties": { "altText": { "type": [ "string", "null" ] }, "createdAt": { "type": "string", "format": "date-time" }, "id": { "$ref": "#/components/schemas/assetImageIdSchema" }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "position": { "type": "integer", "minimum": 0 }, "productId": { "anyOf": [ { "$ref": "#/components/schemas/productIdSchema" }, { "type": "null" } ] }, "updatedAt": { "type": "string", "format": "date-time" }, "url": { "type": "string", "format": "uri" } }, "required": [ "altText", "createdAt", "id", "organizationId", "position", "productId", "updatedAt", "url" ] }, "brandIdSchema": { "type": "string", "enum": [ "centrakit", "mechup", "stabledock", "caresly", "propdock", "marryduck", "signaldock", "spacelio", "legaldock" ] }, "currencyCodeSchema": { "type": "string", "pattern": "^[A-Z]{3}$" }, "customAttributeValueTypeSchema": { "type": "string", "enum": [ "text", "number", "boolean", "date", "url" ] }, "customerAssetIdSchema": { "type": "string", "format": "uuid" }, "digitalDeliveryKindSchema": { "type": "string", "enum": [ "file", "license_key", "link", "manual" ] }, "iconNameSchema": { "type": "string", "minLength": 1, "maxLength": 120, "pattern": "^[A-Za-z0-9]+Icon$" }, "inventoryLocationIdSchema": { "type": "string", "format": "uuid" }, "nullableCurrency": { "anyOf": [ { "$ref": "#/components/schemas/currencyCodeSchema" }, { "type": "null" } ] }, "nullableDate": { "type": [ "string", "null" ], "format": "date" }, "nullableLongText": { "type": [ "string", "null" ], "maxLength": 4000 }, "nullableMoney": { "type": [ "number", "null" ], "minimum": 0 }, "nullableShortText": { "type": [ "string", "null" ], "maxLength": 240 }, "organizationIdSchema": { "type": "string", "format": "uuid" }, "organizationTagSchema": { "type": "object", "properties": { "color": { "anyOf": [ { "$ref": "#/components/schemas/tagColorSchema" }, { "type": "null" } ] }, "createdAt": { "type": "string", "format": "date-time" }, "description": { "type": [ "string", "null" ] }, "id": { "$ref": "#/components/schemas/tagIdSchema" }, "kind": { "$ref": "#/components/schemas/tagKindSchema" }, "name": { "type": "string", "minLength": 1, "maxLength": 80 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "position": { "type": "integer", "minimum": 0 }, "slug": { "$ref": "#/components/schemas/tagSlugSchema" }, "updatedAt": { "type": "string", "format": "date-time" } }, "required": [ "color", "createdAt", "description", "id", "kind", "name", "organizationId", "position", "slug", "updatedAt" ] }, "productCategorySchema": { "type": "object", "properties": { "createdAt": { "type": "string", "format": "date-time" }, "description": { "type": [ "string", "null" ] }, "icon": { "$ref": "#/components/schemas/iconNameSchema" }, "id": { "type": "string", "format": "uuid" }, "name": { "type": "string", "minLength": 1, "maxLength": 120 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "parentId": { "type": [ "string", "null" ], "format": "uuid" }, "position": { "type": "integer", "minimum": 0 }, "slug": { "$ref": "#/components/schemas/slugSchema" }, "updatedAt": { "type": "string", "format": "date-time" } }, "required": [ "createdAt", "description", "icon", "id", "name", "organizationId", "parentId", "position", "slug", "updatedAt" ] }, "productIdSchema": { "type": "string", "format": "uuid" }, "productInputSchema": { "type": "object", "properties": { "brandId": { "anyOf": [ { "$ref": "#/components/schemas/brandIdSchema" }, { "type": "null" } ] }, "categoryIds": { "type": "array", "items": { "type": "string", "format": "uuid" } }, "countryOfOrigin": { "type": [ "string", "null" ], "pattern": "^[A-Z]{2}$" }, "currency": { "allOf": [ { "$ref": "#/components/schemas/nullableCurrency" } ] }, "defaultPurchasePrice": { "allOf": [ { "$ref": "#/components/schemas/nullableMoney" } ] }, "defaultSellingPrice": { "allOf": [ { "$ref": "#/components/schemas/nullableMoney" } ] }, "description": { "allOf": [ { "$ref": "#/components/schemas/nullableLongText" } ] }, "digitalDelivery": { "anyOf": [ { "$ref": "#/components/schemas/digitalDeliveryKindSchema" }, { "type": "null" } ] }, "ean": { "type": [ "string", "null" ], "maxLength": 120 }, "hsCode": { "type": [ "string", "null" ], "pattern": "^[0-9]{6,10}$" }, "manufacturer": { "allOf": [ { "$ref": "#/components/schemas/nullableShortText" } ] }, "manufacturerPartNumber": { "allOf": [ { "$ref": "#/components/schemas/nullableShortText" } ] }, "model": { "allOf": [ { "$ref": "#/components/schemas/nullableShortText" } ] }, "name": { "type": "string", "minLength": 1, "maxLength": 240 }, "productType": { "allOf": [ { "$ref": "#/components/schemas/productTypeSchema" } ] }, "sku": { "type": [ "string", "null" ], "maxLength": 120 }, "slug": { "allOf": [ { "$ref": "#/components/schemas/slugInputSchema" } ] }, "status": { "allOf": [ { "$ref": "#/components/schemas/productStatusSchema" } ] }, "tagIds": { "type": "array", "items": { "type": "string", "format": "uuid" }, "maxItems": 100 }, "trackInventory": { "type": "boolean" }, "unitOfMeasure": { "allOf": [ { "$ref": "#/components/schemas/unitOfMeasureInputSchema" } ] }, "vatPercentage": { "type": [ "number", "null" ], "minimum": 0, "maximum": 100 }, "weightGrams": { "type": [ "integer", "null" ], "minimum": 0 } }, "required": [ "name" ] }, "productOptionSchema": { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "name": { "type": "string" }, "position": { "type": "integer" }, "values": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "position": { "type": "integer" }, "value": { "type": "string" } }, "required": [ "id", "position", "value" ] } } }, "required": [ "id", "name", "position", "values" ] }, "productSchema": { "type": "object", "properties": { "brand": { "type": [ "object", "null" ], "properties": { "createdAt": { "type": "string", "format": "date-time" }, "description": { "type": [ "string", "null" ] }, "id": { "$ref": "#/components/schemas/brandIdSchema" }, "logoUrl": { "type": [ "string", "null" ], "format": "uri" }, "name": { "type": "string", "minLength": 1, "maxLength": 240 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "slug": { "type": "string", "minLength": 1, "maxLength": 160, "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$" }, "updatedAt": { "type": "string", "format": "date-time" }, "website": { "type": [ "string", "null" ] } }, "required": [ "createdAt", "description", "id", "logoUrl", "name", "organizationId", "slug", "updatedAt", "website" ] }, "brandId": { "anyOf": [ { "$ref": "#/components/schemas/brandIdSchema" }, { "type": "null" } ] }, "categories": { "type": "array", "items": { "$ref": "#/components/schemas/productCategorySchema" } }, "categoryIds": { "type": "array", "items": { "type": "string", "format": "uuid" } }, "countryOfOrigin": { "type": [ "string", "null" ] }, "createdAt": { "type": "string", "format": "date-time" }, "createdBy": { "type": [ "string", "null" ], "format": "uuid" }, "currency": { "type": [ "string", "null" ] }, "customAttributes": { "type": "array", "items": { "$ref": "#/components/schemas/assetCustomAttributeSchema" } }, "defaultPurchasePrice": { "type": [ "number", "null" ] }, "defaultSellingPrice": { "type": [ "number", "null" ] }, "description": { "type": [ "string", "null" ] }, "digitalDelivery": { "anyOf": [ { "$ref": "#/components/schemas/digitalDeliveryKindSchema" }, { "type": "null" } ] }, "ean": { "type": [ "string", "null" ] }, "hsCode": { "type": [ "string", "null" ] }, "id": { "$ref": "#/components/schemas/productIdSchema" }, "images": { "type": "array", "items": { "$ref": "#/components/schemas/assetImageSchema" } }, "manufacturer": { "type": [ "string", "null" ] }, "manufacturerPartNumber": { "type": [ "string", "null" ] }, "model": { "type": [ "string", "null" ] }, "name": { "type": "string", "minLength": 1 }, "options": { "type": "array", "items": { "$ref": "#/components/schemas/productOptionSchema" } }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "productType": { "$ref": "#/components/schemas/productTypeSchema" }, "sku": { "type": [ "string", "null" ] }, "slug": { "$ref": "#/components/schemas/slugSchema" }, "status": { "$ref": "#/components/schemas/productStatusSchema" }, "suppliers": { "type": "array", "items": { "$ref": "#/components/schemas/productSupplierLinkSchema" } }, "tags": { "type": "array", "items": { "$ref": "#/components/schemas/organizationTagSchema" } }, "trackInventory": { "type": "boolean" }, "unitOfMeasure": { "type": [ "string", "null" ] }, "updatedAt": { "type": "string", "format": "date-time" }, "updatedBy": { "type": [ "string", "null" ], "format": "uuid" }, "variants": { "type": "array", "items": { "$ref": "#/components/schemas/productVariantSchema" } }, "vatPercentage": { "type": [ "number", "null" ] }, "weightGrams": { "type": [ "integer", "null" ] } }, "required": [ "brand", "brandId", "categories", "categoryIds", "countryOfOrigin", "createdAt", "createdBy", "currency", "customAttributes", "defaultPurchasePrice", "defaultSellingPrice", "description", "digitalDelivery", "ean", "hsCode", "id", "images", "manufacturer", "manufacturerPartNumber", "model", "name", "options", "organizationId", "productType", "sku", "slug", "status", "suppliers", "tags", "trackInventory", "unitOfMeasure", "updatedAt", "updatedBy", "variants", "vatPercentage", "weightGrams" ] }, "productStatusSchema": { "type": "string", "enum": [ "active", "discontinued", "archived" ] }, "productSupplierLinkSchema": { "type": "object", "properties": { "createdAt": { "type": "string", "format": "date-time" }, "currency": { "type": [ "string", "null" ] }, "externalId": { "type": [ "string", "null" ] }, "id": { "type": "string", "format": "uuid" }, "isActive": { "type": "boolean" }, "isPreferred": { "type": "boolean" }, "leadTimeDays": { "type": [ "integer", "null" ] }, "minOrderQuantity": { "type": [ "number", "null" ] }, "orderCode": { "type": [ "string", "null" ] }, "orderMultiple": { "type": [ "number", "null" ] }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "productId": { "$ref": "#/components/schemas/productIdSchema" }, "supplier": { "anyOf": [ { "$ref": "#/components/schemas/supplierSchema" }, { "type": "null" } ] }, "supplierId": { "$ref": "#/components/schemas/supplierIdSchema" }, "supplierSku": { "type": [ "string", "null" ] }, "unitCost": { "type": [ "number", "null" ] }, "updatedAt": { "type": "string", "format": "date-time" } }, "required": [ "createdAt", "currency", "externalId", "id", "isActive", "isPreferred", "leadTimeDays", "minOrderQuantity", "orderCode", "orderMultiple", "organizationId", "productId", "supplier", "supplierId", "supplierSku", "unitCost", "updatedAt" ] }, "productTypeSchema": { "type": "string", "enum": [ "physical", "digital" ] }, "productVariantIdSchema": { "type": "string", "format": "uuid" }, "productVariantSchema": { "type": "object", "properties": { "allowBackorder": { "type": "boolean" }, "averageCost": { "type": [ "number", "null" ] }, "ean": { "type": [ "string", "null" ] }, "id": { "$ref": "#/components/schemas/productVariantIdSchema" }, "imageId": { "type": [ "string", "null" ], "format": "uuid" }, "isDefault": { "type": "boolean" }, "manufacturerPartNumber": { "type": [ "string", "null" ] }, "optionValueIds": { "type": "array", "items": { "type": "string", "format": "uuid" } }, "position": { "type": "integer" }, "productId": { "$ref": "#/components/schemas/productIdSchema" }, "purchasePrice": { "type": [ "number", "null" ] }, "sellingPrice": { "type": [ "number", "null" ] }, "sku": { "type": [ "string", "null" ] }, "status": { "$ref": "#/components/schemas/productStatusSchema" }, "title": { "type": "string" }, "weightGrams": { "type": [ "integer", "null" ] } }, "required": [ "allowBackorder", "averageCost", "ean", "id", "imageId", "isDefault", "manufacturerPartNumber", "optionValueIds", "position", "productId", "purchasePrice", "sellingPrice", "sku", "status", "title", "weightGrams" ] }, "slugInputSchema": { "type": "string", "minLength": 1, "maxLength": 160, "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$" }, "slugSchema": { "type": "string", "minLength": 1, "maxLength": 160, "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$" }, "supplierIdSchema": { "type": "string", "format": "uuid" }, "supplierSchema": { "type": "object", "properties": { "accountNumber": { "type": [ "string", "null" ] }, "addressCity": { "type": [ "string", "null" ] }, "addressCountry": { "type": [ "string", "null" ] }, "addressLine1": { "type": [ "string", "null" ] }, "addressLine2": { "type": [ "string", "null" ] }, "addressPostalCode": { "type": [ "string", "null" ] }, "addressState": { "type": [ "string", "null" ] }, "billingEmail": { "type": [ "string", "null" ] }, "btw": { "type": [ "string", "null" ] }, "code": { "type": [ "string", "null" ] }, "createdAt": { "type": "string", "format": "date-time" }, "currency": { "type": [ "string", "null" ] }, "defaultInventoryLocationId": { "anyOf": [ { "$ref": "#/components/schemas/inventoryLocationIdSchema" }, { "type": "null" } ] }, "defaultLeadTimeDays": { "type": [ "integer", "null" ] }, "email": { "type": [ "string", "null" ] }, "externalId": { "type": [ "string", "null" ] }, "id": { "$ref": "#/components/schemas/supplierIdSchema" }, "kvk": { "type": [ "string", "null" ] }, "name": { "type": "string", "minLength": 1, "maxLength": 240 }, "notes": { "type": [ "string", "null" ] }, "orderingEmail": { "type": [ "string", "null" ] }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "paymentTermsDays": { "type": [ "integer", "null" ] }, "phone": { "type": [ "string", "null" ] }, "source": { "type": [ "string", "null" ] }, "status": { "$ref": "#/components/schemas/supplierStatusSchema" }, "updatedAt": { "type": "string", "format": "date-time" }, "website": { "type": [ "string", "null" ] } }, "required": [ "accountNumber", "addressCity", "addressCountry", "addressLine1", "addressLine2", "addressPostalCode", "addressState", "billingEmail", "btw", "code", "createdAt", "currency", "defaultInventoryLocationId", "defaultLeadTimeDays", "email", "externalId", "id", "kvk", "name", "notes", "orderingEmail", "organizationId", "paymentTermsDays", "phone", "source", "status", "updatedAt", "website" ] }, "supplierStatusSchema": { "type": "string", "enum": [ "active", "inactive", "archived" ] }, "tagColorSchema": { "type": "string", "pattern": "^#(?:[0-9a-f]{3}|[0-9a-f]{6})$" }, "tagIdSchema": { "type": "string", "format": "uuid" }, "tagKindSchema": { "type": "string", "enum": [ "task", "customer", "product" ] }, "tagSlugSchema": { "type": "string", "minLength": 1, "maxLength": 160, "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$" }, "unitOfMeasureInputSchema": { "type": [ "string", "null" ], "maxLength": 240 } } ``` # List suppliers. (/docs/openapi/listSuppliers) Return suppliers for an organization. ## GET /v1/organizations/{organizationId}/suppliers ```json { "operationId": "listSuppliers", "summary": "List suppliers.", "description": "Return suppliers for an organization.", "tags": [ "Assets" ], "parameters": [ { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "responses": { "200": { "description": "Returns suppliers.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/suppliersResponseSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "inventoryLocationIdSchema": { "type": "string", "format": "uuid" }, "listPaginationSchema": { "type": "object", "properties": { "hasMore": { "type": "boolean" }, "limit": { "type": "integer", "minimum": 1, "maximum": 100 }, "offset": { "type": "integer", "minimum": 0 }, "total": { "type": "integer", "minimum": 0 }, "totalIsEstimate": { "type": "boolean" } }, "required": [ "hasMore", "limit", "offset", "total" ] }, "organizationIdSchema": { "type": "string", "format": "uuid" }, "supplierIdSchema": { "type": "string", "format": "uuid" }, "supplierSchema": { "type": "object", "properties": { "accountNumber": { "type": [ "string", "null" ] }, "addressCity": { "type": [ "string", "null" ] }, "addressCountry": { "type": [ "string", "null" ] }, "addressLine1": { "type": [ "string", "null" ] }, "addressLine2": { "type": [ "string", "null" ] }, "addressPostalCode": { "type": [ "string", "null" ] }, "addressState": { "type": [ "string", "null" ] }, "billingEmail": { "type": [ "string", "null" ] }, "btw": { "type": [ "string", "null" ] }, "code": { "type": [ "string", "null" ] }, "createdAt": { "type": "string", "format": "date-time" }, "currency": { "type": [ "string", "null" ] }, "defaultInventoryLocationId": { "anyOf": [ { "$ref": "#/components/schemas/inventoryLocationIdSchema" }, { "type": "null" } ] }, "defaultLeadTimeDays": { "type": [ "integer", "null" ] }, "email": { "type": [ "string", "null" ] }, "externalId": { "type": [ "string", "null" ] }, "id": { "$ref": "#/components/schemas/supplierIdSchema" }, "kvk": { "type": [ "string", "null" ] }, "name": { "type": "string", "minLength": 1, "maxLength": 240 }, "notes": { "type": [ "string", "null" ] }, "orderingEmail": { "type": [ "string", "null" ] }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "paymentTermsDays": { "type": [ "integer", "null" ] }, "phone": { "type": [ "string", "null" ] }, "source": { "type": [ "string", "null" ] }, "status": { "$ref": "#/components/schemas/supplierStatusSchema" }, "updatedAt": { "type": "string", "format": "date-time" }, "website": { "type": [ "string", "null" ] } }, "required": [ "accountNumber", "addressCity", "addressCountry", "addressLine1", "addressLine2", "addressPostalCode", "addressState", "billingEmail", "btw", "code", "createdAt", "currency", "defaultInventoryLocationId", "defaultLeadTimeDays", "email", "externalId", "id", "kvk", "name", "notes", "orderingEmail", "organizationId", "paymentTermsDays", "phone", "source", "status", "updatedAt", "website" ] }, "supplierStatusSchema": { "type": "string", "enum": [ "active", "inactive", "archived" ] }, "suppliersResponseSchema": { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/components/schemas/supplierSchema" } }, "pagination": { "$ref": "#/components/schemas/listPaginationSchema" } }, "required": [ "data", "pagination" ] } } ``` # Create a supplier. (/docs/openapi/createSupplier) Create a supplier for an organization. ## POST /v1/organizations/{organizationId}/suppliers ```json { "operationId": "createSupplier", "summary": "Create a supplier.", "description": "Create a supplier for an organization.", "tags": [ "Assets" ], "parameters": [ { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/supplierInputSchema" } } }, "description": "The supplier payload." }, "responses": { "201": { "description": "Returns the created supplier.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/supplierSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "inventoryLocationIdSchema": { "type": "string", "format": "uuid" }, "nullableEmailInputSchema": { "type": [ "string", "null" ], "format": "email", "maxLength": 320 }, "nullableLongText": { "type": [ "string", "null" ], "maxLength": 4000 }, "nullableNonNegativeInt": { "type": [ "integer", "null" ], "minimum": 0 }, "nullablePhoneInputSchema": { "type": [ "string", "null" ], "maxLength": 20 }, "nullableShortText": { "type": [ "string", "null" ], "maxLength": 240 }, "nullableUrlInputSchema": { "anyOf": [ { "type": "string", "maxLength": 400, "format": "uri" }, { "type": "null" } ] }, "organizationIdSchema": { "type": "string", "format": "uuid" }, "supplierIdSchema": { "type": "string", "format": "uuid" }, "supplierInputSchema": { "type": "object", "properties": { "accountNumber": { "allOf": [ { "$ref": "#/components/schemas/nullableShortText" } ] }, "addressCity": { "allOf": [ { "$ref": "#/components/schemas/nullableShortText" } ] }, "addressCountry": { "allOf": [ { "$ref": "#/components/schemas/nullableShortText" } ] }, "addressLine1": { "allOf": [ { "$ref": "#/components/schemas/nullableShortText" } ] }, "addressLine2": { "allOf": [ { "$ref": "#/components/schemas/nullableShortText" } ] }, "addressPostalCode": { "allOf": [ { "$ref": "#/components/schemas/nullableShortText" } ] }, "addressState": { "allOf": [ { "$ref": "#/components/schemas/nullableShortText" } ] }, "billingEmail": { "allOf": [ { "$ref": "#/components/schemas/nullableEmailInputSchema" } ] }, "btw": { "type": [ "string", "null" ], "maxLength": 40 }, "code": { "allOf": [ { "$ref": "#/components/schemas/nullableShortText" } ] }, "currency": { "type": [ "string", "null" ], "pattern": "^[A-Z]{3}$" }, "defaultInventoryLocationId": { "anyOf": [ { "$ref": "#/components/schemas/inventoryLocationIdSchema" }, { "type": "null" } ] }, "defaultLeadTimeDays": { "allOf": [ { "$ref": "#/components/schemas/nullableNonNegativeInt" } ] }, "email": { "allOf": [ { "$ref": "#/components/schemas/nullableEmailInputSchema" } ] }, "externalId": { "allOf": [ { "$ref": "#/components/schemas/nullableShortText" } ] }, "kvk": { "type": [ "string", "null" ], "maxLength": 40 }, "name": { "type": "string", "minLength": 1, "maxLength": 240 }, "notes": { "allOf": [ { "$ref": "#/components/schemas/nullableLongText" } ] }, "orderingEmail": { "allOf": [ { "$ref": "#/components/schemas/nullableEmailInputSchema" } ] }, "paymentTermsDays": { "allOf": [ { "$ref": "#/components/schemas/nullableNonNegativeInt" } ] }, "phone": { "allOf": [ { "$ref": "#/components/schemas/nullablePhoneInputSchema" } ] }, "source": { "allOf": [ { "$ref": "#/components/schemas/nullableShortText" } ] }, "status": { "allOf": [ { "$ref": "#/components/schemas/supplierStatusSchema" } ] }, "website": { "allOf": [ { "$ref": "#/components/schemas/nullableUrlInputSchema" } ] } }, "required": [ "name" ] }, "supplierSchema": { "type": "object", "properties": { "accountNumber": { "type": [ "string", "null" ] }, "addressCity": { "type": [ "string", "null" ] }, "addressCountry": { "type": [ "string", "null" ] }, "addressLine1": { "type": [ "string", "null" ] }, "addressLine2": { "type": [ "string", "null" ] }, "addressPostalCode": { "type": [ "string", "null" ] }, "addressState": { "type": [ "string", "null" ] }, "billingEmail": { "type": [ "string", "null" ] }, "btw": { "type": [ "string", "null" ] }, "code": { "type": [ "string", "null" ] }, "createdAt": { "type": "string", "format": "date-time" }, "currency": { "type": [ "string", "null" ] }, "defaultInventoryLocationId": { "anyOf": [ { "$ref": "#/components/schemas/inventoryLocationIdSchema" }, { "type": "null" } ] }, "defaultLeadTimeDays": { "type": [ "integer", "null" ] }, "email": { "type": [ "string", "null" ] }, "externalId": { "type": [ "string", "null" ] }, "id": { "$ref": "#/components/schemas/supplierIdSchema" }, "kvk": { "type": [ "string", "null" ] }, "name": { "type": "string", "minLength": 1, "maxLength": 240 }, "notes": { "type": [ "string", "null" ] }, "orderingEmail": { "type": [ "string", "null" ] }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "paymentTermsDays": { "type": [ "integer", "null" ] }, "phone": { "type": [ "string", "null" ] }, "source": { "type": [ "string", "null" ] }, "status": { "$ref": "#/components/schemas/supplierStatusSchema" }, "updatedAt": { "type": "string", "format": "date-time" }, "website": { "type": [ "string", "null" ] } }, "required": [ "accountNumber", "addressCity", "addressCountry", "addressLine1", "addressLine2", "addressPostalCode", "addressState", "billingEmail", "btw", "code", "createdAt", "currency", "defaultInventoryLocationId", "defaultLeadTimeDays", "email", "externalId", "id", "kvk", "name", "notes", "orderingEmail", "organizationId", "paymentTermsDays", "phone", "source", "status", "updatedAt", "website" ] }, "supplierStatusSchema": { "type": "string", "enum": [ "active", "inactive", "archived" ] } } ``` # Get a brand. (/docs/openapi/getBrand) Return a brand for the specified organization. ## GET /v1/organizations/{organizationId}/brands/{brandId} ```json { "operationId": "getBrand", "summary": "Get a brand.", "description": "Return a brand for the specified organization.", "tags": [ "Assets" ], "parameters": [ { "in": "path", "name": "brandId", "required": true, "schema": { "$ref": "#/components/schemas/brandIdSchema" }, "example": "123" }, { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "responses": { "200": { "description": "Returns the requested brand.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/brandSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "brandIdSchema": { "type": "string", "enum": [ "centrakit", "mechup", "stabledock", "caresly", "propdock", "marryduck", "signaldock", "spacelio", "legaldock" ] }, "brandImageIdSchema": { "type": "string", "format": "uuid" }, "brandImageSchema": { "type": "object", "properties": { "altText": { "type": [ "string", "null" ] }, "brandId": { "$ref": "#/components/schemas/brandIdSchema" }, "createdAt": { "type": "string", "format": "date-time" }, "id": { "$ref": "#/components/schemas/brandImageIdSchema" }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "position": { "type": "integer", "minimum": 0 }, "updatedAt": { "type": "string", "format": "date-time" }, "url": { "type": "string", "format": "uri" } }, "required": [ "altText", "brandId", "createdAt", "id", "organizationId", "position", "updatedAt", "url" ] }, "brandSchema": { "type": "object", "properties": { "createdAt": { "type": "string", "format": "date-time" }, "description": { "type": [ "string", "null" ] }, "id": { "$ref": "#/components/schemas/brandIdSchema" }, "images": { "type": "array", "items": { "$ref": "#/components/schemas/brandImageSchema" } }, "logoUrl": { "type": [ "string", "null" ], "format": "uri" }, "name": { "type": "string", "minLength": 1, "maxLength": 240 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "slug": { "type": "string", "minLength": 1, "maxLength": 160, "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$" }, "tags": { "type": "array", "items": { "$ref": "#/components/schemas/organizationTagSchema" } }, "updatedAt": { "type": "string", "format": "date-time" }, "website": { "type": [ "string", "null" ] } }, "required": [ "createdAt", "description", "id", "images", "logoUrl", "name", "organizationId", "slug", "tags", "updatedAt", "website" ] }, "organizationIdSchema": { "type": "string", "format": "uuid" }, "organizationTagSchema": { "type": "object", "properties": { "color": { "anyOf": [ { "$ref": "#/components/schemas/tagColorSchema" }, { "type": "null" } ] }, "createdAt": { "type": "string", "format": "date-time" }, "description": { "type": [ "string", "null" ] }, "id": { "$ref": "#/components/schemas/tagIdSchema" }, "kind": { "$ref": "#/components/schemas/tagKindSchema" }, "name": { "type": "string", "minLength": 1, "maxLength": 80 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "position": { "type": "integer", "minimum": 0 }, "slug": { "$ref": "#/components/schemas/tagSlugSchema" }, "updatedAt": { "type": "string", "format": "date-time" } }, "required": [ "color", "createdAt", "description", "id", "kind", "name", "organizationId", "position", "slug", "updatedAt" ] }, "tagColorSchema": { "type": "string", "pattern": "^#(?:[0-9a-f]{3}|[0-9a-f]{6})$" }, "tagIdSchema": { "type": "string", "format": "uuid" }, "tagKindSchema": { "type": "string", "enum": [ "task", "customer", "product" ] }, "tagSlugSchema": { "type": "string", "minLength": 1, "maxLength": 160, "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$" } } ``` # Update a brand. (/docs/openapi/updateBrand) Update a brand for the specified organization. ## PATCH /v1/organizations/{organizationId}/brands/{brandId} ```json { "operationId": "updateBrand", "summary": "Update a brand.", "description": "Update a brand for the specified organization.", "tags": [ "Assets" ], "parameters": [ { "in": "path", "name": "brandId", "required": true, "schema": { "$ref": "#/components/schemas/brandIdSchema" }, "example": "123" }, { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/brandInputSchema" } } }, "description": "The brand payload." }, "responses": { "200": { "description": "Returns the updated brand.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/brandSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "brandIdSchema": { "type": "string", "enum": [ "centrakit", "mechup", "stabledock", "caresly", "propdock", "marryduck", "signaldock", "spacelio", "legaldock" ] }, "brandImageIdSchema": { "type": "string", "format": "uuid" }, "brandImageSchema": { "type": "object", "properties": { "altText": { "type": [ "string", "null" ] }, "brandId": { "$ref": "#/components/schemas/brandIdSchema" }, "createdAt": { "type": "string", "format": "date-time" }, "id": { "$ref": "#/components/schemas/brandImageIdSchema" }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "position": { "type": "integer", "minimum": 0 }, "updatedAt": { "type": "string", "format": "date-time" }, "url": { "type": "string", "format": "uri" } }, "required": [ "altText", "brandId", "createdAt", "id", "organizationId", "position", "updatedAt", "url" ] }, "brandInputSchema": { "type": "object", "properties": { "description": { "allOf": [ { "$ref": "#/components/schemas/nullableLongText" } ] }, "name": { "type": "string", "minLength": 1, "maxLength": 240 }, "slug": { "type": "string", "minLength": 1, "maxLength": 160, "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$" }, "website": { "allOf": [ { "$ref": "#/components/schemas/nullableUrlInputSchema" } ] } }, "required": [ "name" ] }, "brandSchema": { "type": "object", "properties": { "createdAt": { "type": "string", "format": "date-time" }, "description": { "type": [ "string", "null" ] }, "id": { "$ref": "#/components/schemas/brandIdSchema" }, "images": { "type": "array", "items": { "$ref": "#/components/schemas/brandImageSchema" } }, "logoUrl": { "type": [ "string", "null" ], "format": "uri" }, "name": { "type": "string", "minLength": 1, "maxLength": 240 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "slug": { "type": "string", "minLength": 1, "maxLength": 160, "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$" }, "tags": { "type": "array", "items": { "$ref": "#/components/schemas/organizationTagSchema" } }, "updatedAt": { "type": "string", "format": "date-time" }, "website": { "type": [ "string", "null" ] } }, "required": [ "createdAt", "description", "id", "images", "logoUrl", "name", "organizationId", "slug", "tags", "updatedAt", "website" ] }, "nullableLongText": { "type": [ "string", "null" ], "maxLength": 4000 }, "nullableUrlInputSchema": { "anyOf": [ { "type": "string", "maxLength": 400, "format": "uri" }, { "type": "null" } ] }, "organizationIdSchema": { "type": "string", "format": "uuid" }, "organizationTagSchema": { "type": "object", "properties": { "color": { "anyOf": [ { "$ref": "#/components/schemas/tagColorSchema" }, { "type": "null" } ] }, "createdAt": { "type": "string", "format": "date-time" }, "description": { "type": [ "string", "null" ] }, "id": { "$ref": "#/components/schemas/tagIdSchema" }, "kind": { "$ref": "#/components/schemas/tagKindSchema" }, "name": { "type": "string", "minLength": 1, "maxLength": 80 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "position": { "type": "integer", "minimum": 0 }, "slug": { "$ref": "#/components/schemas/tagSlugSchema" }, "updatedAt": { "type": "string", "format": "date-time" } }, "required": [ "color", "createdAt", "description", "id", "kind", "name", "organizationId", "position", "slug", "updatedAt" ] }, "tagColorSchema": { "type": "string", "pattern": "^#(?:[0-9a-f]{3}|[0-9a-f]{6})$" }, "tagIdSchema": { "type": "string", "format": "uuid" }, "tagKindSchema": { "type": "string", "enum": [ "task", "customer", "product" ] }, "tagSlugSchema": { "type": "string", "minLength": 1, "maxLength": 160, "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$" } } ``` # Delete a brand. (/docs/openapi/deleteBrand) Delete a brand for the specified organization. ## DELETE /v1/organizations/{organizationId}/brands/{brandId} ```json { "operationId": "deleteBrand", "summary": "Delete a brand.", "description": "Delete a brand for the specified organization.", "tags": [ "Assets" ], "parameters": [ { "in": "path", "name": "brandId", "required": true, "schema": { "$ref": "#/components/schemas/brandIdSchema" }, "example": "123" }, { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "responses": { "200": { "description": "Returns a success payload when the brand is deleted.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/publicApiSuccessResponseSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "brandIdSchema": { "type": "string", "enum": [ "centrakit", "mechup", "stabledock", "caresly", "propdock", "marryduck", "signaldock", "spacelio", "legaldock" ] }, "organizationIdSchema": { "type": "string", "format": "uuid" }, "publicApiSuccessResponseSchema": { "type": "object", "properties": { "success": { "type": "boolean", "enum": [ true ] } }, "required": [ "success" ] } } ``` # Get a customer asset. (/docs/openapi/getCustomerAsset) Return a customer asset for the specified organization. ## GET /v1/organizations/{organizationId}/customer-assets/{customerAssetId} ```json { "operationId": "getCustomerAsset", "summary": "Get a customer asset.", "description": "Return a customer asset for the specified organization.", "tags": [ "Assets" ], "parameters": [ { "in": "path", "name": "customerAssetId", "required": true, "schema": { "$ref": "#/components/schemas/customerAssetIdSchema" }, "example": "123" }, { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "responses": { "200": { "description": "Returns the requested customer asset.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/customerAssetSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "assetCustomAttributeSchema": { "type": "object", "properties": { "booleanValue": { "type": [ "boolean", "null" ] }, "dateValue": { "$ref": "#/components/schemas/nullableDate" }, "numberValue": { "type": [ "number", "null" ] }, "textValue": { "type": [ "string", "null" ] }, "urlValue": { "type": [ "string", "null" ] }, "valueType": { "$ref": "#/components/schemas/customAttributeValueTypeSchema" }, "createdAt": { "type": "string", "format": "date-time" }, "customerAssetId": { "anyOf": [ { "$ref": "#/components/schemas/customerAssetIdSchema" }, { "type": "null" } ] }, "id": { "type": "string", "format": "uuid" }, "key": { "type": "string", "minLength": 1, "maxLength": 120 }, "label": { "type": "string", "minLength": 1, "maxLength": 160 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "position": { "type": "integer", "minimum": 0 }, "productId": { "anyOf": [ { "$ref": "#/components/schemas/productIdSchema" }, { "type": "null" } ] }, "updatedAt": { "type": "string", "format": "date-time" } }, "required": [ "booleanValue", "dateValue", "numberValue", "textValue", "urlValue", "valueType", "createdAt", "customerAssetId", "id", "key", "label", "organizationId", "position", "productId", "updatedAt" ] }, "assetImageIdSchema": { "type": "string", "format": "uuid" }, "assetImageSchema": { "type": "object", "properties": { "altText": { "type": [ "string", "null" ] }, "createdAt": { "type": "string", "format": "date-time" }, "id": { "$ref": "#/components/schemas/assetImageIdSchema" }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "position": { "type": "integer", "minimum": 0 }, "productId": { "anyOf": [ { "$ref": "#/components/schemas/productIdSchema" }, { "type": "null" } ] }, "updatedAt": { "type": "string", "format": "date-time" }, "url": { "type": "string", "format": "uri" } }, "required": [ "altText", "createdAt", "id", "organizationId", "position", "productId", "updatedAt", "url" ] }, "brandIdSchema": { "type": "string", "enum": [ "centrakit", "mechup", "stabledock", "caresly", "propdock", "marryduck", "signaldock", "spacelio", "legaldock" ] }, "customAttributeValueTypeSchema": { "type": "string", "enum": [ "text", "number", "boolean", "date", "url" ] }, "customerAddressSchema": { "type": "object", "properties": { "city": { "$ref": "#/components/schemas/nullableShortText" }, "country": { "$ref": "#/components/schemas/nullableShortText" }, "line1": { "$ref": "#/components/schemas/nullableShortText" }, "line2": { "$ref": "#/components/schemas/nullableShortText" }, "postalCode": { "$ref": "#/components/schemas/nullableShortText" }, "state": { "$ref": "#/components/schemas/nullableShortText" } }, "required": [ "city", "country", "line1", "line2", "postalCode", "state" ] }, "customerAssetConditionSchema": { "type": "string", "enum": [ "new", "good", "fair", "poor", "unknown" ] }, "customerAssetFileIdSchema": { "type": "string", "format": "uuid" }, "customerAssetFileSchema": { "type": "object", "properties": { "altText": { "type": [ "string", "null" ] }, "byteSize": { "type": "integer", "exclusiveMinimum": 0 }, "contentType": { "type": "string", "minLength": 1 }, "createdAt": { "type": "string", "format": "date-time" }, "createdBy": { "type": [ "string", "null" ], "format": "uuid" }, "customerAssetId": { "$ref": "#/components/schemas/customerAssetIdSchema" }, "fileName": { "type": "string", "minLength": 1 }, "id": { "$ref": "#/components/schemas/customerAssetFileIdSchema" }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "position": { "type": "integer", "minimum": 0 }, "updatedAt": { "type": "string", "format": "date-time" }, "url": { "type": "string", "format": "uri" } }, "required": [ "altText", "byteSize", "contentType", "createdAt", "createdBy", "customerAssetId", "fileName", "id", "organizationId", "position", "updatedAt", "url" ] }, "customerAssetIdSchema": { "type": "string", "format": "uuid" }, "customerAssetSchema": { "type": "object", "properties": { "assetTag": { "type": [ "string", "null" ] }, "barcode": { "type": [ "string", "null" ] }, "condition": { "$ref": "#/components/schemas/customerAssetConditionSchema" }, "createdAt": { "type": "string", "format": "date-time" }, "createdBy": { "type": [ "string", "null" ], "format": "uuid" }, "currency": { "type": [ "string", "null" ] }, "customAttributes": { "type": "array", "items": { "$ref": "#/components/schemas/assetCustomAttributeSchema" } }, "customerId": { "$ref": "#/components/schemas/customerIdSchema" }, "customerLocation": { "anyOf": [ { "$ref": "#/components/schemas/customerLocationSchema" }, { "type": "null" } ] }, "customerLocationId": { "type": [ "string", "null" ], "format": "uuid" }, "customerName": { "type": [ "string", "null" ] }, "description": { "type": [ "string", "null" ] }, "files": { "type": "array", "items": { "$ref": "#/components/schemas/customerAssetFileSchema" } }, "id": { "$ref": "#/components/schemas/customerAssetIdSchema" }, "installedOn": { "$ref": "#/components/schemas/nullableDate" }, "invoiceId": { "type": [ "string", "null" ], "format": "uuid" }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "product": { "anyOf": [ { "$ref": "#/components/schemas/productListItemSchema" }, { "type": "null" } ] }, "productId": { "$ref": "#/components/schemas/productIdSchema" }, "purchasePrice": { "type": [ "number", "null" ] }, "purchasedOn": { "$ref": "#/components/schemas/nullableDate" }, "quoteId": { "type": [ "string", "null" ], "format": "uuid" }, "serialNumber": { "type": [ "string", "null" ] }, "serviceIntervalDays": { "type": [ "integer", "null" ], "minimum": 1 }, "status": { "$ref": "#/components/schemas/customerAssetStatusSchema" }, "tags": { "type": "array", "items": { "$ref": "#/components/schemas/organizationTagSchema" } }, "updatedAt": { "type": "string", "format": "date-time" }, "updatedBy": { "type": [ "string", "null" ], "format": "uuid" }, "warrantyExpiresOn": { "$ref": "#/components/schemas/nullableDate" }, "warrantyProvider": { "type": [ "string", "null" ] }, "warrantyReference": { "type": [ "string", "null" ] } }, "required": [ "assetTag", "barcode", "condition", "createdAt", "createdBy", "currency", "customAttributes", "customerId", "customerLocation", "customerLocationId", "customerName", "description", "files", "id", "installedOn", "invoiceId", "organizationId", "product", "productId", "purchasePrice", "purchasedOn", "quoteId", "serialNumber", "serviceIntervalDays", "status", "tags", "updatedAt", "updatedBy", "warrantyExpiresOn", "warrantyProvider", "warrantyReference" ] }, "customerAssetStatusSchema": { "type": "string", "enum": [ "active", "inactive", "maintenance", "retired", "archived" ] }, "customerIdSchema": { "type": "string", "format": "uuid" }, "customerLocationSchema": { "type": "object", "properties": { "address": { "$ref": "#/components/schemas/customerAddressSchema" }, "createdAt": { "type": "string", "format": "date-time" }, "customerId": { "$ref": "#/components/schemas/customerIdSchema" }, "id": { "type": "string", "format": "uuid" }, "isPrimary": { "type": "boolean" }, "name": { "type": "string", "minLength": 1, "maxLength": 120 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "updatedAt": { "type": "string", "format": "date-time" } }, "required": [ "address", "createdAt", "customerId", "id", "isPrimary", "name", "organizationId", "updatedAt" ] }, "iconNameSchema": { "type": "string", "minLength": 1, "maxLength": 120, "pattern": "^[A-Za-z0-9]+Icon$" }, "inventoryLocationIdSchema": { "type": "string", "format": "uuid" }, "nullableDate": { "type": [ "string", "null" ], "format": "date" }, "nullableShortText": { "type": [ "string", "null" ], "maxLength": 240 }, "organizationIdSchema": { "type": "string", "format": "uuid" }, "organizationTagSchema": { "type": "object", "properties": { "color": { "anyOf": [ { "$ref": "#/components/schemas/tagColorSchema" }, { "type": "null" } ] }, "createdAt": { "type": "string", "format": "date-time" }, "description": { "type": [ "string", "null" ] }, "id": { "$ref": "#/components/schemas/tagIdSchema" }, "kind": { "$ref": "#/components/schemas/tagKindSchema" }, "name": { "type": "string", "minLength": 1, "maxLength": 80 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "position": { "type": "integer", "minimum": 0 }, "slug": { "$ref": "#/components/schemas/tagSlugSchema" }, "updatedAt": { "type": "string", "format": "date-time" } }, "required": [ "color", "createdAt", "description", "id", "kind", "name", "organizationId", "position", "slug", "updatedAt" ] }, "productCategorySchema": { "type": "object", "properties": { "createdAt": { "type": "string", "format": "date-time" }, "description": { "type": [ "string", "null" ] }, "icon": { "$ref": "#/components/schemas/iconNameSchema" }, "id": { "type": "string", "format": "uuid" }, "name": { "type": "string", "minLength": 1, "maxLength": 120 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "parentId": { "type": [ "string", "null" ], "format": "uuid" }, "position": { "type": "integer", "minimum": 0 }, "slug": { "$ref": "#/components/schemas/slugSchema" }, "updatedAt": { "type": "string", "format": "date-time" } }, "required": [ "createdAt", "description", "icon", "id", "name", "organizationId", "parentId", "position", "slug", "updatedAt" ] }, "productIdSchema": { "type": "string", "format": "uuid" }, "productListItemSchema": { "type": "object", "properties": { "brand": { "type": [ "object", "null" ], "properties": { "createdAt": { "type": "string", "format": "date-time" }, "description": { "type": [ "string", "null" ] }, "id": { "$ref": "#/components/schemas/brandIdSchema" }, "logoUrl": { "type": [ "string", "null" ], "format": "uri" }, "name": { "type": "string", "minLength": 1, "maxLength": 240 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "slug": { "type": "string", "minLength": 1, "maxLength": 160, "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$" }, "updatedAt": { "type": "string", "format": "date-time" }, "website": { "type": [ "string", "null" ] } }, "required": [ "createdAt", "description", "id", "logoUrl", "name", "organizationId", "slug", "updatedAt", "website" ] }, "categories": { "type": "array", "items": { "$ref": "#/components/schemas/productCategorySchema" } }, "categoryIds": { "type": "array", "items": { "type": "string", "format": "uuid" } }, "currency": { "type": [ "string", "null" ] }, "defaultSellingPrice": { "type": [ "number", "null" ] }, "ean": { "type": [ "string", "null" ] }, "id": { "$ref": "#/components/schemas/productIdSchema" }, "images": { "type": "array", "items": { "$ref": "#/components/schemas/assetImageSchema" } }, "manufacturer": { "type": [ "string", "null" ] }, "manufacturerPartNumber": { "type": [ "string", "null" ] }, "name": { "type": "string", "minLength": 1 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "sku": { "type": [ "string", "null" ] }, "slug": { "$ref": "#/components/schemas/slugSchema" }, "status": { "$ref": "#/components/schemas/productStatusSchema" }, "suppliers": { "type": "array", "items": { "$ref": "#/components/schemas/productSupplierLinkSchema" } }, "tags": { "type": "array", "items": { "$ref": "#/components/schemas/organizationTagSchema" } }, "unitOfMeasure": { "type": [ "string", "null" ] }, "updatedAt": { "type": "string", "format": "date-time" }, "vatPercentage": { "type": [ "number", "null" ] }, "stockStatus": { "anyOf": [ { "$ref": "#/components/schemas/stockStatusSchema" }, { "type": "null" } ] } }, "required": [ "brand", "categories", "categoryIds", "currency", "defaultSellingPrice", "ean", "id", "images", "manufacturer", "manufacturerPartNumber", "name", "organizationId", "sku", "slug", "status", "suppliers", "tags", "unitOfMeasure", "updatedAt", "vatPercentage" ] }, "productStatusSchema": { "type": "string", "enum": [ "active", "discontinued", "archived" ] }, "productSupplierLinkSchema": { "type": "object", "properties": { "createdAt": { "type": "string", "format": "date-time" }, "currency": { "type": [ "string", "null" ] }, "externalId": { "type": [ "string", "null" ] }, "id": { "type": "string", "format": "uuid" }, "isActive": { "type": "boolean" }, "isPreferred": { "type": "boolean" }, "leadTimeDays": { "type": [ "integer", "null" ] }, "minOrderQuantity": { "type": [ "number", "null" ] }, "orderCode": { "type": [ "string", "null" ] }, "orderMultiple": { "type": [ "number", "null" ] }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "productId": { "$ref": "#/components/schemas/productIdSchema" }, "supplier": { "anyOf": [ { "$ref": "#/components/schemas/supplierSchema" }, { "type": "null" } ] }, "supplierId": { "$ref": "#/components/schemas/supplierIdSchema" }, "supplierSku": { "type": [ "string", "null" ] }, "unitCost": { "type": [ "number", "null" ] }, "updatedAt": { "type": "string", "format": "date-time" } }, "required": [ "createdAt", "currency", "externalId", "id", "isActive", "isPreferred", "leadTimeDays", "minOrderQuantity", "orderCode", "orderMultiple", "organizationId", "productId", "supplier", "supplierId", "supplierSku", "unitCost", "updatedAt" ] }, "slugSchema": { "type": "string", "minLength": 1, "maxLength": 160, "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$" }, "stockStatusSchema": { "type": "string", "enum": [ "not_tracked", "discontinued", "out_of_stock", "backordered", "low_stock", "in_stock" ] }, "supplierIdSchema": { "type": "string", "format": "uuid" }, "supplierSchema": { "type": "object", "properties": { "accountNumber": { "type": [ "string", "null" ] }, "addressCity": { "type": [ "string", "null" ] }, "addressCountry": { "type": [ "string", "null" ] }, "addressLine1": { "type": [ "string", "null" ] }, "addressLine2": { "type": [ "string", "null" ] }, "addressPostalCode": { "type": [ "string", "null" ] }, "addressState": { "type": [ "string", "null" ] }, "billingEmail": { "type": [ "string", "null" ] }, "btw": { "type": [ "string", "null" ] }, "code": { "type": [ "string", "null" ] }, "createdAt": { "type": "string", "format": "date-time" }, "currency": { "type": [ "string", "null" ] }, "defaultInventoryLocationId": { "anyOf": [ { "$ref": "#/components/schemas/inventoryLocationIdSchema" }, { "type": "null" } ] }, "defaultLeadTimeDays": { "type": [ "integer", "null" ] }, "email": { "type": [ "string", "null" ] }, "externalId": { "type": [ "string", "null" ] }, "id": { "$ref": "#/components/schemas/supplierIdSchema" }, "kvk": { "type": [ "string", "null" ] }, "name": { "type": "string", "minLength": 1, "maxLength": 240 }, "notes": { "type": [ "string", "null" ] }, "orderingEmail": { "type": [ "string", "null" ] }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "paymentTermsDays": { "type": [ "integer", "null" ] }, "phone": { "type": [ "string", "null" ] }, "source": { "type": [ "string", "null" ] }, "status": { "$ref": "#/components/schemas/supplierStatusSchema" }, "updatedAt": { "type": "string", "format": "date-time" }, "website": { "type": [ "string", "null" ] } }, "required": [ "accountNumber", "addressCity", "addressCountry", "addressLine1", "addressLine2", "addressPostalCode", "addressState", "billingEmail", "btw", "code", "createdAt", "currency", "defaultInventoryLocationId", "defaultLeadTimeDays", "email", "externalId", "id", "kvk", "name", "notes", "orderingEmail", "organizationId", "paymentTermsDays", "phone", "source", "status", "updatedAt", "website" ] }, "supplierStatusSchema": { "type": "string", "enum": [ "active", "inactive", "archived" ] }, "tagColorSchema": { "type": "string", "pattern": "^#(?:[0-9a-f]{3}|[0-9a-f]{6})$" }, "tagIdSchema": { "type": "string", "format": "uuid" }, "tagKindSchema": { "type": "string", "enum": [ "task", "customer", "product" ] }, "tagSlugSchema": { "type": "string", "minLength": 1, "maxLength": 160, "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$" } } ``` # Update a customer asset. (/docs/openapi/updateCustomerAsset) Update a customer asset for the specified organization. ## PATCH /v1/organizations/{organizationId}/customer-assets/{customerAssetId} ```json { "operationId": "updateCustomerAsset", "summary": "Update a customer asset.", "description": "Update a customer asset for the specified organization.", "tags": [ "Assets" ], "parameters": [ { "in": "path", "name": "customerAssetId", "required": true, "schema": { "$ref": "#/components/schemas/customerAssetIdSchema" }, "example": "123" }, { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/customerAssetInputSchema" } } }, "description": "The customer asset payload." }, "responses": { "200": { "description": "Returns the updated customer asset.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/customerAssetSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "assetCustomAttributeSchema": { "type": "object", "properties": { "booleanValue": { "type": [ "boolean", "null" ] }, "dateValue": { "$ref": "#/components/schemas/nullableDate" }, "numberValue": { "type": [ "number", "null" ] }, "textValue": { "type": [ "string", "null" ] }, "urlValue": { "type": [ "string", "null" ] }, "valueType": { "$ref": "#/components/schemas/customAttributeValueTypeSchema" }, "createdAt": { "type": "string", "format": "date-time" }, "customerAssetId": { "anyOf": [ { "$ref": "#/components/schemas/customerAssetIdSchema" }, { "type": "null" } ] }, "id": { "type": "string", "format": "uuid" }, "key": { "type": "string", "minLength": 1, "maxLength": 120 }, "label": { "type": "string", "minLength": 1, "maxLength": 160 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "position": { "type": "integer", "minimum": 0 }, "productId": { "anyOf": [ { "$ref": "#/components/schemas/productIdSchema" }, { "type": "null" } ] }, "updatedAt": { "type": "string", "format": "date-time" } }, "required": [ "booleanValue", "dateValue", "numberValue", "textValue", "urlValue", "valueType", "createdAt", "customerAssetId", "id", "key", "label", "organizationId", "position", "productId", "updatedAt" ] }, "assetImageIdSchema": { "type": "string", "format": "uuid" }, "assetImageSchema": { "type": "object", "properties": { "altText": { "type": [ "string", "null" ] }, "createdAt": { "type": "string", "format": "date-time" }, "id": { "$ref": "#/components/schemas/assetImageIdSchema" }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "position": { "type": "integer", "minimum": 0 }, "productId": { "anyOf": [ { "$ref": "#/components/schemas/productIdSchema" }, { "type": "null" } ] }, "updatedAt": { "type": "string", "format": "date-time" }, "url": { "type": "string", "format": "uri" } }, "required": [ "altText", "createdAt", "id", "organizationId", "position", "productId", "updatedAt", "url" ] }, "brandIdSchema": { "type": "string", "enum": [ "centrakit", "mechup", "stabledock", "caresly", "propdock", "marryduck", "signaldock", "spacelio", "legaldock" ] }, "currencyCodeSchema": { "type": "string", "pattern": "^[A-Z]{3}$" }, "customAttributeValueTypeSchema": { "type": "string", "enum": [ "text", "number", "boolean", "date", "url" ] }, "customerAddressSchema": { "type": "object", "properties": { "city": { "$ref": "#/components/schemas/nullableShortText" }, "country": { "$ref": "#/components/schemas/nullableShortText" }, "line1": { "$ref": "#/components/schemas/nullableShortText" }, "line2": { "$ref": "#/components/schemas/nullableShortText" }, "postalCode": { "$ref": "#/components/schemas/nullableShortText" }, "state": { "$ref": "#/components/schemas/nullableShortText" } }, "required": [ "city", "country", "line1", "line2", "postalCode", "state" ] }, "customerAssetConditionSchema": { "type": "string", "enum": [ "new", "good", "fair", "poor", "unknown" ] }, "customerAssetFileIdSchema": { "type": "string", "format": "uuid" }, "customerAssetFileSchema": { "type": "object", "properties": { "altText": { "type": [ "string", "null" ] }, "byteSize": { "type": "integer", "exclusiveMinimum": 0 }, "contentType": { "type": "string", "minLength": 1 }, "createdAt": { "type": "string", "format": "date-time" }, "createdBy": { "type": [ "string", "null" ], "format": "uuid" }, "customerAssetId": { "$ref": "#/components/schemas/customerAssetIdSchema" }, "fileName": { "type": "string", "minLength": 1 }, "id": { "$ref": "#/components/schemas/customerAssetFileIdSchema" }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "position": { "type": "integer", "minimum": 0 }, "updatedAt": { "type": "string", "format": "date-time" }, "url": { "type": "string", "format": "uri" } }, "required": [ "altText", "byteSize", "contentType", "createdAt", "createdBy", "customerAssetId", "fileName", "id", "organizationId", "position", "updatedAt", "url" ] }, "customerAssetIdSchema": { "type": "string", "format": "uuid" }, "customerAssetInputSchema": { "type": "object", "properties": { "assetTag": { "allOf": [ { "$ref": "#/components/schemas/nullableShortText" } ] }, "barcode": { "allOf": [ { "$ref": "#/components/schemas/nullableShortText" } ] }, "condition": { "allOf": [ { "$ref": "#/components/schemas/customerAssetConditionSchema" } ] }, "currency": { "allOf": [ { "$ref": "#/components/schemas/nullableCurrency" } ] }, "customerId": { "$ref": "#/components/schemas/customerIdSchema" }, "customerLocationId": { "type": [ "string", "null" ], "format": "uuid" }, "description": { "allOf": [ { "$ref": "#/components/schemas/nullableLongText" } ] }, "installedOn": { "allOf": [ { "$ref": "#/components/schemas/nullableDate" } ] }, "invoiceId": { "type": [ "string", "null" ], "format": "uuid" }, "productId": { "$ref": "#/components/schemas/productIdSchema" }, "purchasePrice": { "allOf": [ { "$ref": "#/components/schemas/nullableMoney" } ] }, "purchasedOn": { "allOf": [ { "$ref": "#/components/schemas/nullableDate" } ] }, "quoteId": { "type": [ "string", "null" ], "format": "uuid" }, "serialNumber": { "allOf": [ { "$ref": "#/components/schemas/nullableShortText" } ] }, "serviceIntervalDays": { "type": [ "integer", "null" ], "minimum": 1 }, "status": { "allOf": [ { "$ref": "#/components/schemas/customerAssetStatusSchema" } ] }, "warrantyExpiresOn": { "allOf": [ { "$ref": "#/components/schemas/nullableDate" } ] }, "warrantyProvider": { "allOf": [ { "$ref": "#/components/schemas/nullableShortText" } ] }, "warrantyReference": { "allOf": [ { "$ref": "#/components/schemas/nullableShortText" } ] } }, "required": [ "customerId", "productId" ] }, "customerAssetSchema": { "type": "object", "properties": { "assetTag": { "type": [ "string", "null" ] }, "barcode": { "type": [ "string", "null" ] }, "condition": { "$ref": "#/components/schemas/customerAssetConditionSchema" }, "createdAt": { "type": "string", "format": "date-time" }, "createdBy": { "type": [ "string", "null" ], "format": "uuid" }, "currency": { "type": [ "string", "null" ] }, "customAttributes": { "type": "array", "items": { "$ref": "#/components/schemas/assetCustomAttributeSchema" } }, "customerId": { "$ref": "#/components/schemas/customerIdSchema" }, "customerLocation": { "anyOf": [ { "$ref": "#/components/schemas/customerLocationSchema" }, { "type": "null" } ] }, "customerLocationId": { "type": [ "string", "null" ], "format": "uuid" }, "customerName": { "type": [ "string", "null" ] }, "description": { "type": [ "string", "null" ] }, "files": { "type": "array", "items": { "$ref": "#/components/schemas/customerAssetFileSchema" } }, "id": { "$ref": "#/components/schemas/customerAssetIdSchema" }, "installedOn": { "$ref": "#/components/schemas/nullableDate" }, "invoiceId": { "type": [ "string", "null" ], "format": "uuid" }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "product": { "anyOf": [ { "$ref": "#/components/schemas/productListItemSchema" }, { "type": "null" } ] }, "productId": { "$ref": "#/components/schemas/productIdSchema" }, "purchasePrice": { "type": [ "number", "null" ] }, "purchasedOn": { "$ref": "#/components/schemas/nullableDate" }, "quoteId": { "type": [ "string", "null" ], "format": "uuid" }, "serialNumber": { "type": [ "string", "null" ] }, "serviceIntervalDays": { "type": [ "integer", "null" ], "minimum": 1 }, "status": { "$ref": "#/components/schemas/customerAssetStatusSchema" }, "tags": { "type": "array", "items": { "$ref": "#/components/schemas/organizationTagSchema" } }, "updatedAt": { "type": "string", "format": "date-time" }, "updatedBy": { "type": [ "string", "null" ], "format": "uuid" }, "warrantyExpiresOn": { "$ref": "#/components/schemas/nullableDate" }, "warrantyProvider": { "type": [ "string", "null" ] }, "warrantyReference": { "type": [ "string", "null" ] } }, "required": [ "assetTag", "barcode", "condition", "createdAt", "createdBy", "currency", "customAttributes", "customerId", "customerLocation", "customerLocationId", "customerName", "description", "files", "id", "installedOn", "invoiceId", "organizationId", "product", "productId", "purchasePrice", "purchasedOn", "quoteId", "serialNumber", "serviceIntervalDays", "status", "tags", "updatedAt", "updatedBy", "warrantyExpiresOn", "warrantyProvider", "warrantyReference" ] }, "customerAssetStatusSchema": { "type": "string", "enum": [ "active", "inactive", "maintenance", "retired", "archived" ] }, "customerIdSchema": { "type": "string", "format": "uuid" }, "customerLocationSchema": { "type": "object", "properties": { "address": { "$ref": "#/components/schemas/customerAddressSchema" }, "createdAt": { "type": "string", "format": "date-time" }, "customerId": { "$ref": "#/components/schemas/customerIdSchema" }, "id": { "type": "string", "format": "uuid" }, "isPrimary": { "type": "boolean" }, "name": { "type": "string", "minLength": 1, "maxLength": 120 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "updatedAt": { "type": "string", "format": "date-time" } }, "required": [ "address", "createdAt", "customerId", "id", "isPrimary", "name", "organizationId", "updatedAt" ] }, "iconNameSchema": { "type": "string", "minLength": 1, "maxLength": 120, "pattern": "^[A-Za-z0-9]+Icon$" }, "inventoryLocationIdSchema": { "type": "string", "format": "uuid" }, "nullableCurrency": { "anyOf": [ { "$ref": "#/components/schemas/currencyCodeSchema" }, { "type": "null" } ] }, "nullableDate": { "type": [ "string", "null" ], "format": "date" }, "nullableLongText": { "type": [ "string", "null" ], "maxLength": 4000 }, "nullableMoney": { "type": [ "number", "null" ], "minimum": 0 }, "nullableShortText": { "type": [ "string", "null" ], "maxLength": 240 }, "organizationIdSchema": { "type": "string", "format": "uuid" }, "organizationTagSchema": { "type": "object", "properties": { "color": { "anyOf": [ { "$ref": "#/components/schemas/tagColorSchema" }, { "type": "null" } ] }, "createdAt": { "type": "string", "format": "date-time" }, "description": { "type": [ "string", "null" ] }, "id": { "$ref": "#/components/schemas/tagIdSchema" }, "kind": { "$ref": "#/components/schemas/tagKindSchema" }, "name": { "type": "string", "minLength": 1, "maxLength": 80 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "position": { "type": "integer", "minimum": 0 }, "slug": { "$ref": "#/components/schemas/tagSlugSchema" }, "updatedAt": { "type": "string", "format": "date-time" } }, "required": [ "color", "createdAt", "description", "id", "kind", "name", "organizationId", "position", "slug", "updatedAt" ] }, "productCategorySchema": { "type": "object", "properties": { "createdAt": { "type": "string", "format": "date-time" }, "description": { "type": [ "string", "null" ] }, "icon": { "$ref": "#/components/schemas/iconNameSchema" }, "id": { "type": "string", "format": "uuid" }, "name": { "type": "string", "minLength": 1, "maxLength": 120 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "parentId": { "type": [ "string", "null" ], "format": "uuid" }, "position": { "type": "integer", "minimum": 0 }, "slug": { "$ref": "#/components/schemas/slugSchema" }, "updatedAt": { "type": "string", "format": "date-time" } }, "required": [ "createdAt", "description", "icon", "id", "name", "organizationId", "parentId", "position", "slug", "updatedAt" ] }, "productIdSchema": { "type": "string", "format": "uuid" }, "productListItemSchema": { "type": "object", "properties": { "brand": { "type": [ "object", "null" ], "properties": { "createdAt": { "type": "string", "format": "date-time" }, "description": { "type": [ "string", "null" ] }, "id": { "$ref": "#/components/schemas/brandIdSchema" }, "logoUrl": { "type": [ "string", "null" ], "format": "uri" }, "name": { "type": "string", "minLength": 1, "maxLength": 240 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "slug": { "type": "string", "minLength": 1, "maxLength": 160, "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$" }, "updatedAt": { "type": "string", "format": "date-time" }, "website": { "type": [ "string", "null" ] } }, "required": [ "createdAt", "description", "id", "logoUrl", "name", "organizationId", "slug", "updatedAt", "website" ] }, "categories": { "type": "array", "items": { "$ref": "#/components/schemas/productCategorySchema" } }, "categoryIds": { "type": "array", "items": { "type": "string", "format": "uuid" } }, "currency": { "type": [ "string", "null" ] }, "defaultSellingPrice": { "type": [ "number", "null" ] }, "ean": { "type": [ "string", "null" ] }, "id": { "$ref": "#/components/schemas/productIdSchema" }, "images": { "type": "array", "items": { "$ref": "#/components/schemas/assetImageSchema" } }, "manufacturer": { "type": [ "string", "null" ] }, "manufacturerPartNumber": { "type": [ "string", "null" ] }, "name": { "type": "string", "minLength": 1 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "sku": { "type": [ "string", "null" ] }, "slug": { "$ref": "#/components/schemas/slugSchema" }, "status": { "$ref": "#/components/schemas/productStatusSchema" }, "suppliers": { "type": "array", "items": { "$ref": "#/components/schemas/productSupplierLinkSchema" } }, "tags": { "type": "array", "items": { "$ref": "#/components/schemas/organizationTagSchema" } }, "unitOfMeasure": { "type": [ "string", "null" ] }, "updatedAt": { "type": "string", "format": "date-time" }, "vatPercentage": { "type": [ "number", "null" ] }, "stockStatus": { "anyOf": [ { "$ref": "#/components/schemas/stockStatusSchema" }, { "type": "null" } ] } }, "required": [ "brand", "categories", "categoryIds", "currency", "defaultSellingPrice", "ean", "id", "images", "manufacturer", "manufacturerPartNumber", "name", "organizationId", "sku", "slug", "status", "suppliers", "tags", "unitOfMeasure", "updatedAt", "vatPercentage" ] }, "productStatusSchema": { "type": "string", "enum": [ "active", "discontinued", "archived" ] }, "productSupplierLinkSchema": { "type": "object", "properties": { "createdAt": { "type": "string", "format": "date-time" }, "currency": { "type": [ "string", "null" ] }, "externalId": { "type": [ "string", "null" ] }, "id": { "type": "string", "format": "uuid" }, "isActive": { "type": "boolean" }, "isPreferred": { "type": "boolean" }, "leadTimeDays": { "type": [ "integer", "null" ] }, "minOrderQuantity": { "type": [ "number", "null" ] }, "orderCode": { "type": [ "string", "null" ] }, "orderMultiple": { "type": [ "number", "null" ] }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "productId": { "$ref": "#/components/schemas/productIdSchema" }, "supplier": { "anyOf": [ { "$ref": "#/components/schemas/supplierSchema" }, { "type": "null" } ] }, "supplierId": { "$ref": "#/components/schemas/supplierIdSchema" }, "supplierSku": { "type": [ "string", "null" ] }, "unitCost": { "type": [ "number", "null" ] }, "updatedAt": { "type": "string", "format": "date-time" } }, "required": [ "createdAt", "currency", "externalId", "id", "isActive", "isPreferred", "leadTimeDays", "minOrderQuantity", "orderCode", "orderMultiple", "organizationId", "productId", "supplier", "supplierId", "supplierSku", "unitCost", "updatedAt" ] }, "slugSchema": { "type": "string", "minLength": 1, "maxLength": 160, "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$" }, "stockStatusSchema": { "type": "string", "enum": [ "not_tracked", "discontinued", "out_of_stock", "backordered", "low_stock", "in_stock" ] }, "supplierIdSchema": { "type": "string", "format": "uuid" }, "supplierSchema": { "type": "object", "properties": { "accountNumber": { "type": [ "string", "null" ] }, "addressCity": { "type": [ "string", "null" ] }, "addressCountry": { "type": [ "string", "null" ] }, "addressLine1": { "type": [ "string", "null" ] }, "addressLine2": { "type": [ "string", "null" ] }, "addressPostalCode": { "type": [ "string", "null" ] }, "addressState": { "type": [ "string", "null" ] }, "billingEmail": { "type": [ "string", "null" ] }, "btw": { "type": [ "string", "null" ] }, "code": { "type": [ "string", "null" ] }, "createdAt": { "type": "string", "format": "date-time" }, "currency": { "type": [ "string", "null" ] }, "defaultInventoryLocationId": { "anyOf": [ { "$ref": "#/components/schemas/inventoryLocationIdSchema" }, { "type": "null" } ] }, "defaultLeadTimeDays": { "type": [ "integer", "null" ] }, "email": { "type": [ "string", "null" ] }, "externalId": { "type": [ "string", "null" ] }, "id": { "$ref": "#/components/schemas/supplierIdSchema" }, "kvk": { "type": [ "string", "null" ] }, "name": { "type": "string", "minLength": 1, "maxLength": 240 }, "notes": { "type": [ "string", "null" ] }, "orderingEmail": { "type": [ "string", "null" ] }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "paymentTermsDays": { "type": [ "integer", "null" ] }, "phone": { "type": [ "string", "null" ] }, "source": { "type": [ "string", "null" ] }, "status": { "$ref": "#/components/schemas/supplierStatusSchema" }, "updatedAt": { "type": "string", "format": "date-time" }, "website": { "type": [ "string", "null" ] } }, "required": [ "accountNumber", "addressCity", "addressCountry", "addressLine1", "addressLine2", "addressPostalCode", "addressState", "billingEmail", "btw", "code", "createdAt", "currency", "defaultInventoryLocationId", "defaultLeadTimeDays", "email", "externalId", "id", "kvk", "name", "notes", "orderingEmail", "organizationId", "paymentTermsDays", "phone", "source", "status", "updatedAt", "website" ] }, "supplierStatusSchema": { "type": "string", "enum": [ "active", "inactive", "archived" ] }, "tagColorSchema": { "type": "string", "pattern": "^#(?:[0-9a-f]{3}|[0-9a-f]{6})$" }, "tagIdSchema": { "type": "string", "format": "uuid" }, "tagKindSchema": { "type": "string", "enum": [ "task", "customer", "product" ] }, "tagSlugSchema": { "type": "string", "minLength": 1, "maxLength": 160, "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$" } } ``` # Delete a customer asset. (/docs/openapi/deleteCustomerAsset) Delete a customer asset for the specified organization. ## DELETE /v1/organizations/{organizationId}/customer-assets/{customerAssetId} ```json { "operationId": "deleteCustomerAsset", "summary": "Delete a customer asset.", "description": "Delete a customer asset for the specified organization.", "tags": [ "Assets" ], "parameters": [ { "in": "path", "name": "customerAssetId", "required": true, "schema": { "$ref": "#/components/schemas/customerAssetIdSchema" }, "example": "123" }, { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "responses": { "200": { "description": "Returns a success payload when the customer asset is deleted.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/publicApiSuccessResponseSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "customerAssetIdSchema": { "type": "string", "format": "uuid" }, "organizationIdSchema": { "type": "string", "format": "uuid" }, "publicApiSuccessResponseSchema": { "type": "object", "properties": { "success": { "type": "boolean", "enum": [ true ] } }, "required": [ "success" ] } } ``` # List inventory allocations. (/docs/openapi/listInventoryAllocations) Return stock reserved or consumed by quotes, tasks, and invoices. ## GET /v1/organizations/{organizationId}/inventory/allocations ```json { "operationId": "listInventoryAllocations", "summary": "List inventory allocations.", "description": "Return stock reserved or consumed by quotes, tasks, and invoices.", "tags": [ "Assets" ], "parameters": [ { "in": "query", "name": "offset", "required": true, "schema": { "type": "string" }, "example": "example" }, { "in": "query", "name": "limit", "required": true, "schema": { "type": "string" }, "example": "example" }, { "in": "query", "name": "q", "required": false, "schema": { "type": "string" }, "example": "example" }, { "in": "query", "name": "invoiceLineId", "required": false, "schema": { "type": "string", "format": "uuid" }, "example": "123e4567-e89b-12d3-a456-426614174000" }, { "in": "query", "name": "productId", "required": false, "schema": { "allOf": [ { "$ref": "#/components/schemas/productIdSchema" } ] }, "example": "123" }, { "in": "query", "name": "quoteVersionLineId", "required": false, "schema": { "type": "string", "format": "uuid" }, "example": "123e4567-e89b-12d3-a456-426614174000" }, { "in": "query", "name": "stockRecordId", "required": false, "schema": { "allOf": [ { "$ref": "#/components/schemas/inventoryStockRecordIdSchema" } ] }, "example": "123" }, { "in": "query", "name": "taskMaterialId", "required": false, "schema": { "type": "string", "format": "uuid" }, "example": "123e4567-e89b-12d3-a456-426614174000" }, { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "responses": { "200": { "description": "Returns inventory allocations.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/inventoryAllocationsResponseSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "inventoryAllocationSchema": { "type": "object", "properties": { "consumedAt": { "type": [ "string", "null" ], "format": "date-time" }, "createdAt": { "type": "string", "format": "date-time" }, "id": { "type": "string", "format": "uuid" }, "invoiceLineId": { "type": [ "string", "null" ], "format": "uuid" }, "orderLineId": { "type": [ "string", "null" ], "format": "uuid" }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "productId": { "$ref": "#/components/schemas/productIdSchema" }, "quantity": { "type": "number", "exclusiveMinimum": 0 }, "quoteVersionLineId": { "type": [ "string", "null" ], "format": "uuid" }, "source": { "anyOf": [ { "$ref": "#/components/schemas/inventoryAllocationSourceSchema" }, { "type": "null" } ] }, "state": { "$ref": "#/components/schemas/inventoryAllocationStateSchema" }, "stockRecordId": { "anyOf": [ { "$ref": "#/components/schemas/inventoryStockRecordIdSchema" }, { "type": "null" } ] }, "taskMaterialId": { "type": [ "string", "null" ], "format": "uuid" }, "updatedAt": { "type": "string", "format": "date-time" } }, "required": [ "consumedAt", "createdAt", "id", "invoiceLineId", "orderLineId", "organizationId", "productId", "quantity", "quoteVersionLineId", "source", "state", "stockRecordId", "taskMaterialId", "updatedAt" ] }, "inventoryAllocationSourceKindSchema": { "type": "string", "enum": [ "quote", "task", "invoice", "order" ] }, "inventoryAllocationSourceSchema": { "type": "object", "properties": { "displayNumber": { "type": [ "string", "null" ], "minLength": 1 }, "id": { "type": "string", "format": "uuid" }, "kind": { "$ref": "#/components/schemas/inventoryAllocationSourceKindSchema" }, "number": { "type": [ "integer", "null" ], "exclusiveMinimum": 0 } }, "required": [ "displayNumber", "id", "kind", "number" ] }, "inventoryAllocationStateSchema": { "type": "string", "enum": [ "reserved", "consumed", "backordered" ] }, "inventoryAllocationsResponseSchema": { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/components/schemas/inventoryAllocationSchema" } }, "pagination": { "$ref": "#/components/schemas/listPaginationSchema" } }, "required": [ "data", "pagination" ] }, "inventoryStockRecordIdSchema": { "type": "string", "format": "uuid" }, "listPaginationSchema": { "type": "object", "properties": { "hasMore": { "type": "boolean" }, "limit": { "type": "integer", "minimum": 1, "maximum": 100 }, "offset": { "type": "integer", "minimum": 0 }, "total": { "type": "integer", "minimum": 0 }, "totalIsEstimate": { "type": "boolean" } }, "required": [ "hasMore", "limit", "offset", "total" ] }, "organizationIdSchema": { "type": "string", "format": "uuid" }, "productIdSchema": { "type": "string", "format": "uuid" } } ``` # List inventory locations. (/docs/openapi/listInventoryLocations) Return inventory locations for an organization. ## GET /v1/organizations/{organizationId}/inventory/locations ```json { "operationId": "listInventoryLocations", "summary": "List inventory locations.", "description": "Return inventory locations for an organization.", "tags": [ "Assets" ], "parameters": [ { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "responses": { "200": { "description": "Returns inventory locations.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/inventoryLocationsResponseSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "inventoryLocationIdSchema": { "type": "string", "format": "uuid" }, "inventoryLocationSchema": { "type": "object", "properties": { "createdAt": { "type": "string", "format": "date-time" }, "description": { "type": [ "string", "null" ] }, "id": { "$ref": "#/components/schemas/inventoryLocationIdSchema" }, "name": { "type": "string", "minLength": 1, "maxLength": 240 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "updatedAt": { "type": "string", "format": "date-time" } }, "required": [ "createdAt", "description", "id", "name", "organizationId", "updatedAt" ] }, "inventoryLocationsResponseSchema": { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/components/schemas/inventoryLocationSchema" } }, "pagination": { "$ref": "#/components/schemas/listPaginationSchema" } }, "required": [ "data", "pagination" ] }, "listPaginationSchema": { "type": "object", "properties": { "hasMore": { "type": "boolean" }, "limit": { "type": "integer", "minimum": 1, "maximum": 100 }, "offset": { "type": "integer", "minimum": 0 }, "total": { "type": "integer", "minimum": 0 }, "totalIsEstimate": { "type": "boolean" } }, "required": [ "hasMore", "limit", "offset", "total" ] }, "organizationIdSchema": { "type": "string", "format": "uuid" } } ``` # Create an inventory location. (/docs/openapi/createInventoryLocation) Create an inventory location for an organization. ## POST /v1/organizations/{organizationId}/inventory/locations ```json { "operationId": "createInventoryLocation", "summary": "Create an inventory location.", "description": "Create an inventory location for an organization.", "tags": [ "Assets" ], "parameters": [ { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/inventoryLocationInputSchema" } } }, "description": "The inventory location payload." }, "responses": { "201": { "description": "Returns the created inventory location.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/inventoryLocationSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "inventoryLocationIdSchema": { "type": "string", "format": "uuid" }, "inventoryLocationInputSchema": { "type": "object", "properties": { "description": { "allOf": [ { "$ref": "#/components/schemas/nullableLongText" } ] }, "name": { "type": "string", "minLength": 1, "maxLength": 240 } }, "required": [ "name" ] }, "inventoryLocationSchema": { "type": "object", "properties": { "createdAt": { "type": "string", "format": "date-time" }, "description": { "type": [ "string", "null" ] }, "id": { "$ref": "#/components/schemas/inventoryLocationIdSchema" }, "name": { "type": "string", "minLength": 1, "maxLength": 240 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "updatedAt": { "type": "string", "format": "date-time" } }, "required": [ "createdAt", "description", "id", "name", "organizationId", "updatedAt" ] }, "nullableLongText": { "type": [ "string", "null" ], "maxLength": 4000 }, "organizationIdSchema": { "type": "string", "format": "uuid" } } ``` # List inventory movements. (/docs/openapi/listInventoryMovements) Return inventory movement history for an organization. ## GET /v1/organizations/{organizationId}/inventory/movements ```json { "operationId": "listInventoryMovements", "summary": "List inventory movements.", "description": "Return inventory movement history for an organization.", "tags": [ "Assets" ], "parameters": [ { "in": "query", "name": "offset", "required": true, "schema": { "type": "string" }, "example": "example" }, { "in": "query", "name": "limit", "required": true, "schema": { "type": "string" }, "example": "example" }, { "in": "query", "name": "q", "required": false, "schema": { "type": "string" }, "example": "example" }, { "in": "query", "name": "productId", "required": false, "schema": { "allOf": [ { "$ref": "#/components/schemas/productIdSchema" } ] }, "example": "123" }, { "in": "query", "name": "stockRecordId", "required": false, "schema": { "allOf": [ { "$ref": "#/components/schemas/inventoryStockRecordIdSchema" } ] }, "example": "123" }, { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "responses": { "200": { "description": "Returns inventory movements.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/inventoryMovementsResponseSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "inventoryLocationIdSchema": { "type": "string", "format": "uuid" }, "inventoryMovementIdSchema": { "type": "string", "format": "uuid" }, "inventoryMovementSchema": { "type": "object", "properties": { "allocatedAfter": { "type": "number", "minimum": 0 }, "allocatedDelta": { "type": "number" }, "createdAt": { "type": "string", "format": "date-time" }, "createdBy": { "type": [ "string", "null" ], "format": "uuid" }, "id": { "$ref": "#/components/schemas/inventoryMovementIdSchema" }, "invoiceId": { "type": [ "string", "null" ], "format": "uuid" }, "locationId": { "anyOf": [ { "$ref": "#/components/schemas/inventoryLocationIdSchema" }, { "type": "null" } ] }, "movementType": { "$ref": "#/components/schemas/inventoryMovementTypeSchema" }, "occurredAt": { "type": "string", "format": "date-time" }, "onHandDelta": { "type": "number" }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "productId": { "$ref": "#/components/schemas/productIdSchema" }, "quantityAfter": { "type": "number", "minimum": 0 }, "quoteId": { "type": [ "string", "null" ], "format": "uuid" }, "quoteNumber": { "type": [ "integer", "null" ] }, "reason": { "type": [ "string", "null" ] }, "referenceNumber": { "type": [ "string", "null" ] }, "stockRecordId": { "$ref": "#/components/schemas/inventoryStockRecordIdSchema" }, "supplierId": { "anyOf": [ { "$ref": "#/components/schemas/supplierIdSchema" }, { "type": "null" } ] }, "taskMaterialId": { "type": [ "string", "null" ], "format": "uuid" } }, "required": [ "allocatedAfter", "allocatedDelta", "createdAt", "createdBy", "id", "invoiceId", "locationId", "movementType", "occurredAt", "onHandDelta", "organizationId", "productId", "quantityAfter", "quoteId", "quoteNumber", "reason", "referenceNumber", "stockRecordId", "supplierId", "taskMaterialId" ] }, "inventoryMovementTypeSchema": { "type": "string", "enum": [ "initial_count", "purchase_receipt", "sale", "reservation", "reservation_release", "reservation_transfer", "adjustment", "transfer_out", "transfer_in", "return", "write_off", "cycle_count" ] }, "inventoryMovementsResponseSchema": { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/components/schemas/inventoryMovementSchema" } }, "pagination": { "$ref": "#/components/schemas/listPaginationSchema" } }, "required": [ "data", "pagination" ] }, "inventoryStockRecordIdSchema": { "type": "string", "format": "uuid" }, "listPaginationSchema": { "type": "object", "properties": { "hasMore": { "type": "boolean" }, "limit": { "type": "integer", "minimum": 1, "maximum": 100 }, "offset": { "type": "integer", "minimum": 0 }, "total": { "type": "integer", "minimum": 0 }, "totalIsEstimate": { "type": "boolean" } }, "required": [ "hasMore", "limit", "offset", "total" ] }, "organizationIdSchema": { "type": "string", "format": "uuid" }, "productIdSchema": { "type": "string", "format": "uuid" }, "supplierIdSchema": { "type": "string", "format": "uuid" } } ``` # Adjust inventory stock. (/docs/openapi/adjustInventoryStock) Apply a stock adjustment and return the updated stock record. ## POST /v1/organizations/{organizationId}/inventory/stock-adjustments ```json { "operationId": "adjustInventoryStock", "summary": "Adjust inventory stock.", "description": "Apply a stock adjustment and return the updated stock record.", "tags": [ "Assets" ], "parameters": [ { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/inventoryStockAdjustmentInputSchema" } } }, "description": "The inventory stock adjustment payload." }, "responses": { "200": { "description": "Returns the updated stock record.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/inventoryStockRecordSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "assetImageIdSchema": { "type": "string", "format": "uuid" }, "assetImageSchema": { "type": "object", "properties": { "altText": { "type": [ "string", "null" ] }, "createdAt": { "type": "string", "format": "date-time" }, "id": { "$ref": "#/components/schemas/assetImageIdSchema" }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "position": { "type": "integer", "minimum": 0 }, "productId": { "anyOf": [ { "$ref": "#/components/schemas/productIdSchema" }, { "type": "null" } ] }, "updatedAt": { "type": "string", "format": "date-time" }, "url": { "type": "string", "format": "uri" } }, "required": [ "altText", "createdAt", "id", "organizationId", "position", "productId", "updatedAt", "url" ] }, "brandIdSchema": { "type": "string", "enum": [ "centrakit", "mechup", "stabledock", "caresly", "propdock", "marryduck", "signaldock", "spacelio", "legaldock" ] }, "iconNameSchema": { "type": "string", "minLength": 1, "maxLength": 120, "pattern": "^[A-Za-z0-9]+Icon$" }, "inventoryLocationIdSchema": { "type": "string", "format": "uuid" }, "inventoryLocationSchema": { "type": "object", "properties": { "createdAt": { "type": "string", "format": "date-time" }, "description": { "type": [ "string", "null" ] }, "id": { "$ref": "#/components/schemas/inventoryLocationIdSchema" }, "name": { "type": "string", "minLength": 1, "maxLength": 240 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "updatedAt": { "type": "string", "format": "date-time" } }, "required": [ "createdAt", "description", "id", "name", "organizationId", "updatedAt" ] }, "inventoryStatusSchema": { "type": "string", "enum": [ "available", "quarantined", "expired", "archived" ] }, "inventoryStockAdjustmentInputSchema": { "type": "object", "properties": { "movementType": { "type": "string", "enum": [ "adjustment", "cycle_count" ] }, "onHandQuantity": { "type": "number", "minimum": 0 }, "reason": { "type": "string", "minLength": 1, "maxLength": 1000 }, "stockRecordId": { "$ref": "#/components/schemas/inventoryStockRecordIdSchema" } }, "required": [ "onHandQuantity", "reason", "stockRecordId" ] }, "inventoryStockRecordIdSchema": { "type": "string", "format": "uuid" }, "inventoryStockRecordSchema": { "type": "object", "properties": { "allocatedQuantity": { "type": "number", "minimum": 0 }, "availableQuantity": { "type": "number", "minimum": 0 }, "binLocation": { "type": [ "string", "null" ] }, "createdAt": { "type": "string", "format": "date-time" }, "createdBy": { "type": [ "string", "null" ], "format": "uuid" }, "currency": { "type": [ "string", "null" ] }, "expiresOn": { "$ref": "#/components/schemas/nullableDate" }, "id": { "$ref": "#/components/schemas/inventoryStockRecordIdSchema" }, "lastCountedOn": { "$ref": "#/components/schemas/nullableDate" }, "location": { "anyOf": [ { "$ref": "#/components/schemas/inventoryLocationSchema" }, { "type": "null" } ] }, "locationId": { "anyOf": [ { "$ref": "#/components/schemas/inventoryLocationIdSchema" }, { "type": "null" } ] }, "lotNumber": { "type": [ "string", "null" ] }, "notes": { "type": [ "string", "null" ] }, "onHandQuantity": { "type": "number", "minimum": 0 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "product": { "anyOf": [ { "$ref": "#/components/schemas/productListItemSchema" }, { "type": "null" } ] }, "productId": { "$ref": "#/components/schemas/productIdSchema" }, "serialNumber": { "type": [ "string", "null" ] }, "status": { "$ref": "#/components/schemas/inventoryStatusSchema" }, "unitCost": { "type": [ "number", "null" ] }, "updatedAt": { "type": "string", "format": "date-time" }, "updatedBy": { "type": [ "string", "null" ], "format": "uuid" }, "variantId": { "type": "string", "format": "uuid" } }, "required": [ "allocatedQuantity", "availableQuantity", "binLocation", "createdAt", "createdBy", "currency", "expiresOn", "id", "lastCountedOn", "location", "locationId", "lotNumber", "notes", "onHandQuantity", "organizationId", "product", "productId", "serialNumber", "status", "unitCost", "updatedAt", "updatedBy", "variantId" ] }, "nullableDate": { "type": [ "string", "null" ], "format": "date" }, "organizationIdSchema": { "type": "string", "format": "uuid" }, "organizationTagSchema": { "type": "object", "properties": { "color": { "anyOf": [ { "$ref": "#/components/schemas/tagColorSchema" }, { "type": "null" } ] }, "createdAt": { "type": "string", "format": "date-time" }, "description": { "type": [ "string", "null" ] }, "id": { "$ref": "#/components/schemas/tagIdSchema" }, "kind": { "$ref": "#/components/schemas/tagKindSchema" }, "name": { "type": "string", "minLength": 1, "maxLength": 80 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "position": { "type": "integer", "minimum": 0 }, "slug": { "$ref": "#/components/schemas/tagSlugSchema" }, "updatedAt": { "type": "string", "format": "date-time" } }, "required": [ "color", "createdAt", "description", "id", "kind", "name", "organizationId", "position", "slug", "updatedAt" ] }, "productCategorySchema": { "type": "object", "properties": { "createdAt": { "type": "string", "format": "date-time" }, "description": { "type": [ "string", "null" ] }, "icon": { "$ref": "#/components/schemas/iconNameSchema" }, "id": { "type": "string", "format": "uuid" }, "name": { "type": "string", "minLength": 1, "maxLength": 120 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "parentId": { "type": [ "string", "null" ], "format": "uuid" }, "position": { "type": "integer", "minimum": 0 }, "slug": { "$ref": "#/components/schemas/slugSchema" }, "updatedAt": { "type": "string", "format": "date-time" } }, "required": [ "createdAt", "description", "icon", "id", "name", "organizationId", "parentId", "position", "slug", "updatedAt" ] }, "productIdSchema": { "type": "string", "format": "uuid" }, "productListItemSchema": { "type": "object", "properties": { "brand": { "type": [ "object", "null" ], "properties": { "createdAt": { "type": "string", "format": "date-time" }, "description": { "type": [ "string", "null" ] }, "id": { "$ref": "#/components/schemas/brandIdSchema" }, "logoUrl": { "type": [ "string", "null" ], "format": "uri" }, "name": { "type": "string", "minLength": 1, "maxLength": 240 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "slug": { "type": "string", "minLength": 1, "maxLength": 160, "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$" }, "updatedAt": { "type": "string", "format": "date-time" }, "website": { "type": [ "string", "null" ] } }, "required": [ "createdAt", "description", "id", "logoUrl", "name", "organizationId", "slug", "updatedAt", "website" ] }, "categories": { "type": "array", "items": { "$ref": "#/components/schemas/productCategorySchema" } }, "categoryIds": { "type": "array", "items": { "type": "string", "format": "uuid" } }, "currency": { "type": [ "string", "null" ] }, "defaultSellingPrice": { "type": [ "number", "null" ] }, "ean": { "type": [ "string", "null" ] }, "id": { "$ref": "#/components/schemas/productIdSchema" }, "images": { "type": "array", "items": { "$ref": "#/components/schemas/assetImageSchema" } }, "manufacturer": { "type": [ "string", "null" ] }, "manufacturerPartNumber": { "type": [ "string", "null" ] }, "name": { "type": "string", "minLength": 1 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "sku": { "type": [ "string", "null" ] }, "slug": { "$ref": "#/components/schemas/slugSchema" }, "status": { "$ref": "#/components/schemas/productStatusSchema" }, "suppliers": { "type": "array", "items": { "$ref": "#/components/schemas/productSupplierLinkSchema" } }, "tags": { "type": "array", "items": { "$ref": "#/components/schemas/organizationTagSchema" } }, "unitOfMeasure": { "type": [ "string", "null" ] }, "updatedAt": { "type": "string", "format": "date-time" }, "vatPercentage": { "type": [ "number", "null" ] }, "stockStatus": { "anyOf": [ { "$ref": "#/components/schemas/stockStatusSchema" }, { "type": "null" } ] } }, "required": [ "brand", "categories", "categoryIds", "currency", "defaultSellingPrice", "ean", "id", "images", "manufacturer", "manufacturerPartNumber", "name", "organizationId", "sku", "slug", "status", "suppliers", "tags", "unitOfMeasure", "updatedAt", "vatPercentage" ] }, "productStatusSchema": { "type": "string", "enum": [ "active", "discontinued", "archived" ] }, "productSupplierLinkSchema": { "type": "object", "properties": { "createdAt": { "type": "string", "format": "date-time" }, "currency": { "type": [ "string", "null" ] }, "externalId": { "type": [ "string", "null" ] }, "id": { "type": "string", "format": "uuid" }, "isActive": { "type": "boolean" }, "isPreferred": { "type": "boolean" }, "leadTimeDays": { "type": [ "integer", "null" ] }, "minOrderQuantity": { "type": [ "number", "null" ] }, "orderCode": { "type": [ "string", "null" ] }, "orderMultiple": { "type": [ "number", "null" ] }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "productId": { "$ref": "#/components/schemas/productIdSchema" }, "supplier": { "anyOf": [ { "$ref": "#/components/schemas/supplierSchema" }, { "type": "null" } ] }, "supplierId": { "$ref": "#/components/schemas/supplierIdSchema" }, "supplierSku": { "type": [ "string", "null" ] }, "unitCost": { "type": [ "number", "null" ] }, "updatedAt": { "type": "string", "format": "date-time" } }, "required": [ "createdAt", "currency", "externalId", "id", "isActive", "isPreferred", "leadTimeDays", "minOrderQuantity", "orderCode", "orderMultiple", "organizationId", "productId", "supplier", "supplierId", "supplierSku", "unitCost", "updatedAt" ] }, "slugSchema": { "type": "string", "minLength": 1, "maxLength": 160, "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$" }, "stockStatusSchema": { "type": "string", "enum": [ "not_tracked", "discontinued", "out_of_stock", "backordered", "low_stock", "in_stock" ] }, "supplierIdSchema": { "type": "string", "format": "uuid" }, "supplierSchema": { "type": "object", "properties": { "accountNumber": { "type": [ "string", "null" ] }, "addressCity": { "type": [ "string", "null" ] }, "addressCountry": { "type": [ "string", "null" ] }, "addressLine1": { "type": [ "string", "null" ] }, "addressLine2": { "type": [ "string", "null" ] }, "addressPostalCode": { "type": [ "string", "null" ] }, "addressState": { "type": [ "string", "null" ] }, "billingEmail": { "type": [ "string", "null" ] }, "btw": { "type": [ "string", "null" ] }, "code": { "type": [ "string", "null" ] }, "createdAt": { "type": "string", "format": "date-time" }, "currency": { "type": [ "string", "null" ] }, "defaultInventoryLocationId": { "anyOf": [ { "$ref": "#/components/schemas/inventoryLocationIdSchema" }, { "type": "null" } ] }, "defaultLeadTimeDays": { "type": [ "integer", "null" ] }, "email": { "type": [ "string", "null" ] }, "externalId": { "type": [ "string", "null" ] }, "id": { "$ref": "#/components/schemas/supplierIdSchema" }, "kvk": { "type": [ "string", "null" ] }, "name": { "type": "string", "minLength": 1, "maxLength": 240 }, "notes": { "type": [ "string", "null" ] }, "orderingEmail": { "type": [ "string", "null" ] }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "paymentTermsDays": { "type": [ "integer", "null" ] }, "phone": { "type": [ "string", "null" ] }, "source": { "type": [ "string", "null" ] }, "status": { "$ref": "#/components/schemas/supplierStatusSchema" }, "updatedAt": { "type": "string", "format": "date-time" }, "website": { "type": [ "string", "null" ] } }, "required": [ "accountNumber", "addressCity", "addressCountry", "addressLine1", "addressLine2", "addressPostalCode", "addressState", "billingEmail", "btw", "code", "createdAt", "currency", "defaultInventoryLocationId", "defaultLeadTimeDays", "email", "externalId", "id", "kvk", "name", "notes", "orderingEmail", "organizationId", "paymentTermsDays", "phone", "source", "status", "updatedAt", "website" ] }, "supplierStatusSchema": { "type": "string", "enum": [ "active", "inactive", "archived" ] }, "tagColorSchema": { "type": "string", "pattern": "^#(?:[0-9a-f]{3}|[0-9a-f]{6})$" }, "tagIdSchema": { "type": "string", "format": "uuid" }, "tagKindSchema": { "type": "string", "enum": [ "task", "customer", "product" ] }, "tagSlugSchema": { "type": "string", "minLength": 1, "maxLength": 160, "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$" } } ``` # List inventory stock records. (/docs/openapi/listInventoryStockRecords) Return inventory stock records for an organization, optionally filtered by product. ## GET /v1/organizations/{organizationId}/inventory/stock-records ```json { "operationId": "listInventoryStockRecords", "summary": "List inventory stock records.", "description": "Return inventory stock records for an organization, optionally filtered by product.", "tags": [ "Assets" ], "parameters": [ { "in": "query", "name": "offset", "required": true, "schema": { "type": "string" }, "example": "example" }, { "in": "query", "name": "limit", "required": true, "schema": { "type": "string" }, "example": "example" }, { "in": "query", "name": "q", "required": false, "schema": { "type": "string" }, "example": "example" }, { "in": "query", "name": "productId", "required": false, "schema": { "allOf": [ { "$ref": "#/components/schemas/productIdSchema" } ] }, "example": "123" }, { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "responses": { "200": { "description": "Returns inventory stock records.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/inventoryStockRecordsResponseSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "assetImageIdSchema": { "type": "string", "format": "uuid" }, "assetImageSchema": { "type": "object", "properties": { "altText": { "type": [ "string", "null" ] }, "createdAt": { "type": "string", "format": "date-time" }, "id": { "$ref": "#/components/schemas/assetImageIdSchema" }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "position": { "type": "integer", "minimum": 0 }, "productId": { "anyOf": [ { "$ref": "#/components/schemas/productIdSchema" }, { "type": "null" } ] }, "updatedAt": { "type": "string", "format": "date-time" }, "url": { "type": "string", "format": "uri" } }, "required": [ "altText", "createdAt", "id", "organizationId", "position", "productId", "updatedAt", "url" ] }, "brandIdSchema": { "type": "string", "enum": [ "centrakit", "mechup", "stabledock", "caresly", "propdock", "marryduck", "signaldock", "spacelio", "legaldock" ] }, "iconNameSchema": { "type": "string", "minLength": 1, "maxLength": 120, "pattern": "^[A-Za-z0-9]+Icon$" }, "inventoryLocationIdSchema": { "type": "string", "format": "uuid" }, "inventoryLocationSchema": { "type": "object", "properties": { "createdAt": { "type": "string", "format": "date-time" }, "description": { "type": [ "string", "null" ] }, "id": { "$ref": "#/components/schemas/inventoryLocationIdSchema" }, "name": { "type": "string", "minLength": 1, "maxLength": 240 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "updatedAt": { "type": "string", "format": "date-time" } }, "required": [ "createdAt", "description", "id", "name", "organizationId", "updatedAt" ] }, "inventoryStatusSchema": { "type": "string", "enum": [ "available", "quarantined", "expired", "archived" ] }, "inventoryStockRecordIdSchema": { "type": "string", "format": "uuid" }, "inventoryStockRecordSchema": { "type": "object", "properties": { "allocatedQuantity": { "type": "number", "minimum": 0 }, "availableQuantity": { "type": "number", "minimum": 0 }, "binLocation": { "type": [ "string", "null" ] }, "createdAt": { "type": "string", "format": "date-time" }, "createdBy": { "type": [ "string", "null" ], "format": "uuid" }, "currency": { "type": [ "string", "null" ] }, "expiresOn": { "$ref": "#/components/schemas/nullableDate" }, "id": { "$ref": "#/components/schemas/inventoryStockRecordIdSchema" }, "lastCountedOn": { "$ref": "#/components/schemas/nullableDate" }, "location": { "anyOf": [ { "$ref": "#/components/schemas/inventoryLocationSchema" }, { "type": "null" } ] }, "locationId": { "anyOf": [ { "$ref": "#/components/schemas/inventoryLocationIdSchema" }, { "type": "null" } ] }, "lotNumber": { "type": [ "string", "null" ] }, "notes": { "type": [ "string", "null" ] }, "onHandQuantity": { "type": "number", "minimum": 0 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "product": { "anyOf": [ { "$ref": "#/components/schemas/productListItemSchema" }, { "type": "null" } ] }, "productId": { "$ref": "#/components/schemas/productIdSchema" }, "serialNumber": { "type": [ "string", "null" ] }, "status": { "$ref": "#/components/schemas/inventoryStatusSchema" }, "unitCost": { "type": [ "number", "null" ] }, "updatedAt": { "type": "string", "format": "date-time" }, "updatedBy": { "type": [ "string", "null" ], "format": "uuid" }, "variantId": { "type": "string", "format": "uuid" } }, "required": [ "allocatedQuantity", "availableQuantity", "binLocation", "createdAt", "createdBy", "currency", "expiresOn", "id", "lastCountedOn", "location", "locationId", "lotNumber", "notes", "onHandQuantity", "organizationId", "product", "productId", "serialNumber", "status", "unitCost", "updatedAt", "updatedBy", "variantId" ] }, "inventoryStockRecordsResponseSchema": { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/components/schemas/inventoryStockRecordSchema" } }, "pagination": { "$ref": "#/components/schemas/listPaginationSchema" } }, "required": [ "data", "pagination" ] }, "listPaginationSchema": { "type": "object", "properties": { "hasMore": { "type": "boolean" }, "limit": { "type": "integer", "minimum": 1, "maximum": 100 }, "offset": { "type": "integer", "minimum": 0 }, "total": { "type": "integer", "minimum": 0 }, "totalIsEstimate": { "type": "boolean" } }, "required": [ "hasMore", "limit", "offset", "total" ] }, "nullableDate": { "type": [ "string", "null" ], "format": "date" }, "organizationIdSchema": { "type": "string", "format": "uuid" }, "organizationTagSchema": { "type": "object", "properties": { "color": { "anyOf": [ { "$ref": "#/components/schemas/tagColorSchema" }, { "type": "null" } ] }, "createdAt": { "type": "string", "format": "date-time" }, "description": { "type": [ "string", "null" ] }, "id": { "$ref": "#/components/schemas/tagIdSchema" }, "kind": { "$ref": "#/components/schemas/tagKindSchema" }, "name": { "type": "string", "minLength": 1, "maxLength": 80 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "position": { "type": "integer", "minimum": 0 }, "slug": { "$ref": "#/components/schemas/tagSlugSchema" }, "updatedAt": { "type": "string", "format": "date-time" } }, "required": [ "color", "createdAt", "description", "id", "kind", "name", "organizationId", "position", "slug", "updatedAt" ] }, "productCategorySchema": { "type": "object", "properties": { "createdAt": { "type": "string", "format": "date-time" }, "description": { "type": [ "string", "null" ] }, "icon": { "$ref": "#/components/schemas/iconNameSchema" }, "id": { "type": "string", "format": "uuid" }, "name": { "type": "string", "minLength": 1, "maxLength": 120 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "parentId": { "type": [ "string", "null" ], "format": "uuid" }, "position": { "type": "integer", "minimum": 0 }, "slug": { "$ref": "#/components/schemas/slugSchema" }, "updatedAt": { "type": "string", "format": "date-time" } }, "required": [ "createdAt", "description", "icon", "id", "name", "organizationId", "parentId", "position", "slug", "updatedAt" ] }, "productIdSchema": { "type": "string", "format": "uuid" }, "productListItemSchema": { "type": "object", "properties": { "brand": { "type": [ "object", "null" ], "properties": { "createdAt": { "type": "string", "format": "date-time" }, "description": { "type": [ "string", "null" ] }, "id": { "$ref": "#/components/schemas/brandIdSchema" }, "logoUrl": { "type": [ "string", "null" ], "format": "uri" }, "name": { "type": "string", "minLength": 1, "maxLength": 240 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "slug": { "type": "string", "minLength": 1, "maxLength": 160, "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$" }, "updatedAt": { "type": "string", "format": "date-time" }, "website": { "type": [ "string", "null" ] } }, "required": [ "createdAt", "description", "id", "logoUrl", "name", "organizationId", "slug", "updatedAt", "website" ] }, "categories": { "type": "array", "items": { "$ref": "#/components/schemas/productCategorySchema" } }, "categoryIds": { "type": "array", "items": { "type": "string", "format": "uuid" } }, "currency": { "type": [ "string", "null" ] }, "defaultSellingPrice": { "type": [ "number", "null" ] }, "ean": { "type": [ "string", "null" ] }, "id": { "$ref": "#/components/schemas/productIdSchema" }, "images": { "type": "array", "items": { "$ref": "#/components/schemas/assetImageSchema" } }, "manufacturer": { "type": [ "string", "null" ] }, "manufacturerPartNumber": { "type": [ "string", "null" ] }, "name": { "type": "string", "minLength": 1 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "sku": { "type": [ "string", "null" ] }, "slug": { "$ref": "#/components/schemas/slugSchema" }, "status": { "$ref": "#/components/schemas/productStatusSchema" }, "suppliers": { "type": "array", "items": { "$ref": "#/components/schemas/productSupplierLinkSchema" } }, "tags": { "type": "array", "items": { "$ref": "#/components/schemas/organizationTagSchema" } }, "unitOfMeasure": { "type": [ "string", "null" ] }, "updatedAt": { "type": "string", "format": "date-time" }, "vatPercentage": { "type": [ "number", "null" ] }, "stockStatus": { "anyOf": [ { "$ref": "#/components/schemas/stockStatusSchema" }, { "type": "null" } ] } }, "required": [ "brand", "categories", "categoryIds", "currency", "defaultSellingPrice", "ean", "id", "images", "manufacturer", "manufacturerPartNumber", "name", "organizationId", "sku", "slug", "status", "suppliers", "tags", "unitOfMeasure", "updatedAt", "vatPercentage" ] }, "productStatusSchema": { "type": "string", "enum": [ "active", "discontinued", "archived" ] }, "productSupplierLinkSchema": { "type": "object", "properties": { "createdAt": { "type": "string", "format": "date-time" }, "currency": { "type": [ "string", "null" ] }, "externalId": { "type": [ "string", "null" ] }, "id": { "type": "string", "format": "uuid" }, "isActive": { "type": "boolean" }, "isPreferred": { "type": "boolean" }, "leadTimeDays": { "type": [ "integer", "null" ] }, "minOrderQuantity": { "type": [ "number", "null" ] }, "orderCode": { "type": [ "string", "null" ] }, "orderMultiple": { "type": [ "number", "null" ] }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "productId": { "$ref": "#/components/schemas/productIdSchema" }, "supplier": { "anyOf": [ { "$ref": "#/components/schemas/supplierSchema" }, { "type": "null" } ] }, "supplierId": { "$ref": "#/components/schemas/supplierIdSchema" }, "supplierSku": { "type": [ "string", "null" ] }, "unitCost": { "type": [ "number", "null" ] }, "updatedAt": { "type": "string", "format": "date-time" } }, "required": [ "createdAt", "currency", "externalId", "id", "isActive", "isPreferred", "leadTimeDays", "minOrderQuantity", "orderCode", "orderMultiple", "organizationId", "productId", "supplier", "supplierId", "supplierSku", "unitCost", "updatedAt" ] }, "slugSchema": { "type": "string", "minLength": 1, "maxLength": 160, "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$" }, "stockStatusSchema": { "type": "string", "enum": [ "not_tracked", "discontinued", "out_of_stock", "backordered", "low_stock", "in_stock" ] }, "supplierIdSchema": { "type": "string", "format": "uuid" }, "supplierSchema": { "type": "object", "properties": { "accountNumber": { "type": [ "string", "null" ] }, "addressCity": { "type": [ "string", "null" ] }, "addressCountry": { "type": [ "string", "null" ] }, "addressLine1": { "type": [ "string", "null" ] }, "addressLine2": { "type": [ "string", "null" ] }, "addressPostalCode": { "type": [ "string", "null" ] }, "addressState": { "type": [ "string", "null" ] }, "billingEmail": { "type": [ "string", "null" ] }, "btw": { "type": [ "string", "null" ] }, "code": { "type": [ "string", "null" ] }, "createdAt": { "type": "string", "format": "date-time" }, "currency": { "type": [ "string", "null" ] }, "defaultInventoryLocationId": { "anyOf": [ { "$ref": "#/components/schemas/inventoryLocationIdSchema" }, { "type": "null" } ] }, "defaultLeadTimeDays": { "type": [ "integer", "null" ] }, "email": { "type": [ "string", "null" ] }, "externalId": { "type": [ "string", "null" ] }, "id": { "$ref": "#/components/schemas/supplierIdSchema" }, "kvk": { "type": [ "string", "null" ] }, "name": { "type": "string", "minLength": 1, "maxLength": 240 }, "notes": { "type": [ "string", "null" ] }, "orderingEmail": { "type": [ "string", "null" ] }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "paymentTermsDays": { "type": [ "integer", "null" ] }, "phone": { "type": [ "string", "null" ] }, "source": { "type": [ "string", "null" ] }, "status": { "$ref": "#/components/schemas/supplierStatusSchema" }, "updatedAt": { "type": "string", "format": "date-time" }, "website": { "type": [ "string", "null" ] } }, "required": [ "accountNumber", "addressCity", "addressCountry", "addressLine1", "addressLine2", "addressPostalCode", "addressState", "billingEmail", "btw", "code", "createdAt", "currency", "defaultInventoryLocationId", "defaultLeadTimeDays", "email", "externalId", "id", "kvk", "name", "notes", "orderingEmail", "organizationId", "paymentTermsDays", "phone", "source", "status", "updatedAt", "website" ] }, "supplierStatusSchema": { "type": "string", "enum": [ "active", "inactive", "archived" ] }, "tagColorSchema": { "type": "string", "pattern": "^#(?:[0-9a-f]{3}|[0-9a-f]{6})$" }, "tagIdSchema": { "type": "string", "format": "uuid" }, "tagKindSchema": { "type": "string", "enum": [ "task", "customer", "product" ] }, "tagSlugSchema": { "type": "string", "minLength": 1, "maxLength": 160, "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$" } } ``` # Create an inventory stock record. (/docs/openapi/createInventoryStockRecord) Create an inventory stock record for a product. ## POST /v1/organizations/{organizationId}/inventory/stock-records ```json { "operationId": "createInventoryStockRecord", "summary": "Create an inventory stock record.", "description": "Create an inventory stock record for a product.", "tags": [ "Assets" ], "parameters": [ { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/inventoryStockRecordInputSchema" } } }, "description": "The inventory stock record payload." }, "responses": { "201": { "description": "Returns the created stock record.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/inventoryStockRecordSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "assetImageIdSchema": { "type": "string", "format": "uuid" }, "assetImageSchema": { "type": "object", "properties": { "altText": { "type": [ "string", "null" ] }, "createdAt": { "type": "string", "format": "date-time" }, "id": { "$ref": "#/components/schemas/assetImageIdSchema" }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "position": { "type": "integer", "minimum": 0 }, "productId": { "anyOf": [ { "$ref": "#/components/schemas/productIdSchema" }, { "type": "null" } ] }, "updatedAt": { "type": "string", "format": "date-time" }, "url": { "type": "string", "format": "uri" } }, "required": [ "altText", "createdAt", "id", "organizationId", "position", "productId", "updatedAt", "url" ] }, "brandIdSchema": { "type": "string", "enum": [ "centrakit", "mechup", "stabledock", "caresly", "propdock", "marryduck", "signaldock", "spacelio", "legaldock" ] }, "currencyCodeSchema": { "type": "string", "pattern": "^[A-Z]{3}$" }, "iconNameSchema": { "type": "string", "minLength": 1, "maxLength": 120, "pattern": "^[A-Za-z0-9]+Icon$" }, "inventoryLocationIdSchema": { "type": "string", "format": "uuid" }, "inventoryLocationSchema": { "type": "object", "properties": { "createdAt": { "type": "string", "format": "date-time" }, "description": { "type": [ "string", "null" ] }, "id": { "$ref": "#/components/schemas/inventoryLocationIdSchema" }, "name": { "type": "string", "minLength": 1, "maxLength": 240 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "updatedAt": { "type": "string", "format": "date-time" } }, "required": [ "createdAt", "description", "id", "name", "organizationId", "updatedAt" ] }, "inventoryStatusSchema": { "type": "string", "enum": [ "available", "quarantined", "expired", "archived" ] }, "inventoryStockRecordIdSchema": { "type": "string", "format": "uuid" }, "inventoryStockRecordInputSchema": { "type": "object", "properties": { "binLocation": { "allOf": [ { "$ref": "#/components/schemas/nullableShortText" } ] }, "currency": { "allOf": [ { "$ref": "#/components/schemas/nullableCurrency" } ] }, "expiresOn": { "allOf": [ { "$ref": "#/components/schemas/nullableDate" } ] }, "lastCountedOn": { "allOf": [ { "$ref": "#/components/schemas/nullableDate" } ] }, "locationId": { "anyOf": [ { "$ref": "#/components/schemas/inventoryLocationIdSchema" }, { "type": "null" } ] }, "locationName": { "allOf": [ { "$ref": "#/components/schemas/nullableShortText" } ] }, "lotNumber": { "allOf": [ { "$ref": "#/components/schemas/nullableShortText" } ] }, "notes": { "allOf": [ { "$ref": "#/components/schemas/nullableLongText" } ] }, "onHandQuantity": { "type": "number", "minimum": 0 }, "productId": { "$ref": "#/components/schemas/productIdSchema" }, "serialNumber": { "allOf": [ { "$ref": "#/components/schemas/nullableShortText" } ] }, "status": { "allOf": [ { "$ref": "#/components/schemas/inventoryStatusSchema" } ] }, "unitCost": { "allOf": [ { "$ref": "#/components/schemas/nullableMoney" } ] }, "variantId": { "type": "string", "format": "uuid" } }, "required": [ "onHandQuantity", "productId" ] }, "inventoryStockRecordSchema": { "type": "object", "properties": { "allocatedQuantity": { "type": "number", "minimum": 0 }, "availableQuantity": { "type": "number", "minimum": 0 }, "binLocation": { "type": [ "string", "null" ] }, "createdAt": { "type": "string", "format": "date-time" }, "createdBy": { "type": [ "string", "null" ], "format": "uuid" }, "currency": { "type": [ "string", "null" ] }, "expiresOn": { "$ref": "#/components/schemas/nullableDate" }, "id": { "$ref": "#/components/schemas/inventoryStockRecordIdSchema" }, "lastCountedOn": { "$ref": "#/components/schemas/nullableDate" }, "location": { "anyOf": [ { "$ref": "#/components/schemas/inventoryLocationSchema" }, { "type": "null" } ] }, "locationId": { "anyOf": [ { "$ref": "#/components/schemas/inventoryLocationIdSchema" }, { "type": "null" } ] }, "lotNumber": { "type": [ "string", "null" ] }, "notes": { "type": [ "string", "null" ] }, "onHandQuantity": { "type": "number", "minimum": 0 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "product": { "anyOf": [ { "$ref": "#/components/schemas/productListItemSchema" }, { "type": "null" } ] }, "productId": { "$ref": "#/components/schemas/productIdSchema" }, "serialNumber": { "type": [ "string", "null" ] }, "status": { "$ref": "#/components/schemas/inventoryStatusSchema" }, "unitCost": { "type": [ "number", "null" ] }, "updatedAt": { "type": "string", "format": "date-time" }, "updatedBy": { "type": [ "string", "null" ], "format": "uuid" }, "variantId": { "type": "string", "format": "uuid" } }, "required": [ "allocatedQuantity", "availableQuantity", "binLocation", "createdAt", "createdBy", "currency", "expiresOn", "id", "lastCountedOn", "location", "locationId", "lotNumber", "notes", "onHandQuantity", "organizationId", "product", "productId", "serialNumber", "status", "unitCost", "updatedAt", "updatedBy", "variantId" ] }, "nullableCurrency": { "anyOf": [ { "$ref": "#/components/schemas/currencyCodeSchema" }, { "type": "null" } ] }, "nullableDate": { "type": [ "string", "null" ], "format": "date" }, "nullableLongText": { "type": [ "string", "null" ], "maxLength": 4000 }, "nullableMoney": { "type": [ "number", "null" ], "minimum": 0 }, "nullableShortText": { "type": [ "string", "null" ], "maxLength": 240 }, "organizationIdSchema": { "type": "string", "format": "uuid" }, "organizationTagSchema": { "type": "object", "properties": { "color": { "anyOf": [ { "$ref": "#/components/schemas/tagColorSchema" }, { "type": "null" } ] }, "createdAt": { "type": "string", "format": "date-time" }, "description": { "type": [ "string", "null" ] }, "id": { "$ref": "#/components/schemas/tagIdSchema" }, "kind": { "$ref": "#/components/schemas/tagKindSchema" }, "name": { "type": "string", "minLength": 1, "maxLength": 80 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "position": { "type": "integer", "minimum": 0 }, "slug": { "$ref": "#/components/schemas/tagSlugSchema" }, "updatedAt": { "type": "string", "format": "date-time" } }, "required": [ "color", "createdAt", "description", "id", "kind", "name", "organizationId", "position", "slug", "updatedAt" ] }, "productCategorySchema": { "type": "object", "properties": { "createdAt": { "type": "string", "format": "date-time" }, "description": { "type": [ "string", "null" ] }, "icon": { "$ref": "#/components/schemas/iconNameSchema" }, "id": { "type": "string", "format": "uuid" }, "name": { "type": "string", "minLength": 1, "maxLength": 120 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "parentId": { "type": [ "string", "null" ], "format": "uuid" }, "position": { "type": "integer", "minimum": 0 }, "slug": { "$ref": "#/components/schemas/slugSchema" }, "updatedAt": { "type": "string", "format": "date-time" } }, "required": [ "createdAt", "description", "icon", "id", "name", "organizationId", "parentId", "position", "slug", "updatedAt" ] }, "productIdSchema": { "type": "string", "format": "uuid" }, "productListItemSchema": { "type": "object", "properties": { "brand": { "type": [ "object", "null" ], "properties": { "createdAt": { "type": "string", "format": "date-time" }, "description": { "type": [ "string", "null" ] }, "id": { "$ref": "#/components/schemas/brandIdSchema" }, "logoUrl": { "type": [ "string", "null" ], "format": "uri" }, "name": { "type": "string", "minLength": 1, "maxLength": 240 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "slug": { "type": "string", "minLength": 1, "maxLength": 160, "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$" }, "updatedAt": { "type": "string", "format": "date-time" }, "website": { "type": [ "string", "null" ] } }, "required": [ "createdAt", "description", "id", "logoUrl", "name", "organizationId", "slug", "updatedAt", "website" ] }, "categories": { "type": "array", "items": { "$ref": "#/components/schemas/productCategorySchema" } }, "categoryIds": { "type": "array", "items": { "type": "string", "format": "uuid" } }, "currency": { "type": [ "string", "null" ] }, "defaultSellingPrice": { "type": [ "number", "null" ] }, "ean": { "type": [ "string", "null" ] }, "id": { "$ref": "#/components/schemas/productIdSchema" }, "images": { "type": "array", "items": { "$ref": "#/components/schemas/assetImageSchema" } }, "manufacturer": { "type": [ "string", "null" ] }, "manufacturerPartNumber": { "type": [ "string", "null" ] }, "name": { "type": "string", "minLength": 1 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "sku": { "type": [ "string", "null" ] }, "slug": { "$ref": "#/components/schemas/slugSchema" }, "status": { "$ref": "#/components/schemas/productStatusSchema" }, "suppliers": { "type": "array", "items": { "$ref": "#/components/schemas/productSupplierLinkSchema" } }, "tags": { "type": "array", "items": { "$ref": "#/components/schemas/organizationTagSchema" } }, "unitOfMeasure": { "type": [ "string", "null" ] }, "updatedAt": { "type": "string", "format": "date-time" }, "vatPercentage": { "type": [ "number", "null" ] }, "stockStatus": { "anyOf": [ { "$ref": "#/components/schemas/stockStatusSchema" }, { "type": "null" } ] } }, "required": [ "brand", "categories", "categoryIds", "currency", "defaultSellingPrice", "ean", "id", "images", "manufacturer", "manufacturerPartNumber", "name", "organizationId", "sku", "slug", "status", "suppliers", "tags", "unitOfMeasure", "updatedAt", "vatPercentage" ] }, "productStatusSchema": { "type": "string", "enum": [ "active", "discontinued", "archived" ] }, "productSupplierLinkSchema": { "type": "object", "properties": { "createdAt": { "type": "string", "format": "date-time" }, "currency": { "type": [ "string", "null" ] }, "externalId": { "type": [ "string", "null" ] }, "id": { "type": "string", "format": "uuid" }, "isActive": { "type": "boolean" }, "isPreferred": { "type": "boolean" }, "leadTimeDays": { "type": [ "integer", "null" ] }, "minOrderQuantity": { "type": [ "number", "null" ] }, "orderCode": { "type": [ "string", "null" ] }, "orderMultiple": { "type": [ "number", "null" ] }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "productId": { "$ref": "#/components/schemas/productIdSchema" }, "supplier": { "anyOf": [ { "$ref": "#/components/schemas/supplierSchema" }, { "type": "null" } ] }, "supplierId": { "$ref": "#/components/schemas/supplierIdSchema" }, "supplierSku": { "type": [ "string", "null" ] }, "unitCost": { "type": [ "number", "null" ] }, "updatedAt": { "type": "string", "format": "date-time" } }, "required": [ "createdAt", "currency", "externalId", "id", "isActive", "isPreferred", "leadTimeDays", "minOrderQuantity", "orderCode", "orderMultiple", "organizationId", "productId", "supplier", "supplierId", "supplierSku", "unitCost", "updatedAt" ] }, "slugSchema": { "type": "string", "minLength": 1, "maxLength": 160, "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$" }, "stockStatusSchema": { "type": "string", "enum": [ "not_tracked", "discontinued", "out_of_stock", "backordered", "low_stock", "in_stock" ] }, "supplierIdSchema": { "type": "string", "format": "uuid" }, "supplierSchema": { "type": "object", "properties": { "accountNumber": { "type": [ "string", "null" ] }, "addressCity": { "type": [ "string", "null" ] }, "addressCountry": { "type": [ "string", "null" ] }, "addressLine1": { "type": [ "string", "null" ] }, "addressLine2": { "type": [ "string", "null" ] }, "addressPostalCode": { "type": [ "string", "null" ] }, "addressState": { "type": [ "string", "null" ] }, "billingEmail": { "type": [ "string", "null" ] }, "btw": { "type": [ "string", "null" ] }, "code": { "type": [ "string", "null" ] }, "createdAt": { "type": "string", "format": "date-time" }, "currency": { "type": [ "string", "null" ] }, "defaultInventoryLocationId": { "anyOf": [ { "$ref": "#/components/schemas/inventoryLocationIdSchema" }, { "type": "null" } ] }, "defaultLeadTimeDays": { "type": [ "integer", "null" ] }, "email": { "type": [ "string", "null" ] }, "externalId": { "type": [ "string", "null" ] }, "id": { "$ref": "#/components/schemas/supplierIdSchema" }, "kvk": { "type": [ "string", "null" ] }, "name": { "type": "string", "minLength": 1, "maxLength": 240 }, "notes": { "type": [ "string", "null" ] }, "orderingEmail": { "type": [ "string", "null" ] }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "paymentTermsDays": { "type": [ "integer", "null" ] }, "phone": { "type": [ "string", "null" ] }, "source": { "type": [ "string", "null" ] }, "status": { "$ref": "#/components/schemas/supplierStatusSchema" }, "updatedAt": { "type": "string", "format": "date-time" }, "website": { "type": [ "string", "null" ] } }, "required": [ "accountNumber", "addressCity", "addressCountry", "addressLine1", "addressLine2", "addressPostalCode", "addressState", "billingEmail", "btw", "code", "createdAt", "currency", "defaultInventoryLocationId", "defaultLeadTimeDays", "email", "externalId", "id", "kvk", "name", "notes", "orderingEmail", "organizationId", "paymentTermsDays", "phone", "source", "status", "updatedAt", "website" ] }, "supplierStatusSchema": { "type": "string", "enum": [ "active", "inactive", "archived" ] }, "tagColorSchema": { "type": "string", "pattern": "^#(?:[0-9a-f]{3}|[0-9a-f]{6})$" }, "tagIdSchema": { "type": "string", "format": "uuid" }, "tagKindSchema": { "type": "string", "enum": [ "task", "customer", "product" ] }, "tagSlugSchema": { "type": "string", "minLength": 1, "maxLength": 160, "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$" } } ``` # Get a product category. (/docs/openapi/getProductCategory) Return a product category for the specified organization. ## GET /v1/organizations/{organizationId}/product-categories/{categoryId} ```json { "operationId": "getProductCategory", "summary": "Get a product category.", "description": "Return a product category for the specified organization.", "tags": [ "Assets" ], "parameters": [ { "in": "path", "name": "categoryId", "required": true, "schema": { "type": "string", "format": "uuid" }, "example": "123e4567-e89b-12d3-a456-426614174000" }, { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "responses": { "200": { "description": "Returns the requested product category.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/productCategorySchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "iconNameSchema": { "type": "string", "minLength": 1, "maxLength": 120, "pattern": "^[A-Za-z0-9]+Icon$" }, "organizationIdSchema": { "type": "string", "format": "uuid" }, "productCategorySchema": { "type": "object", "properties": { "createdAt": { "type": "string", "format": "date-time" }, "description": { "type": [ "string", "null" ] }, "icon": { "$ref": "#/components/schemas/iconNameSchema" }, "id": { "type": "string", "format": "uuid" }, "name": { "type": "string", "minLength": 1, "maxLength": 120 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "parentId": { "type": [ "string", "null" ], "format": "uuid" }, "position": { "type": "integer", "minimum": 0 }, "slug": { "$ref": "#/components/schemas/slugSchema" }, "updatedAt": { "type": "string", "format": "date-time" } }, "required": [ "createdAt", "description", "icon", "id", "name", "organizationId", "parentId", "position", "slug", "updatedAt" ] }, "slugSchema": { "type": "string", "minLength": 1, "maxLength": 160, "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$" } } ``` # Update a product category. (/docs/openapi/updateProductCategory) Update a product category for the specified organization. ## PATCH /v1/organizations/{organizationId}/product-categories/{categoryId} ```json { "operationId": "updateProductCategory", "summary": "Update a product category.", "description": "Update a product category for the specified organization.", "tags": [ "Assets" ], "parameters": [ { "in": "path", "name": "categoryId", "required": true, "schema": { "type": "string", "format": "uuid" }, "example": "123e4567-e89b-12d3-a456-426614174000" }, { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/productCategoryInputSchema" } } }, "description": "The product category payload." }, "responses": { "200": { "description": "Returns the updated product category.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/productCategorySchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "iconNameInputSchema": { "type": "string", "minLength": 1, "maxLength": 120, "pattern": "^[A-Za-z0-9]+Icon$" }, "iconNameSchema": { "type": "string", "minLength": 1, "maxLength": 120, "pattern": "^[A-Za-z0-9]+Icon$" }, "nullableLongText": { "type": [ "string", "null" ], "maxLength": 4000 }, "organizationIdSchema": { "type": "string", "format": "uuid" }, "productCategoryInputSchema": { "type": "object", "properties": { "description": { "allOf": [ { "$ref": "#/components/schemas/nullableLongText" } ] }, "icon": { "allOf": [ { "$ref": "#/components/schemas/iconNameInputSchema" } ] }, "name": { "type": "string", "minLength": 1, "maxLength": 120 }, "parentId": { "type": [ "string", "null" ], "format": "uuid" }, "position": { "type": "integer", "minimum": 0 }, "slug": { "allOf": [ { "$ref": "#/components/schemas/slugInputSchema" } ] } }, "required": [ "name" ] }, "productCategorySchema": { "type": "object", "properties": { "createdAt": { "type": "string", "format": "date-time" }, "description": { "type": [ "string", "null" ] }, "icon": { "$ref": "#/components/schemas/iconNameSchema" }, "id": { "type": "string", "format": "uuid" }, "name": { "type": "string", "minLength": 1, "maxLength": 120 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "parentId": { "type": [ "string", "null" ], "format": "uuid" }, "position": { "type": "integer", "minimum": 0 }, "slug": { "$ref": "#/components/schemas/slugSchema" }, "updatedAt": { "type": "string", "format": "date-time" } }, "required": [ "createdAt", "description", "icon", "id", "name", "organizationId", "parentId", "position", "slug", "updatedAt" ] }, "slugInputSchema": { "type": "string", "minLength": 1, "maxLength": 160, "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$" }, "slugSchema": { "type": "string", "minLength": 1, "maxLength": 160, "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$" } } ``` # Delete a product category. (/docs/openapi/deleteProductCategory) Delete a product category for the specified organization. ## DELETE /v1/organizations/{organizationId}/product-categories/{categoryId} ```json { "operationId": "deleteProductCategory", "summary": "Delete a product category.", "description": "Delete a product category for the specified organization.", "tags": [ "Assets" ], "parameters": [ { "in": "path", "name": "categoryId", "required": true, "schema": { "type": "string", "format": "uuid" }, "example": "123e4567-e89b-12d3-a456-426614174000" }, { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "responses": { "200": { "description": "Returns a success payload when the product category is deleted.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/publicApiSuccessResponseSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "organizationIdSchema": { "type": "string", "format": "uuid" }, "publicApiSuccessResponseSchema": { "type": "object", "properties": { "success": { "type": "boolean", "enum": [ true ] } }, "required": [ "success" ] } } ``` # Get a product. (/docs/openapi/getProduct) Return a product for the specified organization. ## GET /v1/organizations/{organizationId}/products/{productId} ```json { "operationId": "getProduct", "summary": "Get a product.", "description": "Return a product for the specified organization.", "tags": [ "Assets" ], "parameters": [ { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" }, { "in": "path", "name": "productId", "required": true, "schema": { "$ref": "#/components/schemas/productIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "responses": { "200": { "description": "Returns the requested product.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/productSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "assetCustomAttributeSchema": { "type": "object", "properties": { "booleanValue": { "type": [ "boolean", "null" ] }, "dateValue": { "$ref": "#/components/schemas/nullableDate" }, "numberValue": { "type": [ "number", "null" ] }, "textValue": { "type": [ "string", "null" ] }, "urlValue": { "type": [ "string", "null" ] }, "valueType": { "$ref": "#/components/schemas/customAttributeValueTypeSchema" }, "createdAt": { "type": "string", "format": "date-time" }, "customerAssetId": { "anyOf": [ { "$ref": "#/components/schemas/customerAssetIdSchema" }, { "type": "null" } ] }, "id": { "type": "string", "format": "uuid" }, "key": { "type": "string", "minLength": 1, "maxLength": 120 }, "label": { "type": "string", "minLength": 1, "maxLength": 160 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "position": { "type": "integer", "minimum": 0 }, "productId": { "anyOf": [ { "$ref": "#/components/schemas/productIdSchema" }, { "type": "null" } ] }, "updatedAt": { "type": "string", "format": "date-time" } }, "required": [ "booleanValue", "dateValue", "numberValue", "textValue", "urlValue", "valueType", "createdAt", "customerAssetId", "id", "key", "label", "organizationId", "position", "productId", "updatedAt" ] }, "assetImageIdSchema": { "type": "string", "format": "uuid" }, "assetImageSchema": { "type": "object", "properties": { "altText": { "type": [ "string", "null" ] }, "createdAt": { "type": "string", "format": "date-time" }, "id": { "$ref": "#/components/schemas/assetImageIdSchema" }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "position": { "type": "integer", "minimum": 0 }, "productId": { "anyOf": [ { "$ref": "#/components/schemas/productIdSchema" }, { "type": "null" } ] }, "updatedAt": { "type": "string", "format": "date-time" }, "url": { "type": "string", "format": "uri" } }, "required": [ "altText", "createdAt", "id", "organizationId", "position", "productId", "updatedAt", "url" ] }, "brandIdSchema": { "type": "string", "enum": [ "centrakit", "mechup", "stabledock", "caresly", "propdock", "marryduck", "signaldock", "spacelio", "legaldock" ] }, "customAttributeValueTypeSchema": { "type": "string", "enum": [ "text", "number", "boolean", "date", "url" ] }, "customerAssetIdSchema": { "type": "string", "format": "uuid" }, "digitalDeliveryKindSchema": { "type": "string", "enum": [ "file", "license_key", "link", "manual" ] }, "iconNameSchema": { "type": "string", "minLength": 1, "maxLength": 120, "pattern": "^[A-Za-z0-9]+Icon$" }, "inventoryLocationIdSchema": { "type": "string", "format": "uuid" }, "nullableDate": { "type": [ "string", "null" ], "format": "date" }, "organizationIdSchema": { "type": "string", "format": "uuid" }, "organizationTagSchema": { "type": "object", "properties": { "color": { "anyOf": [ { "$ref": "#/components/schemas/tagColorSchema" }, { "type": "null" } ] }, "createdAt": { "type": "string", "format": "date-time" }, "description": { "type": [ "string", "null" ] }, "id": { "$ref": "#/components/schemas/tagIdSchema" }, "kind": { "$ref": "#/components/schemas/tagKindSchema" }, "name": { "type": "string", "minLength": 1, "maxLength": 80 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "position": { "type": "integer", "minimum": 0 }, "slug": { "$ref": "#/components/schemas/tagSlugSchema" }, "updatedAt": { "type": "string", "format": "date-time" } }, "required": [ "color", "createdAt", "description", "id", "kind", "name", "organizationId", "position", "slug", "updatedAt" ] }, "productCategorySchema": { "type": "object", "properties": { "createdAt": { "type": "string", "format": "date-time" }, "description": { "type": [ "string", "null" ] }, "icon": { "$ref": "#/components/schemas/iconNameSchema" }, "id": { "type": "string", "format": "uuid" }, "name": { "type": "string", "minLength": 1, "maxLength": 120 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "parentId": { "type": [ "string", "null" ], "format": "uuid" }, "position": { "type": "integer", "minimum": 0 }, "slug": { "$ref": "#/components/schemas/slugSchema" }, "updatedAt": { "type": "string", "format": "date-time" } }, "required": [ "createdAt", "description", "icon", "id", "name", "organizationId", "parentId", "position", "slug", "updatedAt" ] }, "productIdSchema": { "type": "string", "format": "uuid" }, "productOptionSchema": { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "name": { "type": "string" }, "position": { "type": "integer" }, "values": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "position": { "type": "integer" }, "value": { "type": "string" } }, "required": [ "id", "position", "value" ] } } }, "required": [ "id", "name", "position", "values" ] }, "productSchema": { "type": "object", "properties": { "brand": { "type": [ "object", "null" ], "properties": { "createdAt": { "type": "string", "format": "date-time" }, "description": { "type": [ "string", "null" ] }, "id": { "$ref": "#/components/schemas/brandIdSchema" }, "logoUrl": { "type": [ "string", "null" ], "format": "uri" }, "name": { "type": "string", "minLength": 1, "maxLength": 240 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "slug": { "type": "string", "minLength": 1, "maxLength": 160, "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$" }, "updatedAt": { "type": "string", "format": "date-time" }, "website": { "type": [ "string", "null" ] } }, "required": [ "createdAt", "description", "id", "logoUrl", "name", "organizationId", "slug", "updatedAt", "website" ] }, "brandId": { "anyOf": [ { "$ref": "#/components/schemas/brandIdSchema" }, { "type": "null" } ] }, "categories": { "type": "array", "items": { "$ref": "#/components/schemas/productCategorySchema" } }, "categoryIds": { "type": "array", "items": { "type": "string", "format": "uuid" } }, "countryOfOrigin": { "type": [ "string", "null" ] }, "createdAt": { "type": "string", "format": "date-time" }, "createdBy": { "type": [ "string", "null" ], "format": "uuid" }, "currency": { "type": [ "string", "null" ] }, "customAttributes": { "type": "array", "items": { "$ref": "#/components/schemas/assetCustomAttributeSchema" } }, "defaultPurchasePrice": { "type": [ "number", "null" ] }, "defaultSellingPrice": { "type": [ "number", "null" ] }, "description": { "type": [ "string", "null" ] }, "digitalDelivery": { "anyOf": [ { "$ref": "#/components/schemas/digitalDeliveryKindSchema" }, { "type": "null" } ] }, "ean": { "type": [ "string", "null" ] }, "hsCode": { "type": [ "string", "null" ] }, "id": { "$ref": "#/components/schemas/productIdSchema" }, "images": { "type": "array", "items": { "$ref": "#/components/schemas/assetImageSchema" } }, "manufacturer": { "type": [ "string", "null" ] }, "manufacturerPartNumber": { "type": [ "string", "null" ] }, "model": { "type": [ "string", "null" ] }, "name": { "type": "string", "minLength": 1 }, "options": { "type": "array", "items": { "$ref": "#/components/schemas/productOptionSchema" } }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "productType": { "$ref": "#/components/schemas/productTypeSchema" }, "sku": { "type": [ "string", "null" ] }, "slug": { "$ref": "#/components/schemas/slugSchema" }, "status": { "$ref": "#/components/schemas/productStatusSchema" }, "suppliers": { "type": "array", "items": { "$ref": "#/components/schemas/productSupplierLinkSchema" } }, "tags": { "type": "array", "items": { "$ref": "#/components/schemas/organizationTagSchema" } }, "trackInventory": { "type": "boolean" }, "unitOfMeasure": { "type": [ "string", "null" ] }, "updatedAt": { "type": "string", "format": "date-time" }, "updatedBy": { "type": [ "string", "null" ], "format": "uuid" }, "variants": { "type": "array", "items": { "$ref": "#/components/schemas/productVariantSchema" } }, "vatPercentage": { "type": [ "number", "null" ] }, "weightGrams": { "type": [ "integer", "null" ] } }, "required": [ "brand", "brandId", "categories", "categoryIds", "countryOfOrigin", "createdAt", "createdBy", "currency", "customAttributes", "defaultPurchasePrice", "defaultSellingPrice", "description", "digitalDelivery", "ean", "hsCode", "id", "images", "manufacturer", "manufacturerPartNumber", "model", "name", "options", "organizationId", "productType", "sku", "slug", "status", "suppliers", "tags", "trackInventory", "unitOfMeasure", "updatedAt", "updatedBy", "variants", "vatPercentage", "weightGrams" ] }, "productStatusSchema": { "type": "string", "enum": [ "active", "discontinued", "archived" ] }, "productSupplierLinkSchema": { "type": "object", "properties": { "createdAt": { "type": "string", "format": "date-time" }, "currency": { "type": [ "string", "null" ] }, "externalId": { "type": [ "string", "null" ] }, "id": { "type": "string", "format": "uuid" }, "isActive": { "type": "boolean" }, "isPreferred": { "type": "boolean" }, "leadTimeDays": { "type": [ "integer", "null" ] }, "minOrderQuantity": { "type": [ "number", "null" ] }, "orderCode": { "type": [ "string", "null" ] }, "orderMultiple": { "type": [ "number", "null" ] }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "productId": { "$ref": "#/components/schemas/productIdSchema" }, "supplier": { "anyOf": [ { "$ref": "#/components/schemas/supplierSchema" }, { "type": "null" } ] }, "supplierId": { "$ref": "#/components/schemas/supplierIdSchema" }, "supplierSku": { "type": [ "string", "null" ] }, "unitCost": { "type": [ "number", "null" ] }, "updatedAt": { "type": "string", "format": "date-time" } }, "required": [ "createdAt", "currency", "externalId", "id", "isActive", "isPreferred", "leadTimeDays", "minOrderQuantity", "orderCode", "orderMultiple", "organizationId", "productId", "supplier", "supplierId", "supplierSku", "unitCost", "updatedAt" ] }, "productTypeSchema": { "type": "string", "enum": [ "physical", "digital" ] }, "productVariantIdSchema": { "type": "string", "format": "uuid" }, "productVariantSchema": { "type": "object", "properties": { "allowBackorder": { "type": "boolean" }, "averageCost": { "type": [ "number", "null" ] }, "ean": { "type": [ "string", "null" ] }, "id": { "$ref": "#/components/schemas/productVariantIdSchema" }, "imageId": { "type": [ "string", "null" ], "format": "uuid" }, "isDefault": { "type": "boolean" }, "manufacturerPartNumber": { "type": [ "string", "null" ] }, "optionValueIds": { "type": "array", "items": { "type": "string", "format": "uuid" } }, "position": { "type": "integer" }, "productId": { "$ref": "#/components/schemas/productIdSchema" }, "purchasePrice": { "type": [ "number", "null" ] }, "sellingPrice": { "type": [ "number", "null" ] }, "sku": { "type": [ "string", "null" ] }, "status": { "$ref": "#/components/schemas/productStatusSchema" }, "title": { "type": "string" }, "weightGrams": { "type": [ "integer", "null" ] } }, "required": [ "allowBackorder", "averageCost", "ean", "id", "imageId", "isDefault", "manufacturerPartNumber", "optionValueIds", "position", "productId", "purchasePrice", "sellingPrice", "sku", "status", "title", "weightGrams" ] }, "slugSchema": { "type": "string", "minLength": 1, "maxLength": 160, "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$" }, "supplierIdSchema": { "type": "string", "format": "uuid" }, "supplierSchema": { "type": "object", "properties": { "accountNumber": { "type": [ "string", "null" ] }, "addressCity": { "type": [ "string", "null" ] }, "addressCountry": { "type": [ "string", "null" ] }, "addressLine1": { "type": [ "string", "null" ] }, "addressLine2": { "type": [ "string", "null" ] }, "addressPostalCode": { "type": [ "string", "null" ] }, "addressState": { "type": [ "string", "null" ] }, "billingEmail": { "type": [ "string", "null" ] }, "btw": { "type": [ "string", "null" ] }, "code": { "type": [ "string", "null" ] }, "createdAt": { "type": "string", "format": "date-time" }, "currency": { "type": [ "string", "null" ] }, "defaultInventoryLocationId": { "anyOf": [ { "$ref": "#/components/schemas/inventoryLocationIdSchema" }, { "type": "null" } ] }, "defaultLeadTimeDays": { "type": [ "integer", "null" ] }, "email": { "type": [ "string", "null" ] }, "externalId": { "type": [ "string", "null" ] }, "id": { "$ref": "#/components/schemas/supplierIdSchema" }, "kvk": { "type": [ "string", "null" ] }, "name": { "type": "string", "minLength": 1, "maxLength": 240 }, "notes": { "type": [ "string", "null" ] }, "orderingEmail": { "type": [ "string", "null" ] }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "paymentTermsDays": { "type": [ "integer", "null" ] }, "phone": { "type": [ "string", "null" ] }, "source": { "type": [ "string", "null" ] }, "status": { "$ref": "#/components/schemas/supplierStatusSchema" }, "updatedAt": { "type": "string", "format": "date-time" }, "website": { "type": [ "string", "null" ] } }, "required": [ "accountNumber", "addressCity", "addressCountry", "addressLine1", "addressLine2", "addressPostalCode", "addressState", "billingEmail", "btw", "code", "createdAt", "currency", "defaultInventoryLocationId", "defaultLeadTimeDays", "email", "externalId", "id", "kvk", "name", "notes", "orderingEmail", "organizationId", "paymentTermsDays", "phone", "source", "status", "updatedAt", "website" ] }, "supplierStatusSchema": { "type": "string", "enum": [ "active", "inactive", "archived" ] }, "tagColorSchema": { "type": "string", "pattern": "^#(?:[0-9a-f]{3}|[0-9a-f]{6})$" }, "tagIdSchema": { "type": "string", "format": "uuid" }, "tagKindSchema": { "type": "string", "enum": [ "task", "customer", "product" ] }, "tagSlugSchema": { "type": "string", "minLength": 1, "maxLength": 160, "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$" } } ``` # Update a product. (/docs/openapi/updateProduct) Update a product for the specified organization. ## PATCH /v1/organizations/{organizationId}/products/{productId} ```json { "operationId": "updateProduct", "summary": "Update a product.", "description": "Update a product for the specified organization.", "tags": [ "Assets" ], "parameters": [ { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" }, { "in": "path", "name": "productId", "required": true, "schema": { "$ref": "#/components/schemas/productIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/productInputSchema" } } }, "description": "The product payload.", "required": true }, "responses": { "200": { "description": "Returns the updated product.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/productSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "assetCustomAttributeSchema": { "type": "object", "properties": { "booleanValue": { "type": [ "boolean", "null" ] }, "dateValue": { "$ref": "#/components/schemas/nullableDate" }, "numberValue": { "type": [ "number", "null" ] }, "textValue": { "type": [ "string", "null" ] }, "urlValue": { "type": [ "string", "null" ] }, "valueType": { "$ref": "#/components/schemas/customAttributeValueTypeSchema" }, "createdAt": { "type": "string", "format": "date-time" }, "customerAssetId": { "anyOf": [ { "$ref": "#/components/schemas/customerAssetIdSchema" }, { "type": "null" } ] }, "id": { "type": "string", "format": "uuid" }, "key": { "type": "string", "minLength": 1, "maxLength": 120 }, "label": { "type": "string", "minLength": 1, "maxLength": 160 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "position": { "type": "integer", "minimum": 0 }, "productId": { "anyOf": [ { "$ref": "#/components/schemas/productIdSchema" }, { "type": "null" } ] }, "updatedAt": { "type": "string", "format": "date-time" } }, "required": [ "booleanValue", "dateValue", "numberValue", "textValue", "urlValue", "valueType", "createdAt", "customerAssetId", "id", "key", "label", "organizationId", "position", "productId", "updatedAt" ] }, "assetImageIdSchema": { "type": "string", "format": "uuid" }, "assetImageSchema": { "type": "object", "properties": { "altText": { "type": [ "string", "null" ] }, "createdAt": { "type": "string", "format": "date-time" }, "id": { "$ref": "#/components/schemas/assetImageIdSchema" }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "position": { "type": "integer", "minimum": 0 }, "productId": { "anyOf": [ { "$ref": "#/components/schemas/productIdSchema" }, { "type": "null" } ] }, "updatedAt": { "type": "string", "format": "date-time" }, "url": { "type": "string", "format": "uri" } }, "required": [ "altText", "createdAt", "id", "organizationId", "position", "productId", "updatedAt", "url" ] }, "brandIdSchema": { "type": "string", "enum": [ "centrakit", "mechup", "stabledock", "caresly", "propdock", "marryduck", "signaldock", "spacelio", "legaldock" ] }, "currencyCodeSchema": { "type": "string", "pattern": "^[A-Z]{3}$" }, "customAttributeValueTypeSchema": { "type": "string", "enum": [ "text", "number", "boolean", "date", "url" ] }, "customerAssetIdSchema": { "type": "string", "format": "uuid" }, "digitalDeliveryKindSchema": { "type": "string", "enum": [ "file", "license_key", "link", "manual" ] }, "iconNameSchema": { "type": "string", "minLength": 1, "maxLength": 120, "pattern": "^[A-Za-z0-9]+Icon$" }, "inventoryLocationIdSchema": { "type": "string", "format": "uuid" }, "nullableCurrency": { "anyOf": [ { "$ref": "#/components/schemas/currencyCodeSchema" }, { "type": "null" } ] }, "nullableDate": { "type": [ "string", "null" ], "format": "date" }, "nullableLongText": { "type": [ "string", "null" ], "maxLength": 4000 }, "nullableMoney": { "type": [ "number", "null" ], "minimum": 0 }, "nullableShortText": { "type": [ "string", "null" ], "maxLength": 240 }, "organizationIdSchema": { "type": "string", "format": "uuid" }, "organizationTagSchema": { "type": "object", "properties": { "color": { "anyOf": [ { "$ref": "#/components/schemas/tagColorSchema" }, { "type": "null" } ] }, "createdAt": { "type": "string", "format": "date-time" }, "description": { "type": [ "string", "null" ] }, "id": { "$ref": "#/components/schemas/tagIdSchema" }, "kind": { "$ref": "#/components/schemas/tagKindSchema" }, "name": { "type": "string", "minLength": 1, "maxLength": 80 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "position": { "type": "integer", "minimum": 0 }, "slug": { "$ref": "#/components/schemas/tagSlugSchema" }, "updatedAt": { "type": "string", "format": "date-time" } }, "required": [ "color", "createdAt", "description", "id", "kind", "name", "organizationId", "position", "slug", "updatedAt" ] }, "productCategorySchema": { "type": "object", "properties": { "createdAt": { "type": "string", "format": "date-time" }, "description": { "type": [ "string", "null" ] }, "icon": { "$ref": "#/components/schemas/iconNameSchema" }, "id": { "type": "string", "format": "uuid" }, "name": { "type": "string", "minLength": 1, "maxLength": 120 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "parentId": { "type": [ "string", "null" ], "format": "uuid" }, "position": { "type": "integer", "minimum": 0 }, "slug": { "$ref": "#/components/schemas/slugSchema" }, "updatedAt": { "type": "string", "format": "date-time" } }, "required": [ "createdAt", "description", "icon", "id", "name", "organizationId", "parentId", "position", "slug", "updatedAt" ] }, "productIdSchema": { "type": "string", "format": "uuid" }, "productInputSchema": { "type": "object", "properties": { "brandId": { "anyOf": [ { "$ref": "#/components/schemas/brandIdSchema" }, { "type": "null" } ] }, "categoryIds": { "type": "array", "items": { "type": "string", "format": "uuid" } }, "countryOfOrigin": { "type": [ "string", "null" ], "pattern": "^[A-Z]{2}$" }, "currency": { "allOf": [ { "$ref": "#/components/schemas/nullableCurrency" } ] }, "defaultPurchasePrice": { "allOf": [ { "$ref": "#/components/schemas/nullableMoney" } ] }, "defaultSellingPrice": { "allOf": [ { "$ref": "#/components/schemas/nullableMoney" } ] }, "description": { "allOf": [ { "$ref": "#/components/schemas/nullableLongText" } ] }, "digitalDelivery": { "anyOf": [ { "$ref": "#/components/schemas/digitalDeliveryKindSchema" }, { "type": "null" } ] }, "ean": { "type": [ "string", "null" ], "maxLength": 120 }, "hsCode": { "type": [ "string", "null" ], "pattern": "^[0-9]{6,10}$" }, "manufacturer": { "allOf": [ { "$ref": "#/components/schemas/nullableShortText" } ] }, "manufacturerPartNumber": { "allOf": [ { "$ref": "#/components/schemas/nullableShortText" } ] }, "model": { "allOf": [ { "$ref": "#/components/schemas/nullableShortText" } ] }, "name": { "type": "string", "minLength": 1, "maxLength": 240 }, "productType": { "allOf": [ { "$ref": "#/components/schemas/productTypeSchema" } ] }, "sku": { "type": [ "string", "null" ], "maxLength": 120 }, "slug": { "allOf": [ { "$ref": "#/components/schemas/slugInputSchema" } ] }, "status": { "allOf": [ { "$ref": "#/components/schemas/productStatusSchema" } ] }, "tagIds": { "type": "array", "items": { "type": "string", "format": "uuid" }, "maxItems": 100 }, "trackInventory": { "type": "boolean" }, "unitOfMeasure": { "allOf": [ { "$ref": "#/components/schemas/unitOfMeasureInputSchema" } ] }, "vatPercentage": { "type": [ "number", "null" ], "minimum": 0, "maximum": 100 }, "weightGrams": { "type": [ "integer", "null" ], "minimum": 0 } }, "required": [ "name" ] }, "productOptionSchema": { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "name": { "type": "string" }, "position": { "type": "integer" }, "values": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "position": { "type": "integer" }, "value": { "type": "string" } }, "required": [ "id", "position", "value" ] } } }, "required": [ "id", "name", "position", "values" ] }, "productSchema": { "type": "object", "properties": { "brand": { "type": [ "object", "null" ], "properties": { "createdAt": { "type": "string", "format": "date-time" }, "description": { "type": [ "string", "null" ] }, "id": { "$ref": "#/components/schemas/brandIdSchema" }, "logoUrl": { "type": [ "string", "null" ], "format": "uri" }, "name": { "type": "string", "minLength": 1, "maxLength": 240 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "slug": { "type": "string", "minLength": 1, "maxLength": 160, "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$" }, "updatedAt": { "type": "string", "format": "date-time" }, "website": { "type": [ "string", "null" ] } }, "required": [ "createdAt", "description", "id", "logoUrl", "name", "organizationId", "slug", "updatedAt", "website" ] }, "brandId": { "anyOf": [ { "$ref": "#/components/schemas/brandIdSchema" }, { "type": "null" } ] }, "categories": { "type": "array", "items": { "$ref": "#/components/schemas/productCategorySchema" } }, "categoryIds": { "type": "array", "items": { "type": "string", "format": "uuid" } }, "countryOfOrigin": { "type": [ "string", "null" ] }, "createdAt": { "type": "string", "format": "date-time" }, "createdBy": { "type": [ "string", "null" ], "format": "uuid" }, "currency": { "type": [ "string", "null" ] }, "customAttributes": { "type": "array", "items": { "$ref": "#/components/schemas/assetCustomAttributeSchema" } }, "defaultPurchasePrice": { "type": [ "number", "null" ] }, "defaultSellingPrice": { "type": [ "number", "null" ] }, "description": { "type": [ "string", "null" ] }, "digitalDelivery": { "anyOf": [ { "$ref": "#/components/schemas/digitalDeliveryKindSchema" }, { "type": "null" } ] }, "ean": { "type": [ "string", "null" ] }, "hsCode": { "type": [ "string", "null" ] }, "id": { "$ref": "#/components/schemas/productIdSchema" }, "images": { "type": "array", "items": { "$ref": "#/components/schemas/assetImageSchema" } }, "manufacturer": { "type": [ "string", "null" ] }, "manufacturerPartNumber": { "type": [ "string", "null" ] }, "model": { "type": [ "string", "null" ] }, "name": { "type": "string", "minLength": 1 }, "options": { "type": "array", "items": { "$ref": "#/components/schemas/productOptionSchema" } }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "productType": { "$ref": "#/components/schemas/productTypeSchema" }, "sku": { "type": [ "string", "null" ] }, "slug": { "$ref": "#/components/schemas/slugSchema" }, "status": { "$ref": "#/components/schemas/productStatusSchema" }, "suppliers": { "type": "array", "items": { "$ref": "#/components/schemas/productSupplierLinkSchema" } }, "tags": { "type": "array", "items": { "$ref": "#/components/schemas/organizationTagSchema" } }, "trackInventory": { "type": "boolean" }, "unitOfMeasure": { "type": [ "string", "null" ] }, "updatedAt": { "type": "string", "format": "date-time" }, "updatedBy": { "type": [ "string", "null" ], "format": "uuid" }, "variants": { "type": "array", "items": { "$ref": "#/components/schemas/productVariantSchema" } }, "vatPercentage": { "type": [ "number", "null" ] }, "weightGrams": { "type": [ "integer", "null" ] } }, "required": [ "brand", "brandId", "categories", "categoryIds", "countryOfOrigin", "createdAt", "createdBy", "currency", "customAttributes", "defaultPurchasePrice", "defaultSellingPrice", "description", "digitalDelivery", "ean", "hsCode", "id", "images", "manufacturer", "manufacturerPartNumber", "model", "name", "options", "organizationId", "productType", "sku", "slug", "status", "suppliers", "tags", "trackInventory", "unitOfMeasure", "updatedAt", "updatedBy", "variants", "vatPercentage", "weightGrams" ] }, "productStatusSchema": { "type": "string", "enum": [ "active", "discontinued", "archived" ] }, "productSupplierLinkSchema": { "type": "object", "properties": { "createdAt": { "type": "string", "format": "date-time" }, "currency": { "type": [ "string", "null" ] }, "externalId": { "type": [ "string", "null" ] }, "id": { "type": "string", "format": "uuid" }, "isActive": { "type": "boolean" }, "isPreferred": { "type": "boolean" }, "leadTimeDays": { "type": [ "integer", "null" ] }, "minOrderQuantity": { "type": [ "number", "null" ] }, "orderCode": { "type": [ "string", "null" ] }, "orderMultiple": { "type": [ "number", "null" ] }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "productId": { "$ref": "#/components/schemas/productIdSchema" }, "supplier": { "anyOf": [ { "$ref": "#/components/schemas/supplierSchema" }, { "type": "null" } ] }, "supplierId": { "$ref": "#/components/schemas/supplierIdSchema" }, "supplierSku": { "type": [ "string", "null" ] }, "unitCost": { "type": [ "number", "null" ] }, "updatedAt": { "type": "string", "format": "date-time" } }, "required": [ "createdAt", "currency", "externalId", "id", "isActive", "isPreferred", "leadTimeDays", "minOrderQuantity", "orderCode", "orderMultiple", "organizationId", "productId", "supplier", "supplierId", "supplierSku", "unitCost", "updatedAt" ] }, "productTypeSchema": { "type": "string", "enum": [ "physical", "digital" ] }, "productVariantIdSchema": { "type": "string", "format": "uuid" }, "productVariantSchema": { "type": "object", "properties": { "allowBackorder": { "type": "boolean" }, "averageCost": { "type": [ "number", "null" ] }, "ean": { "type": [ "string", "null" ] }, "id": { "$ref": "#/components/schemas/productVariantIdSchema" }, "imageId": { "type": [ "string", "null" ], "format": "uuid" }, "isDefault": { "type": "boolean" }, "manufacturerPartNumber": { "type": [ "string", "null" ] }, "optionValueIds": { "type": "array", "items": { "type": "string", "format": "uuid" } }, "position": { "type": "integer" }, "productId": { "$ref": "#/components/schemas/productIdSchema" }, "purchasePrice": { "type": [ "number", "null" ] }, "sellingPrice": { "type": [ "number", "null" ] }, "sku": { "type": [ "string", "null" ] }, "status": { "$ref": "#/components/schemas/productStatusSchema" }, "title": { "type": "string" }, "weightGrams": { "type": [ "integer", "null" ] } }, "required": [ "allowBackorder", "averageCost", "ean", "id", "imageId", "isDefault", "manufacturerPartNumber", "optionValueIds", "position", "productId", "purchasePrice", "sellingPrice", "sku", "status", "title", "weightGrams" ] }, "slugInputSchema": { "type": "string", "minLength": 1, "maxLength": 160, "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$" }, "slugSchema": { "type": "string", "minLength": 1, "maxLength": 160, "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$" }, "supplierIdSchema": { "type": "string", "format": "uuid" }, "supplierSchema": { "type": "object", "properties": { "accountNumber": { "type": [ "string", "null" ] }, "addressCity": { "type": [ "string", "null" ] }, "addressCountry": { "type": [ "string", "null" ] }, "addressLine1": { "type": [ "string", "null" ] }, "addressLine2": { "type": [ "string", "null" ] }, "addressPostalCode": { "type": [ "string", "null" ] }, "addressState": { "type": [ "string", "null" ] }, "billingEmail": { "type": [ "string", "null" ] }, "btw": { "type": [ "string", "null" ] }, "code": { "type": [ "string", "null" ] }, "createdAt": { "type": "string", "format": "date-time" }, "currency": { "type": [ "string", "null" ] }, "defaultInventoryLocationId": { "anyOf": [ { "$ref": "#/components/schemas/inventoryLocationIdSchema" }, { "type": "null" } ] }, "defaultLeadTimeDays": { "type": [ "integer", "null" ] }, "email": { "type": [ "string", "null" ] }, "externalId": { "type": [ "string", "null" ] }, "id": { "$ref": "#/components/schemas/supplierIdSchema" }, "kvk": { "type": [ "string", "null" ] }, "name": { "type": "string", "minLength": 1, "maxLength": 240 }, "notes": { "type": [ "string", "null" ] }, "orderingEmail": { "type": [ "string", "null" ] }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "paymentTermsDays": { "type": [ "integer", "null" ] }, "phone": { "type": [ "string", "null" ] }, "source": { "type": [ "string", "null" ] }, "status": { "$ref": "#/components/schemas/supplierStatusSchema" }, "updatedAt": { "type": "string", "format": "date-time" }, "website": { "type": [ "string", "null" ] } }, "required": [ "accountNumber", "addressCity", "addressCountry", "addressLine1", "addressLine2", "addressPostalCode", "addressState", "billingEmail", "btw", "code", "createdAt", "currency", "defaultInventoryLocationId", "defaultLeadTimeDays", "email", "externalId", "id", "kvk", "name", "notes", "orderingEmail", "organizationId", "paymentTermsDays", "phone", "source", "status", "updatedAt", "website" ] }, "supplierStatusSchema": { "type": "string", "enum": [ "active", "inactive", "archived" ] }, "tagColorSchema": { "type": "string", "pattern": "^#(?:[0-9a-f]{3}|[0-9a-f]{6})$" }, "tagIdSchema": { "type": "string", "format": "uuid" }, "tagKindSchema": { "type": "string", "enum": [ "task", "customer", "product" ] }, "tagSlugSchema": { "type": "string", "minLength": 1, "maxLength": 160, "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$" }, "unitOfMeasureInputSchema": { "type": [ "string", "null" ], "maxLength": 240 } } ``` # Delete a product. (/docs/openapi/deleteProduct) Delete a product for the specified organization. ## DELETE /v1/organizations/{organizationId}/products/{productId} ```json { "operationId": "deleteProduct", "summary": "Delete a product.", "description": "Delete a product for the specified organization.", "tags": [ "Assets" ], "parameters": [ { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" }, { "in": "path", "name": "productId", "required": true, "schema": { "$ref": "#/components/schemas/productIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "responses": { "200": { "description": "Returns a success payload when the product is deleted.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/publicApiSuccessResponseSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "organizationIdSchema": { "type": "string", "format": "uuid" }, "productIdSchema": { "type": "string", "format": "uuid" }, "publicApiSuccessResponseSchema": { "type": "object", "properties": { "success": { "type": "boolean", "enum": [ true ] } }, "required": [ "success" ] } } ``` # Get a supplier. (/docs/openapi/getSupplier) Return a supplier for the specified organization. ## GET /v1/organizations/{organizationId}/suppliers/{supplierId} ```json { "operationId": "getSupplier", "summary": "Get a supplier.", "description": "Return a supplier for the specified organization.", "tags": [ "Assets" ], "parameters": [ { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" }, { "in": "path", "name": "supplierId", "required": true, "schema": { "$ref": "#/components/schemas/supplierIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "responses": { "200": { "description": "Returns the requested supplier.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/supplierSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "inventoryLocationIdSchema": { "type": "string", "format": "uuid" }, "organizationIdSchema": { "type": "string", "format": "uuid" }, "supplierIdSchema": { "type": "string", "format": "uuid" }, "supplierSchema": { "type": "object", "properties": { "accountNumber": { "type": [ "string", "null" ] }, "addressCity": { "type": [ "string", "null" ] }, "addressCountry": { "type": [ "string", "null" ] }, "addressLine1": { "type": [ "string", "null" ] }, "addressLine2": { "type": [ "string", "null" ] }, "addressPostalCode": { "type": [ "string", "null" ] }, "addressState": { "type": [ "string", "null" ] }, "billingEmail": { "type": [ "string", "null" ] }, "btw": { "type": [ "string", "null" ] }, "code": { "type": [ "string", "null" ] }, "createdAt": { "type": "string", "format": "date-time" }, "currency": { "type": [ "string", "null" ] }, "defaultInventoryLocationId": { "anyOf": [ { "$ref": "#/components/schemas/inventoryLocationIdSchema" }, { "type": "null" } ] }, "defaultLeadTimeDays": { "type": [ "integer", "null" ] }, "email": { "type": [ "string", "null" ] }, "externalId": { "type": [ "string", "null" ] }, "id": { "$ref": "#/components/schemas/supplierIdSchema" }, "kvk": { "type": [ "string", "null" ] }, "name": { "type": "string", "minLength": 1, "maxLength": 240 }, "notes": { "type": [ "string", "null" ] }, "orderingEmail": { "type": [ "string", "null" ] }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "paymentTermsDays": { "type": [ "integer", "null" ] }, "phone": { "type": [ "string", "null" ] }, "source": { "type": [ "string", "null" ] }, "status": { "$ref": "#/components/schemas/supplierStatusSchema" }, "updatedAt": { "type": "string", "format": "date-time" }, "website": { "type": [ "string", "null" ] } }, "required": [ "accountNumber", "addressCity", "addressCountry", "addressLine1", "addressLine2", "addressPostalCode", "addressState", "billingEmail", "btw", "code", "createdAt", "currency", "defaultInventoryLocationId", "defaultLeadTimeDays", "email", "externalId", "id", "kvk", "name", "notes", "orderingEmail", "organizationId", "paymentTermsDays", "phone", "source", "status", "updatedAt", "website" ] }, "supplierStatusSchema": { "type": "string", "enum": [ "active", "inactive", "archived" ] } } ``` # Update a supplier. (/docs/openapi/updateSupplier) Update a supplier for the specified organization. ## PATCH /v1/organizations/{organizationId}/suppliers/{supplierId} ```json { "operationId": "updateSupplier", "summary": "Update a supplier.", "description": "Update a supplier for the specified organization.", "tags": [ "Assets" ], "parameters": [ { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" }, { "in": "path", "name": "supplierId", "required": true, "schema": { "$ref": "#/components/schemas/supplierIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/supplierInputSchema" } } }, "description": "The supplier payload." }, "responses": { "200": { "description": "Returns the updated supplier.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/supplierSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "inventoryLocationIdSchema": { "type": "string", "format": "uuid" }, "nullableEmailInputSchema": { "type": [ "string", "null" ], "format": "email", "maxLength": 320 }, "nullableLongText": { "type": [ "string", "null" ], "maxLength": 4000 }, "nullableNonNegativeInt": { "type": [ "integer", "null" ], "minimum": 0 }, "nullablePhoneInputSchema": { "type": [ "string", "null" ], "maxLength": 20 }, "nullableShortText": { "type": [ "string", "null" ], "maxLength": 240 }, "nullableUrlInputSchema": { "anyOf": [ { "type": "string", "maxLength": 400, "format": "uri" }, { "type": "null" } ] }, "organizationIdSchema": { "type": "string", "format": "uuid" }, "supplierIdSchema": { "type": "string", "format": "uuid" }, "supplierInputSchema": { "type": "object", "properties": { "accountNumber": { "allOf": [ { "$ref": "#/components/schemas/nullableShortText" } ] }, "addressCity": { "allOf": [ { "$ref": "#/components/schemas/nullableShortText" } ] }, "addressCountry": { "allOf": [ { "$ref": "#/components/schemas/nullableShortText" } ] }, "addressLine1": { "allOf": [ { "$ref": "#/components/schemas/nullableShortText" } ] }, "addressLine2": { "allOf": [ { "$ref": "#/components/schemas/nullableShortText" } ] }, "addressPostalCode": { "allOf": [ { "$ref": "#/components/schemas/nullableShortText" } ] }, "addressState": { "allOf": [ { "$ref": "#/components/schemas/nullableShortText" } ] }, "billingEmail": { "allOf": [ { "$ref": "#/components/schemas/nullableEmailInputSchema" } ] }, "btw": { "type": [ "string", "null" ], "maxLength": 40 }, "code": { "allOf": [ { "$ref": "#/components/schemas/nullableShortText" } ] }, "currency": { "type": [ "string", "null" ], "pattern": "^[A-Z]{3}$" }, "defaultInventoryLocationId": { "anyOf": [ { "$ref": "#/components/schemas/inventoryLocationIdSchema" }, { "type": "null" } ] }, "defaultLeadTimeDays": { "allOf": [ { "$ref": "#/components/schemas/nullableNonNegativeInt" } ] }, "email": { "allOf": [ { "$ref": "#/components/schemas/nullableEmailInputSchema" } ] }, "externalId": { "allOf": [ { "$ref": "#/components/schemas/nullableShortText" } ] }, "kvk": { "type": [ "string", "null" ], "maxLength": 40 }, "name": { "type": "string", "minLength": 1, "maxLength": 240 }, "notes": { "allOf": [ { "$ref": "#/components/schemas/nullableLongText" } ] }, "orderingEmail": { "allOf": [ { "$ref": "#/components/schemas/nullableEmailInputSchema" } ] }, "paymentTermsDays": { "allOf": [ { "$ref": "#/components/schemas/nullableNonNegativeInt" } ] }, "phone": { "allOf": [ { "$ref": "#/components/schemas/nullablePhoneInputSchema" } ] }, "source": { "allOf": [ { "$ref": "#/components/schemas/nullableShortText" } ] }, "status": { "allOf": [ { "$ref": "#/components/schemas/supplierStatusSchema" } ] }, "website": { "allOf": [ { "$ref": "#/components/schemas/nullableUrlInputSchema" } ] } }, "required": [ "name" ] }, "supplierSchema": { "type": "object", "properties": { "accountNumber": { "type": [ "string", "null" ] }, "addressCity": { "type": [ "string", "null" ] }, "addressCountry": { "type": [ "string", "null" ] }, "addressLine1": { "type": [ "string", "null" ] }, "addressLine2": { "type": [ "string", "null" ] }, "addressPostalCode": { "type": [ "string", "null" ] }, "addressState": { "type": [ "string", "null" ] }, "billingEmail": { "type": [ "string", "null" ] }, "btw": { "type": [ "string", "null" ] }, "code": { "type": [ "string", "null" ] }, "createdAt": { "type": "string", "format": "date-time" }, "currency": { "type": [ "string", "null" ] }, "defaultInventoryLocationId": { "anyOf": [ { "$ref": "#/components/schemas/inventoryLocationIdSchema" }, { "type": "null" } ] }, "defaultLeadTimeDays": { "type": [ "integer", "null" ] }, "email": { "type": [ "string", "null" ] }, "externalId": { "type": [ "string", "null" ] }, "id": { "$ref": "#/components/schemas/supplierIdSchema" }, "kvk": { "type": [ "string", "null" ] }, "name": { "type": "string", "minLength": 1, "maxLength": 240 }, "notes": { "type": [ "string", "null" ] }, "orderingEmail": { "type": [ "string", "null" ] }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "paymentTermsDays": { "type": [ "integer", "null" ] }, "phone": { "type": [ "string", "null" ] }, "source": { "type": [ "string", "null" ] }, "status": { "$ref": "#/components/schemas/supplierStatusSchema" }, "updatedAt": { "type": "string", "format": "date-time" }, "website": { "type": [ "string", "null" ] } }, "required": [ "accountNumber", "addressCity", "addressCountry", "addressLine1", "addressLine2", "addressPostalCode", "addressState", "billingEmail", "btw", "code", "createdAt", "currency", "defaultInventoryLocationId", "defaultLeadTimeDays", "email", "externalId", "id", "kvk", "name", "notes", "orderingEmail", "organizationId", "paymentTermsDays", "phone", "source", "status", "updatedAt", "website" ] }, "supplierStatusSchema": { "type": "string", "enum": [ "active", "inactive", "archived" ] } } ``` # Delete a supplier. (/docs/openapi/deleteSupplier) Delete a supplier for the specified organization. ## DELETE /v1/organizations/{organizationId}/suppliers/{supplierId} ```json { "operationId": "deleteSupplier", "summary": "Delete a supplier.", "description": "Delete a supplier for the specified organization.", "tags": [ "Assets" ], "parameters": [ { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" }, { "in": "path", "name": "supplierId", "required": true, "schema": { "$ref": "#/components/schemas/supplierIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "responses": { "200": { "description": "Returns a success payload when the supplier is deleted.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/publicApiSuccessResponseSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "organizationIdSchema": { "type": "string", "format": "uuid" }, "publicApiSuccessResponseSchema": { "type": "object", "properties": { "success": { "type": "boolean", "enum": [ true ] } }, "required": [ "success" ] }, "supplierIdSchema": { "type": "string", "format": "uuid" } } ``` # List customer asset notes. (/docs/openapi/listCustomerAssetNotes) Return internal notes for a customer asset. ## GET /v1/organizations/{organizationId}/customer-assets/{customerAssetId}/notes ```json { "operationId": "listCustomerAssetNotes", "summary": "List customer asset notes.", "description": "Return internal notes for a customer asset.", "tags": [ "Assets" ], "parameters": [ { "in": "path", "name": "customerAssetId", "required": true, "schema": { "type": "string", "format": "uuid" }, "example": "123e4567-e89b-12d3-a456-426614174000" }, { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "responses": { "200": { "description": "Returns customer asset notes.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/customerAssetNotesResponseSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "collaborationBlockSchema": { "type": "object", "discriminator": { "propertyName": "type" }, "oneOf": [ { "type": "object", "properties": { "content": { "$ref": "#/components/schemas/collaborationInlineContentSchema" }, "type": { "type": "string", "enum": [ "paragraph" ] } }, "required": [ "content", "type" ], "additionalProperties": false }, { "type": "object", "properties": { "items": { "type": "array", "items": { "$ref": "#/components/schemas/collaborationInlineContentSchema" }, "minItems": 1, "maxItems": 500 }, "type": { "type": "string", "enum": [ "bullet-list" ] } }, "required": [ "items", "type" ], "additionalProperties": false }, { "type": "object", "properties": { "items": { "type": "array", "items": { "$ref": "#/components/schemas/collaborationInlineContentSchema" }, "minItems": 1, "maxItems": 500 }, "type": { "type": "string", "enum": [ "numbered-list" ] } }, "required": [ "items", "type" ], "additionalProperties": false } ] }, "collaborationCommentAuthorSchema": { "type": "object", "properties": { "avatarUrl": { "type": [ "string", "null" ], "format": "uri" }, "displayName": { "type": "string", "minLength": 1 }, "userId": { "type": "string", "format": "uuid" } }, "required": [ "avatarUrl", "displayName", "userId" ] }, "collaborationDocumentSchema": { "type": "object", "properties": { "blocks": { "type": "array", "items": { "$ref": "#/components/schemas/collaborationBlockSchema" }, "minItems": 1, "maxItems": 1000 }, "version": { "type": "integer", "enum": [ 1 ] } }, "required": [ "blocks", "version" ], "additionalProperties": false }, "collaborationInlineContentSchema": { "type": "array", "items": { "$ref": "#/components/schemas/collaborationInlineSchema" }, "minItems": 1, "maxItems": 1000 }, "collaborationInlineSchema": { "type": "object", "discriminator": { "propertyName": "type" }, "oneOf": [ { "type": "object", "properties": { "text": { "type": "string", "minLength": 1, "maxLength": 20000 }, "type": { "type": "string", "enum": [ "text" ] } }, "required": [ "text", "type" ], "additionalProperties": false }, { "type": "object", "properties": { "reference": { "$ref": "#/components/schemas/collaborationReferenceSchema" }, "type": { "type": "string", "enum": [ "reference" ] } }, "required": [ "reference", "type" ], "additionalProperties": false } ] }, "collaborationReferenceSchema": { "type": "object", "discriminator": { "propertyName": "kind" }, "oneOf": [ { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "kind": { "type": "string", "enum": [ "user" ] }, "label": { "type": "string", "minLength": 1, "maxLength": 500 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "path": { "type": "string", "pattern": "^/", "maxLength": 2000 } }, "required": [ "id", "kind", "label", "organizationId", "path" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "kind": { "type": "string", "enum": [ "team" ] }, "label": { "type": "string", "minLength": 1, "maxLength": 500 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "path": { "type": "string", "pattern": "^/", "maxLength": 2000 } }, "required": [ "id", "kind", "label", "organizationId", "path" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "kind": { "type": "string", "enum": [ "task" ] }, "label": { "type": "string", "minLength": 1, "maxLength": 500 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "path": { "type": "string", "pattern": "^/", "maxLength": 2000 } }, "required": [ "id", "kind", "label", "organizationId", "path" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "kind": { "type": "string", "enum": [ "customer" ] }, "label": { "type": "string", "minLength": 1, "maxLength": 500 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "path": { "type": "string", "pattern": "^/", "maxLength": 2000 } }, "required": [ "id", "kind", "label", "organizationId", "path" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "$ref": "#/components/schemas/quoteIdSchema" }, "kind": { "type": "string", "enum": [ "quote" ] }, "label": { "type": "string", "minLength": 1, "maxLength": 500 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "path": { "type": "string", "pattern": "^/", "maxLength": 2000 } }, "required": [ "id", "kind", "label", "organizationId", "path" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "$ref": "#/components/schemas/invoiceIdSchema" }, "kind": { "type": "string", "enum": [ "invoice" ] }, "label": { "type": "string", "minLength": 1, "maxLength": 500 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "path": { "type": "string", "pattern": "^/", "maxLength": 2000 } }, "required": [ "id", "kind", "label", "organizationId", "path" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "$ref": "#/components/schemas/productIdSchema" }, "kind": { "type": "string", "enum": [ "product" ] }, "label": { "type": "string", "minLength": 1, "maxLength": 500 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "path": { "type": "string", "pattern": "^/", "maxLength": 2000 } }, "required": [ "id", "kind", "label", "organizationId", "path" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "kind": { "type": "string", "enum": [ "customer-asset" ] }, "label": { "type": "string", "minLength": 1, "maxLength": 500 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "path": { "type": "string", "pattern": "^/", "maxLength": 2000 } }, "required": [ "id", "kind", "label", "organizationId", "path" ], "additionalProperties": false } ] }, "customerAssetIdSchema": { "type": "string", "format": "uuid" }, "customerAssetNoteSchema": { "type": "object", "properties": { "author": { "anyOf": [ { "$ref": "#/components/schemas/collaborationCommentAuthorSchema" }, { "type": "null" } ] }, "authorId": { "type": [ "string", "null" ], "format": "uuid" }, "contentDoc": { "$ref": "#/components/schemas/collaborationDocumentSchema" }, "contentText": { "type": "string", "minLength": 1, "maxLength": 20000 }, "createdAt": { "type": "string", "format": "date-time" }, "customerAssetId": { "$ref": "#/components/schemas/customerAssetIdSchema" }, "id": { "type": "string", "format": "uuid" }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "updatedAt": { "type": "string", "format": "date-time" } }, "required": [ "author", "authorId", "contentDoc", "contentText", "createdAt", "customerAssetId", "id", "organizationId", "updatedAt" ] }, "customerAssetNotesResponseSchema": { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/components/schemas/customerAssetNoteSchema" } }, "pagination": { "$ref": "#/components/schemas/listPaginationSchema" } }, "required": [ "data", "pagination" ] }, "invoiceIdSchema": { "type": "string", "format": "uuid" }, "listPaginationSchema": { "type": "object", "properties": { "hasMore": { "type": "boolean" }, "limit": { "type": "integer", "minimum": 1, "maximum": 100 }, "offset": { "type": "integer", "minimum": 0 }, "total": { "type": "integer", "minimum": 0 }, "totalIsEstimate": { "type": "boolean" } }, "required": [ "hasMore", "limit", "offset", "total" ] }, "organizationIdSchema": { "type": "string", "format": "uuid" }, "productIdSchema": { "type": "string", "format": "uuid" }, "quoteIdSchema": { "type": "string", "format": "uuid" } } ``` # Add a customer asset note. (/docs/openapi/createCustomerAssetNote) Add an internal note to a customer asset. ## POST /v1/organizations/{organizationId}/customer-assets/{customerAssetId}/notes ```json { "operationId": "createCustomerAssetNote", "summary": "Add a customer asset note.", "description": "Add an internal note to a customer asset.", "tags": [ "Assets" ], "parameters": [ { "in": "path", "name": "customerAssetId", "required": true, "schema": { "type": "string", "format": "uuid" }, "example": "123e4567-e89b-12d3-a456-426614174000" }, { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/customerAssetNoteInputSchema" } } }, "description": "The customer asset note payload." }, "responses": { "201": { "description": "Returns the created customer asset note.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/customerAssetNoteSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "collaborationBlockSchema": { "type": "object", "discriminator": { "propertyName": "type" }, "oneOf": [ { "type": "object", "properties": { "content": { "$ref": "#/components/schemas/collaborationInlineContentSchema" }, "type": { "type": "string", "enum": [ "paragraph" ] } }, "required": [ "content", "type" ], "additionalProperties": false }, { "type": "object", "properties": { "items": { "type": "array", "items": { "$ref": "#/components/schemas/collaborationInlineContentSchema" }, "minItems": 1, "maxItems": 500 }, "type": { "type": "string", "enum": [ "bullet-list" ] } }, "required": [ "items", "type" ], "additionalProperties": false }, { "type": "object", "properties": { "items": { "type": "array", "items": { "$ref": "#/components/schemas/collaborationInlineContentSchema" }, "minItems": 1, "maxItems": 500 }, "type": { "type": "string", "enum": [ "numbered-list" ] } }, "required": [ "items", "type" ], "additionalProperties": false } ] }, "collaborationCommentAuthorSchema": { "type": "object", "properties": { "avatarUrl": { "type": [ "string", "null" ], "format": "uri" }, "displayName": { "type": "string", "minLength": 1 }, "userId": { "type": "string", "format": "uuid" } }, "required": [ "avatarUrl", "displayName", "userId" ] }, "collaborationComposerInputSchema": { "type": "object", "properties": { "selections": { "type": "array", "items": { "$ref": "#/components/schemas/collaborationComposerSelectionSchema" }, "maxItems": 100 }, "text": { "type": "string", "minLength": 1, "maxLength": 20000 } }, "required": [ "selections", "text" ], "additionalProperties": false }, "collaborationComposerSelectionSchema": { "type": "object", "properties": { "end": { "type": "integer", "exclusiveMinimum": 0, "maximum": 20000 }, "reference": { "$ref": "#/components/schemas/collaborationReferenceIdSchema" }, "start": { "type": "integer", "minimum": 0, "maximum": 20000 } }, "required": [ "end", "reference", "start" ], "additionalProperties": false }, "collaborationDocumentSchema": { "type": "object", "properties": { "blocks": { "type": "array", "items": { "$ref": "#/components/schemas/collaborationBlockSchema" }, "minItems": 1, "maxItems": 1000 }, "version": { "type": "integer", "enum": [ 1 ] } }, "required": [ "blocks", "version" ], "additionalProperties": false }, "collaborationInlineContentSchema": { "type": "array", "items": { "$ref": "#/components/schemas/collaborationInlineSchema" }, "minItems": 1, "maxItems": 1000 }, "collaborationInlineSchema": { "type": "object", "discriminator": { "propertyName": "type" }, "oneOf": [ { "type": "object", "properties": { "text": { "type": "string", "minLength": 1, "maxLength": 20000 }, "type": { "type": "string", "enum": [ "text" ] } }, "required": [ "text", "type" ], "additionalProperties": false }, { "type": "object", "properties": { "reference": { "$ref": "#/components/schemas/collaborationReferenceSchema" }, "type": { "type": "string", "enum": [ "reference" ] } }, "required": [ "reference", "type" ], "additionalProperties": false } ] }, "collaborationReferenceIdSchema": { "type": "object", "discriminator": { "propertyName": "kind" }, "oneOf": [ { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "kind": { "type": "string", "enum": [ "user" ] } }, "required": [ "id", "kind" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "kind": { "type": "string", "enum": [ "team" ] } }, "required": [ "id", "kind" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "kind": { "type": "string", "enum": [ "task" ] } }, "required": [ "id", "kind" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "kind": { "type": "string", "enum": [ "customer" ] } }, "required": [ "id", "kind" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "$ref": "#/components/schemas/quoteIdSchema" }, "kind": { "type": "string", "enum": [ "quote" ] } }, "required": [ "id", "kind" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "$ref": "#/components/schemas/invoiceIdSchema" }, "kind": { "type": "string", "enum": [ "invoice" ] } }, "required": [ "id", "kind" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "$ref": "#/components/schemas/productIdSchema" }, "kind": { "type": "string", "enum": [ "product" ] } }, "required": [ "id", "kind" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "kind": { "type": "string", "enum": [ "customer-asset" ] } }, "required": [ "id", "kind" ], "additionalProperties": false } ] }, "collaborationReferenceSchema": { "type": "object", "discriminator": { "propertyName": "kind" }, "oneOf": [ { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "kind": { "type": "string", "enum": [ "user" ] }, "label": { "type": "string", "minLength": 1, "maxLength": 500 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "path": { "type": "string", "pattern": "^/", "maxLength": 2000 } }, "required": [ "id", "kind", "label", "organizationId", "path" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "kind": { "type": "string", "enum": [ "team" ] }, "label": { "type": "string", "minLength": 1, "maxLength": 500 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "path": { "type": "string", "pattern": "^/", "maxLength": 2000 } }, "required": [ "id", "kind", "label", "organizationId", "path" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "kind": { "type": "string", "enum": [ "task" ] }, "label": { "type": "string", "minLength": 1, "maxLength": 500 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "path": { "type": "string", "pattern": "^/", "maxLength": 2000 } }, "required": [ "id", "kind", "label", "organizationId", "path" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "kind": { "type": "string", "enum": [ "customer" ] }, "label": { "type": "string", "minLength": 1, "maxLength": 500 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "path": { "type": "string", "pattern": "^/", "maxLength": 2000 } }, "required": [ "id", "kind", "label", "organizationId", "path" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "$ref": "#/components/schemas/quoteIdSchema" }, "kind": { "type": "string", "enum": [ "quote" ] }, "label": { "type": "string", "minLength": 1, "maxLength": 500 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "path": { "type": "string", "pattern": "^/", "maxLength": 2000 } }, "required": [ "id", "kind", "label", "organizationId", "path" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "$ref": "#/components/schemas/invoiceIdSchema" }, "kind": { "type": "string", "enum": [ "invoice" ] }, "label": { "type": "string", "minLength": 1, "maxLength": 500 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "path": { "type": "string", "pattern": "^/", "maxLength": 2000 } }, "required": [ "id", "kind", "label", "organizationId", "path" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "$ref": "#/components/schemas/productIdSchema" }, "kind": { "type": "string", "enum": [ "product" ] }, "label": { "type": "string", "minLength": 1, "maxLength": 500 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "path": { "type": "string", "pattern": "^/", "maxLength": 2000 } }, "required": [ "id", "kind", "label", "organizationId", "path" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "kind": { "type": "string", "enum": [ "customer-asset" ] }, "label": { "type": "string", "minLength": 1, "maxLength": 500 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "path": { "type": "string", "pattern": "^/", "maxLength": 2000 } }, "required": [ "id", "kind", "label", "organizationId", "path" ], "additionalProperties": false } ] }, "customerAssetIdSchema": { "type": "string", "format": "uuid" }, "customerAssetNoteInputSchema": { "$ref": "#/components/schemas/collaborationComposerInputSchema" }, "customerAssetNoteSchema": { "type": "object", "properties": { "author": { "anyOf": [ { "$ref": "#/components/schemas/collaborationCommentAuthorSchema" }, { "type": "null" } ] }, "authorId": { "type": [ "string", "null" ], "format": "uuid" }, "contentDoc": { "$ref": "#/components/schemas/collaborationDocumentSchema" }, "contentText": { "type": "string", "minLength": 1, "maxLength": 20000 }, "createdAt": { "type": "string", "format": "date-time" }, "customerAssetId": { "$ref": "#/components/schemas/customerAssetIdSchema" }, "id": { "type": "string", "format": "uuid" }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "updatedAt": { "type": "string", "format": "date-time" } }, "required": [ "author", "authorId", "contentDoc", "contentText", "createdAt", "customerAssetId", "id", "organizationId", "updatedAt" ] }, "invoiceIdSchema": { "type": "string", "format": "uuid" }, "organizationIdSchema": { "type": "string", "format": "uuid" }, "productIdSchema": { "type": "string", "format": "uuid" }, "quoteIdSchema": { "type": "string", "format": "uuid" } } ``` # Update an inventory stock record. (/docs/openapi/updateInventoryStockRecord) Update an inventory stock record for a product. ## PATCH /v1/organizations/{organizationId}/inventory/stock-records/{stockRecordId} ```json { "operationId": "updateInventoryStockRecord", "summary": "Update an inventory stock record.", "description": "Update an inventory stock record for a product.", "tags": [ "Assets" ], "parameters": [ { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" }, { "in": "path", "name": "stockRecordId", "required": true, "schema": { "$ref": "#/components/schemas/inventoryStockRecordIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/inventoryStockRecordInputSchema" } } }, "description": "The inventory stock record payload." }, "responses": { "200": { "description": "Returns the updated stock record.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/inventoryStockRecordSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "assetImageIdSchema": { "type": "string", "format": "uuid" }, "assetImageSchema": { "type": "object", "properties": { "altText": { "type": [ "string", "null" ] }, "createdAt": { "type": "string", "format": "date-time" }, "id": { "$ref": "#/components/schemas/assetImageIdSchema" }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "position": { "type": "integer", "minimum": 0 }, "productId": { "anyOf": [ { "$ref": "#/components/schemas/productIdSchema" }, { "type": "null" } ] }, "updatedAt": { "type": "string", "format": "date-time" }, "url": { "type": "string", "format": "uri" } }, "required": [ "altText", "createdAt", "id", "organizationId", "position", "productId", "updatedAt", "url" ] }, "brandIdSchema": { "type": "string", "enum": [ "centrakit", "mechup", "stabledock", "caresly", "propdock", "marryduck", "signaldock", "spacelio", "legaldock" ] }, "currencyCodeSchema": { "type": "string", "pattern": "^[A-Z]{3}$" }, "iconNameSchema": { "type": "string", "minLength": 1, "maxLength": 120, "pattern": "^[A-Za-z0-9]+Icon$" }, "inventoryLocationIdSchema": { "type": "string", "format": "uuid" }, "inventoryLocationSchema": { "type": "object", "properties": { "createdAt": { "type": "string", "format": "date-time" }, "description": { "type": [ "string", "null" ] }, "id": { "$ref": "#/components/schemas/inventoryLocationIdSchema" }, "name": { "type": "string", "minLength": 1, "maxLength": 240 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "updatedAt": { "type": "string", "format": "date-time" } }, "required": [ "createdAt", "description", "id", "name", "organizationId", "updatedAt" ] }, "inventoryStatusSchema": { "type": "string", "enum": [ "available", "quarantined", "expired", "archived" ] }, "inventoryStockRecordIdSchema": { "type": "string", "format": "uuid" }, "inventoryStockRecordInputSchema": { "type": "object", "properties": { "binLocation": { "allOf": [ { "$ref": "#/components/schemas/nullableShortText" } ] }, "currency": { "allOf": [ { "$ref": "#/components/schemas/nullableCurrency" } ] }, "expiresOn": { "allOf": [ { "$ref": "#/components/schemas/nullableDate" } ] }, "lastCountedOn": { "allOf": [ { "$ref": "#/components/schemas/nullableDate" } ] }, "locationId": { "anyOf": [ { "$ref": "#/components/schemas/inventoryLocationIdSchema" }, { "type": "null" } ] }, "locationName": { "allOf": [ { "$ref": "#/components/schemas/nullableShortText" } ] }, "lotNumber": { "allOf": [ { "$ref": "#/components/schemas/nullableShortText" } ] }, "notes": { "allOf": [ { "$ref": "#/components/schemas/nullableLongText" } ] }, "onHandQuantity": { "type": "number", "minimum": 0 }, "productId": { "$ref": "#/components/schemas/productIdSchema" }, "serialNumber": { "allOf": [ { "$ref": "#/components/schemas/nullableShortText" } ] }, "status": { "allOf": [ { "$ref": "#/components/schemas/inventoryStatusSchema" } ] }, "unitCost": { "allOf": [ { "$ref": "#/components/schemas/nullableMoney" } ] }, "variantId": { "type": "string", "format": "uuid" } }, "required": [ "onHandQuantity", "productId" ] }, "inventoryStockRecordSchema": { "type": "object", "properties": { "allocatedQuantity": { "type": "number", "minimum": 0 }, "availableQuantity": { "type": "number", "minimum": 0 }, "binLocation": { "type": [ "string", "null" ] }, "createdAt": { "type": "string", "format": "date-time" }, "createdBy": { "type": [ "string", "null" ], "format": "uuid" }, "currency": { "type": [ "string", "null" ] }, "expiresOn": { "$ref": "#/components/schemas/nullableDate" }, "id": { "$ref": "#/components/schemas/inventoryStockRecordIdSchema" }, "lastCountedOn": { "$ref": "#/components/schemas/nullableDate" }, "location": { "anyOf": [ { "$ref": "#/components/schemas/inventoryLocationSchema" }, { "type": "null" } ] }, "locationId": { "anyOf": [ { "$ref": "#/components/schemas/inventoryLocationIdSchema" }, { "type": "null" } ] }, "lotNumber": { "type": [ "string", "null" ] }, "notes": { "type": [ "string", "null" ] }, "onHandQuantity": { "type": "number", "minimum": 0 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "product": { "anyOf": [ { "$ref": "#/components/schemas/productListItemSchema" }, { "type": "null" } ] }, "productId": { "$ref": "#/components/schemas/productIdSchema" }, "serialNumber": { "type": [ "string", "null" ] }, "status": { "$ref": "#/components/schemas/inventoryStatusSchema" }, "unitCost": { "type": [ "number", "null" ] }, "updatedAt": { "type": "string", "format": "date-time" }, "updatedBy": { "type": [ "string", "null" ], "format": "uuid" }, "variantId": { "type": "string", "format": "uuid" } }, "required": [ "allocatedQuantity", "availableQuantity", "binLocation", "createdAt", "createdBy", "currency", "expiresOn", "id", "lastCountedOn", "location", "locationId", "lotNumber", "notes", "onHandQuantity", "organizationId", "product", "productId", "serialNumber", "status", "unitCost", "updatedAt", "updatedBy", "variantId" ] }, "nullableCurrency": { "anyOf": [ { "$ref": "#/components/schemas/currencyCodeSchema" }, { "type": "null" } ] }, "nullableDate": { "type": [ "string", "null" ], "format": "date" }, "nullableLongText": { "type": [ "string", "null" ], "maxLength": 4000 }, "nullableMoney": { "type": [ "number", "null" ], "minimum": 0 }, "nullableShortText": { "type": [ "string", "null" ], "maxLength": 240 }, "organizationIdSchema": { "type": "string", "format": "uuid" }, "organizationTagSchema": { "type": "object", "properties": { "color": { "anyOf": [ { "$ref": "#/components/schemas/tagColorSchema" }, { "type": "null" } ] }, "createdAt": { "type": "string", "format": "date-time" }, "description": { "type": [ "string", "null" ] }, "id": { "$ref": "#/components/schemas/tagIdSchema" }, "kind": { "$ref": "#/components/schemas/tagKindSchema" }, "name": { "type": "string", "minLength": 1, "maxLength": 80 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "position": { "type": "integer", "minimum": 0 }, "slug": { "$ref": "#/components/schemas/tagSlugSchema" }, "updatedAt": { "type": "string", "format": "date-time" } }, "required": [ "color", "createdAt", "description", "id", "kind", "name", "organizationId", "position", "slug", "updatedAt" ] }, "productCategorySchema": { "type": "object", "properties": { "createdAt": { "type": "string", "format": "date-time" }, "description": { "type": [ "string", "null" ] }, "icon": { "$ref": "#/components/schemas/iconNameSchema" }, "id": { "type": "string", "format": "uuid" }, "name": { "type": "string", "minLength": 1, "maxLength": 120 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "parentId": { "type": [ "string", "null" ], "format": "uuid" }, "position": { "type": "integer", "minimum": 0 }, "slug": { "$ref": "#/components/schemas/slugSchema" }, "updatedAt": { "type": "string", "format": "date-time" } }, "required": [ "createdAt", "description", "icon", "id", "name", "organizationId", "parentId", "position", "slug", "updatedAt" ] }, "productIdSchema": { "type": "string", "format": "uuid" }, "productListItemSchema": { "type": "object", "properties": { "brand": { "type": [ "object", "null" ], "properties": { "createdAt": { "type": "string", "format": "date-time" }, "description": { "type": [ "string", "null" ] }, "id": { "$ref": "#/components/schemas/brandIdSchema" }, "logoUrl": { "type": [ "string", "null" ], "format": "uri" }, "name": { "type": "string", "minLength": 1, "maxLength": 240 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "slug": { "type": "string", "minLength": 1, "maxLength": 160, "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$" }, "updatedAt": { "type": "string", "format": "date-time" }, "website": { "type": [ "string", "null" ] } }, "required": [ "createdAt", "description", "id", "logoUrl", "name", "organizationId", "slug", "updatedAt", "website" ] }, "categories": { "type": "array", "items": { "$ref": "#/components/schemas/productCategorySchema" } }, "categoryIds": { "type": "array", "items": { "type": "string", "format": "uuid" } }, "currency": { "type": [ "string", "null" ] }, "defaultSellingPrice": { "type": [ "number", "null" ] }, "ean": { "type": [ "string", "null" ] }, "id": { "$ref": "#/components/schemas/productIdSchema" }, "images": { "type": "array", "items": { "$ref": "#/components/schemas/assetImageSchema" } }, "manufacturer": { "type": [ "string", "null" ] }, "manufacturerPartNumber": { "type": [ "string", "null" ] }, "name": { "type": "string", "minLength": 1 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "sku": { "type": [ "string", "null" ] }, "slug": { "$ref": "#/components/schemas/slugSchema" }, "status": { "$ref": "#/components/schemas/productStatusSchema" }, "suppliers": { "type": "array", "items": { "$ref": "#/components/schemas/productSupplierLinkSchema" } }, "tags": { "type": "array", "items": { "$ref": "#/components/schemas/organizationTagSchema" } }, "unitOfMeasure": { "type": [ "string", "null" ] }, "updatedAt": { "type": "string", "format": "date-time" }, "vatPercentage": { "type": [ "number", "null" ] }, "stockStatus": { "anyOf": [ { "$ref": "#/components/schemas/stockStatusSchema" }, { "type": "null" } ] } }, "required": [ "brand", "categories", "categoryIds", "currency", "defaultSellingPrice", "ean", "id", "images", "manufacturer", "manufacturerPartNumber", "name", "organizationId", "sku", "slug", "status", "suppliers", "tags", "unitOfMeasure", "updatedAt", "vatPercentage" ] }, "productStatusSchema": { "type": "string", "enum": [ "active", "discontinued", "archived" ] }, "productSupplierLinkSchema": { "type": "object", "properties": { "createdAt": { "type": "string", "format": "date-time" }, "currency": { "type": [ "string", "null" ] }, "externalId": { "type": [ "string", "null" ] }, "id": { "type": "string", "format": "uuid" }, "isActive": { "type": "boolean" }, "isPreferred": { "type": "boolean" }, "leadTimeDays": { "type": [ "integer", "null" ] }, "minOrderQuantity": { "type": [ "number", "null" ] }, "orderCode": { "type": [ "string", "null" ] }, "orderMultiple": { "type": [ "number", "null" ] }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "productId": { "$ref": "#/components/schemas/productIdSchema" }, "supplier": { "anyOf": [ { "$ref": "#/components/schemas/supplierSchema" }, { "type": "null" } ] }, "supplierId": { "$ref": "#/components/schemas/supplierIdSchema" }, "supplierSku": { "type": [ "string", "null" ] }, "unitCost": { "type": [ "number", "null" ] }, "updatedAt": { "type": "string", "format": "date-time" } }, "required": [ "createdAt", "currency", "externalId", "id", "isActive", "isPreferred", "leadTimeDays", "minOrderQuantity", "orderCode", "orderMultiple", "organizationId", "productId", "supplier", "supplierId", "supplierSku", "unitCost", "updatedAt" ] }, "slugSchema": { "type": "string", "minLength": 1, "maxLength": 160, "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$" }, "stockStatusSchema": { "type": "string", "enum": [ "not_tracked", "discontinued", "out_of_stock", "backordered", "low_stock", "in_stock" ] }, "supplierIdSchema": { "type": "string", "format": "uuid" }, "supplierSchema": { "type": "object", "properties": { "accountNumber": { "type": [ "string", "null" ] }, "addressCity": { "type": [ "string", "null" ] }, "addressCountry": { "type": [ "string", "null" ] }, "addressLine1": { "type": [ "string", "null" ] }, "addressLine2": { "type": [ "string", "null" ] }, "addressPostalCode": { "type": [ "string", "null" ] }, "addressState": { "type": [ "string", "null" ] }, "billingEmail": { "type": [ "string", "null" ] }, "btw": { "type": [ "string", "null" ] }, "code": { "type": [ "string", "null" ] }, "createdAt": { "type": "string", "format": "date-time" }, "currency": { "type": [ "string", "null" ] }, "defaultInventoryLocationId": { "anyOf": [ { "$ref": "#/components/schemas/inventoryLocationIdSchema" }, { "type": "null" } ] }, "defaultLeadTimeDays": { "type": [ "integer", "null" ] }, "email": { "type": [ "string", "null" ] }, "externalId": { "type": [ "string", "null" ] }, "id": { "$ref": "#/components/schemas/supplierIdSchema" }, "kvk": { "type": [ "string", "null" ] }, "name": { "type": "string", "minLength": 1, "maxLength": 240 }, "notes": { "type": [ "string", "null" ] }, "orderingEmail": { "type": [ "string", "null" ] }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "paymentTermsDays": { "type": [ "integer", "null" ] }, "phone": { "type": [ "string", "null" ] }, "source": { "type": [ "string", "null" ] }, "status": { "$ref": "#/components/schemas/supplierStatusSchema" }, "updatedAt": { "type": "string", "format": "date-time" }, "website": { "type": [ "string", "null" ] } }, "required": [ "accountNumber", "addressCity", "addressCountry", "addressLine1", "addressLine2", "addressPostalCode", "addressState", "billingEmail", "btw", "code", "createdAt", "currency", "defaultInventoryLocationId", "defaultLeadTimeDays", "email", "externalId", "id", "kvk", "name", "notes", "orderingEmail", "organizationId", "paymentTermsDays", "phone", "source", "status", "updatedAt", "website" ] }, "supplierStatusSchema": { "type": "string", "enum": [ "active", "inactive", "archived" ] }, "tagColorSchema": { "type": "string", "pattern": "^#(?:[0-9a-f]{3}|[0-9a-f]{6})$" }, "tagIdSchema": { "type": "string", "format": "uuid" }, "tagKindSchema": { "type": "string", "enum": [ "task", "customer", "product" ] }, "tagSlugSchema": { "type": "string", "minLength": 1, "maxLength": 160, "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$" } } ``` # Archive an inventory stock record. (/docs/openapi/archiveInventoryStockRecord) Archive an inventory stock record. ## DELETE /v1/organizations/{organizationId}/inventory/stock-records/{stockRecordId} ```json { "operationId": "archiveInventoryStockRecord", "summary": "Archive an inventory stock record.", "description": "Archive an inventory stock record.", "tags": [ "Assets" ], "parameters": [ { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" }, { "in": "path", "name": "stockRecordId", "required": true, "schema": { "$ref": "#/components/schemas/inventoryStockRecordIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "responses": { "200": { "description": "Returns a success payload when the stock record is archived.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/publicApiSuccessResponseSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "inventoryStockRecordIdSchema": { "type": "string", "format": "uuid" }, "organizationIdSchema": { "type": "string", "format": "uuid" }, "publicApiSuccessResponseSchema": { "type": "object", "properties": { "success": { "type": "boolean", "enum": [ true ] } }, "required": [ "success" ] } } ``` # Move a product category. (/docs/openapi/moveProductCategory) Move a product category to a new parent and position. ## POST /v1/organizations/{organizationId}/product-categories/{categoryId}/move ```json { "operationId": "moveProductCategory", "summary": "Move a product category.", "description": "Move a product category to a new parent and position.", "tags": [ "Assets" ], "parameters": [ { "in": "path", "name": "categoryId", "required": true, "schema": { "type": "string", "format": "uuid" }, "example": "123e4567-e89b-12d3-a456-426614174000" }, { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/productCategoryMoveInputSchema" } } }, "description": "The category move payload." }, "responses": { "200": { "description": "Returns the updated product category tree as a paginated response.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/productCategoriesListResponseSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "organizationIdSchema": { "type": "string", "format": "uuid" }, "productCategoriesListResponseSchema": { "type": "object" }, "productCategoryMoveInputSchema": { "type": "object", "properties": { "parentId": { "type": [ "string", "null" ], "format": "uuid" }, "position": { "type": "integer", "minimum": 0 } }, "required": [ "parentId", "position" ] } } ``` # Add a product supplier link. (/docs/openapi/addProductSupplierLink) Link a supplier to a product. ## POST /v1/organizations/{organizationId}/products/{productId}/suppliers ```json { "operationId": "addProductSupplierLink", "summary": "Add a product supplier link.", "description": "Link a supplier to a product.", "tags": [ "Assets" ], "parameters": [ { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" }, { "in": "path", "name": "productId", "required": true, "schema": { "$ref": "#/components/schemas/productIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/productSupplierLinkInputSchema" } } }, "description": "The product supplier link payload." }, "responses": { "201": { "description": "Returns the created product supplier link.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/productSupplierLinkSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "inventoryLocationIdSchema": { "type": "string", "format": "uuid" }, "nullableMoney": { "type": [ "number", "null" ], "minimum": 0 }, "nullableNonNegativeInt": { "type": [ "integer", "null" ], "minimum": 0 }, "nullableNonNegativeQuantity": { "type": [ "number", "null" ], "minimum": 0 }, "nullablePositiveQuantity": { "type": [ "number", "null" ], "exclusiveMinimum": 0 }, "nullableShortText": { "type": [ "string", "null" ], "maxLength": 240 }, "organizationIdSchema": { "type": "string", "format": "uuid" }, "productIdSchema": { "type": "string", "format": "uuid" }, "productSupplierLinkInputSchema": { "type": "object", "properties": { "currency": { "type": [ "string", "null" ], "pattern": "^[A-Z]{3}$" }, "externalId": { "allOf": [ { "$ref": "#/components/schemas/nullableShortText" } ] }, "isActive": { "type": "boolean" }, "isPreferred": { "type": "boolean" }, "leadTimeDays": { "allOf": [ { "$ref": "#/components/schemas/nullableNonNegativeInt" } ] }, "minOrderQuantity": { "allOf": [ { "$ref": "#/components/schemas/nullableNonNegativeQuantity" } ] }, "orderCode": { "allOf": [ { "$ref": "#/components/schemas/nullableShortText" } ] }, "orderMultiple": { "allOf": [ { "$ref": "#/components/schemas/nullablePositiveQuantity" } ] }, "supplierId": { "$ref": "#/components/schemas/supplierIdSchema" }, "supplierSku": { "allOf": [ { "$ref": "#/components/schemas/nullableShortText" } ] }, "unitCost": { "allOf": [ { "$ref": "#/components/schemas/nullableMoney" } ] } }, "required": [ "supplierId" ] }, "productSupplierLinkSchema": { "type": "object", "properties": { "createdAt": { "type": "string", "format": "date-time" }, "currency": { "type": [ "string", "null" ] }, "externalId": { "type": [ "string", "null" ] }, "id": { "type": "string", "format": "uuid" }, "isActive": { "type": "boolean" }, "isPreferred": { "type": "boolean" }, "leadTimeDays": { "type": [ "integer", "null" ] }, "minOrderQuantity": { "type": [ "number", "null" ] }, "orderCode": { "type": [ "string", "null" ] }, "orderMultiple": { "type": [ "number", "null" ] }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "productId": { "$ref": "#/components/schemas/productIdSchema" }, "supplier": { "anyOf": [ { "$ref": "#/components/schemas/supplierSchema" }, { "type": "null" } ] }, "supplierId": { "$ref": "#/components/schemas/supplierIdSchema" }, "supplierSku": { "type": [ "string", "null" ] }, "unitCost": { "type": [ "number", "null" ] }, "updatedAt": { "type": "string", "format": "date-time" } }, "required": [ "createdAt", "currency", "externalId", "id", "isActive", "isPreferred", "leadTimeDays", "minOrderQuantity", "orderCode", "orderMultiple", "organizationId", "productId", "supplier", "supplierId", "supplierSku", "unitCost", "updatedAt" ] }, "supplierIdSchema": { "type": "string", "format": "uuid" }, "supplierSchema": { "type": "object", "properties": { "accountNumber": { "type": [ "string", "null" ] }, "addressCity": { "type": [ "string", "null" ] }, "addressCountry": { "type": [ "string", "null" ] }, "addressLine1": { "type": [ "string", "null" ] }, "addressLine2": { "type": [ "string", "null" ] }, "addressPostalCode": { "type": [ "string", "null" ] }, "addressState": { "type": [ "string", "null" ] }, "billingEmail": { "type": [ "string", "null" ] }, "btw": { "type": [ "string", "null" ] }, "code": { "type": [ "string", "null" ] }, "createdAt": { "type": "string", "format": "date-time" }, "currency": { "type": [ "string", "null" ] }, "defaultInventoryLocationId": { "anyOf": [ { "$ref": "#/components/schemas/inventoryLocationIdSchema" }, { "type": "null" } ] }, "defaultLeadTimeDays": { "type": [ "integer", "null" ] }, "email": { "type": [ "string", "null" ] }, "externalId": { "type": [ "string", "null" ] }, "id": { "$ref": "#/components/schemas/supplierIdSchema" }, "kvk": { "type": [ "string", "null" ] }, "name": { "type": "string", "minLength": 1, "maxLength": 240 }, "notes": { "type": [ "string", "null" ] }, "orderingEmail": { "type": [ "string", "null" ] }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "paymentTermsDays": { "type": [ "integer", "null" ] }, "phone": { "type": [ "string", "null" ] }, "source": { "type": [ "string", "null" ] }, "status": { "$ref": "#/components/schemas/supplierStatusSchema" }, "updatedAt": { "type": "string", "format": "date-time" }, "website": { "type": [ "string", "null" ] } }, "required": [ "accountNumber", "addressCity", "addressCountry", "addressLine1", "addressLine2", "addressPostalCode", "addressState", "billingEmail", "btw", "code", "createdAt", "currency", "defaultInventoryLocationId", "defaultLeadTimeDays", "email", "externalId", "id", "kvk", "name", "notes", "orderingEmail", "organizationId", "paymentTermsDays", "phone", "source", "status", "updatedAt", "website" ] }, "supplierStatusSchema": { "type": "string", "enum": [ "active", "inactive", "archived" ] } } ``` # Update a customer asset note. (/docs/openapi/updateCustomerAssetNote) Update an internal note on a customer asset. ## PATCH /v1/organizations/{organizationId}/customer-assets/{customerAssetId}/notes/{noteId} ```json { "operationId": "updateCustomerAssetNote", "summary": "Update a customer asset note.", "description": "Update an internal note on a customer asset.", "tags": [ "Assets" ], "parameters": [ { "in": "path", "name": "customerAssetId", "required": true, "schema": { "type": "string", "format": "uuid" }, "example": "123e4567-e89b-12d3-a456-426614174000" }, { "in": "path", "name": "noteId", "required": true, "schema": { "type": "string", "format": "uuid" }, "example": "123e4567-e89b-12d3-a456-426614174000" }, { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/customerAssetNoteInputSchema" } } }, "description": "The customer asset note payload." }, "responses": { "200": { "description": "Returns the updated customer asset note.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/customerAssetNoteSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "collaborationBlockSchema": { "type": "object", "discriminator": { "propertyName": "type" }, "oneOf": [ { "type": "object", "properties": { "content": { "$ref": "#/components/schemas/collaborationInlineContentSchema" }, "type": { "type": "string", "enum": [ "paragraph" ] } }, "required": [ "content", "type" ], "additionalProperties": false }, { "type": "object", "properties": { "items": { "type": "array", "items": { "$ref": "#/components/schemas/collaborationInlineContentSchema" }, "minItems": 1, "maxItems": 500 }, "type": { "type": "string", "enum": [ "bullet-list" ] } }, "required": [ "items", "type" ], "additionalProperties": false }, { "type": "object", "properties": { "items": { "type": "array", "items": { "$ref": "#/components/schemas/collaborationInlineContentSchema" }, "minItems": 1, "maxItems": 500 }, "type": { "type": "string", "enum": [ "numbered-list" ] } }, "required": [ "items", "type" ], "additionalProperties": false } ] }, "collaborationCommentAuthorSchema": { "type": "object", "properties": { "avatarUrl": { "type": [ "string", "null" ], "format": "uri" }, "displayName": { "type": "string", "minLength": 1 }, "userId": { "type": "string", "format": "uuid" } }, "required": [ "avatarUrl", "displayName", "userId" ] }, "collaborationComposerInputSchema": { "type": "object", "properties": { "selections": { "type": "array", "items": { "$ref": "#/components/schemas/collaborationComposerSelectionSchema" }, "maxItems": 100 }, "text": { "type": "string", "minLength": 1, "maxLength": 20000 } }, "required": [ "selections", "text" ], "additionalProperties": false }, "collaborationComposerSelectionSchema": { "type": "object", "properties": { "end": { "type": "integer", "exclusiveMinimum": 0, "maximum": 20000 }, "reference": { "$ref": "#/components/schemas/collaborationReferenceIdSchema" }, "start": { "type": "integer", "minimum": 0, "maximum": 20000 } }, "required": [ "end", "reference", "start" ], "additionalProperties": false }, "collaborationDocumentSchema": { "type": "object", "properties": { "blocks": { "type": "array", "items": { "$ref": "#/components/schemas/collaborationBlockSchema" }, "minItems": 1, "maxItems": 1000 }, "version": { "type": "integer", "enum": [ 1 ] } }, "required": [ "blocks", "version" ], "additionalProperties": false }, "collaborationInlineContentSchema": { "type": "array", "items": { "$ref": "#/components/schemas/collaborationInlineSchema" }, "minItems": 1, "maxItems": 1000 }, "collaborationInlineSchema": { "type": "object", "discriminator": { "propertyName": "type" }, "oneOf": [ { "type": "object", "properties": { "text": { "type": "string", "minLength": 1, "maxLength": 20000 }, "type": { "type": "string", "enum": [ "text" ] } }, "required": [ "text", "type" ], "additionalProperties": false }, { "type": "object", "properties": { "reference": { "$ref": "#/components/schemas/collaborationReferenceSchema" }, "type": { "type": "string", "enum": [ "reference" ] } }, "required": [ "reference", "type" ], "additionalProperties": false } ] }, "collaborationReferenceIdSchema": { "type": "object", "discriminator": { "propertyName": "kind" }, "oneOf": [ { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "kind": { "type": "string", "enum": [ "user" ] } }, "required": [ "id", "kind" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "kind": { "type": "string", "enum": [ "team" ] } }, "required": [ "id", "kind" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "kind": { "type": "string", "enum": [ "task" ] } }, "required": [ "id", "kind" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "kind": { "type": "string", "enum": [ "customer" ] } }, "required": [ "id", "kind" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "$ref": "#/components/schemas/quoteIdSchema" }, "kind": { "type": "string", "enum": [ "quote" ] } }, "required": [ "id", "kind" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "$ref": "#/components/schemas/invoiceIdSchema" }, "kind": { "type": "string", "enum": [ "invoice" ] } }, "required": [ "id", "kind" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "$ref": "#/components/schemas/productIdSchema" }, "kind": { "type": "string", "enum": [ "product" ] } }, "required": [ "id", "kind" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "kind": { "type": "string", "enum": [ "customer-asset" ] } }, "required": [ "id", "kind" ], "additionalProperties": false } ] }, "collaborationReferenceSchema": { "type": "object", "discriminator": { "propertyName": "kind" }, "oneOf": [ { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "kind": { "type": "string", "enum": [ "user" ] }, "label": { "type": "string", "minLength": 1, "maxLength": 500 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "path": { "type": "string", "pattern": "^/", "maxLength": 2000 } }, "required": [ "id", "kind", "label", "organizationId", "path" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "kind": { "type": "string", "enum": [ "team" ] }, "label": { "type": "string", "minLength": 1, "maxLength": 500 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "path": { "type": "string", "pattern": "^/", "maxLength": 2000 } }, "required": [ "id", "kind", "label", "organizationId", "path" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "kind": { "type": "string", "enum": [ "task" ] }, "label": { "type": "string", "minLength": 1, "maxLength": 500 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "path": { "type": "string", "pattern": "^/", "maxLength": 2000 } }, "required": [ "id", "kind", "label", "organizationId", "path" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "kind": { "type": "string", "enum": [ "customer" ] }, "label": { "type": "string", "minLength": 1, "maxLength": 500 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "path": { "type": "string", "pattern": "^/", "maxLength": 2000 } }, "required": [ "id", "kind", "label", "organizationId", "path" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "$ref": "#/components/schemas/quoteIdSchema" }, "kind": { "type": "string", "enum": [ "quote" ] }, "label": { "type": "string", "minLength": 1, "maxLength": 500 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "path": { "type": "string", "pattern": "^/", "maxLength": 2000 } }, "required": [ "id", "kind", "label", "organizationId", "path" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "$ref": "#/components/schemas/invoiceIdSchema" }, "kind": { "type": "string", "enum": [ "invoice" ] }, "label": { "type": "string", "minLength": 1, "maxLength": 500 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "path": { "type": "string", "pattern": "^/", "maxLength": 2000 } }, "required": [ "id", "kind", "label", "organizationId", "path" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "$ref": "#/components/schemas/productIdSchema" }, "kind": { "type": "string", "enum": [ "product" ] }, "label": { "type": "string", "minLength": 1, "maxLength": 500 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "path": { "type": "string", "pattern": "^/", "maxLength": 2000 } }, "required": [ "id", "kind", "label", "organizationId", "path" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "kind": { "type": "string", "enum": [ "customer-asset" ] }, "label": { "type": "string", "minLength": 1, "maxLength": 500 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "path": { "type": "string", "pattern": "^/", "maxLength": 2000 } }, "required": [ "id", "kind", "label", "organizationId", "path" ], "additionalProperties": false } ] }, "customerAssetIdSchema": { "type": "string", "format": "uuid" }, "customerAssetNoteInputSchema": { "$ref": "#/components/schemas/collaborationComposerInputSchema" }, "customerAssetNoteSchema": { "type": "object", "properties": { "author": { "anyOf": [ { "$ref": "#/components/schemas/collaborationCommentAuthorSchema" }, { "type": "null" } ] }, "authorId": { "type": [ "string", "null" ], "format": "uuid" }, "contentDoc": { "$ref": "#/components/schemas/collaborationDocumentSchema" }, "contentText": { "type": "string", "minLength": 1, "maxLength": 20000 }, "createdAt": { "type": "string", "format": "date-time" }, "customerAssetId": { "$ref": "#/components/schemas/customerAssetIdSchema" }, "id": { "type": "string", "format": "uuid" }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "updatedAt": { "type": "string", "format": "date-time" } }, "required": [ "author", "authorId", "contentDoc", "contentText", "createdAt", "customerAssetId", "id", "organizationId", "updatedAt" ] }, "invoiceIdSchema": { "type": "string", "format": "uuid" }, "organizationIdSchema": { "type": "string", "format": "uuid" }, "productIdSchema": { "type": "string", "format": "uuid" }, "quoteIdSchema": { "type": "string", "format": "uuid" } } ``` # Delete a customer asset note. (/docs/openapi/deleteCustomerAssetNote) Delete an internal note from a customer asset. ## DELETE /v1/organizations/{organizationId}/customer-assets/{customerAssetId}/notes/{noteId} ```json { "operationId": "deleteCustomerAssetNote", "summary": "Delete a customer asset note.", "description": "Delete an internal note from a customer asset.", "tags": [ "Assets" ], "parameters": [ { "in": "path", "name": "customerAssetId", "required": true, "schema": { "type": "string", "format": "uuid" }, "example": "123e4567-e89b-12d3-a456-426614174000" }, { "in": "path", "name": "noteId", "required": true, "schema": { "type": "string", "format": "uuid" }, "example": "123e4567-e89b-12d3-a456-426614174000" }, { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "responses": { "200": { "description": "Returns a success payload when the note is deleted.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/publicApiSuccessResponseSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "organizationIdSchema": { "type": "string", "format": "uuid" }, "publicApiSuccessResponseSchema": { "type": "object", "properties": { "success": { "type": "boolean", "enum": [ true ] } }, "required": [ "success" ] } } ``` # Update a product supplier link. (/docs/openapi/updateProductSupplierLink) Update ordering details for a product supplier link. ## PATCH /v1/organizations/{organizationId}/products/{productId}/suppliers/{linkId} ```json { "operationId": "updateProductSupplierLink", "summary": "Update a product supplier link.", "description": "Update ordering details for a product supplier link.", "tags": [ "Assets" ], "parameters": [ { "in": "path", "name": "linkId", "required": true, "schema": { "type": "string", "format": "uuid" }, "example": "123e4567-e89b-12d3-a456-426614174000" }, { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" }, { "in": "path", "name": "productId", "required": true, "schema": { "$ref": "#/components/schemas/productIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/productSupplierLinkUpdateSchema" } } }, "description": "The product supplier link fields to update." }, "responses": { "200": { "description": "Returns the updated product supplier link.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/productSupplierLinkSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "inventoryLocationIdSchema": { "type": "string", "format": "uuid" }, "nullableMoney": { "type": [ "number", "null" ], "minimum": 0 }, "nullableNonNegativeInt": { "type": [ "integer", "null" ], "minimum": 0 }, "nullableNonNegativeQuantity": { "type": [ "number", "null" ], "minimum": 0 }, "nullablePositiveQuantity": { "type": [ "number", "null" ], "exclusiveMinimum": 0 }, "nullableShortText": { "type": [ "string", "null" ], "maxLength": 240 }, "organizationIdSchema": { "type": "string", "format": "uuid" }, "productIdSchema": { "type": "string", "format": "uuid" }, "productSupplierLinkSchema": { "type": "object", "properties": { "createdAt": { "type": "string", "format": "date-time" }, "currency": { "type": [ "string", "null" ] }, "externalId": { "type": [ "string", "null" ] }, "id": { "type": "string", "format": "uuid" }, "isActive": { "type": "boolean" }, "isPreferred": { "type": "boolean" }, "leadTimeDays": { "type": [ "integer", "null" ] }, "minOrderQuantity": { "type": [ "number", "null" ] }, "orderCode": { "type": [ "string", "null" ] }, "orderMultiple": { "type": [ "number", "null" ] }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "productId": { "$ref": "#/components/schemas/productIdSchema" }, "supplier": { "anyOf": [ { "$ref": "#/components/schemas/supplierSchema" }, { "type": "null" } ] }, "supplierId": { "$ref": "#/components/schemas/supplierIdSchema" }, "supplierSku": { "type": [ "string", "null" ] }, "unitCost": { "type": [ "number", "null" ] }, "updatedAt": { "type": "string", "format": "date-time" } }, "required": [ "createdAt", "currency", "externalId", "id", "isActive", "isPreferred", "leadTimeDays", "minOrderQuantity", "orderCode", "orderMultiple", "organizationId", "productId", "supplier", "supplierId", "supplierSku", "unitCost", "updatedAt" ] }, "productSupplierLinkUpdateSchema": { "type": "object", "properties": { "currency": { "type": [ "string", "null" ], "pattern": "^[A-Z]{3}$" }, "externalId": { "allOf": [ { "$ref": "#/components/schemas/nullableShortText" } ] }, "isActive": { "type": "boolean" }, "isPreferred": { "type": "boolean" }, "leadTimeDays": { "allOf": [ { "$ref": "#/components/schemas/nullableNonNegativeInt" } ] }, "minOrderQuantity": { "allOf": [ { "$ref": "#/components/schemas/nullableNonNegativeQuantity" } ] }, "orderCode": { "allOf": [ { "$ref": "#/components/schemas/nullableShortText" } ] }, "orderMultiple": { "allOf": [ { "$ref": "#/components/schemas/nullablePositiveQuantity" } ] }, "supplierSku": { "allOf": [ { "$ref": "#/components/schemas/nullableShortText" } ] }, "unitCost": { "allOf": [ { "$ref": "#/components/schemas/nullableMoney" } ] } }, "required": [] }, "supplierIdSchema": { "type": "string", "format": "uuid" }, "supplierSchema": { "type": "object", "properties": { "accountNumber": { "type": [ "string", "null" ] }, "addressCity": { "type": [ "string", "null" ] }, "addressCountry": { "type": [ "string", "null" ] }, "addressLine1": { "type": [ "string", "null" ] }, "addressLine2": { "type": [ "string", "null" ] }, "addressPostalCode": { "type": [ "string", "null" ] }, "addressState": { "type": [ "string", "null" ] }, "billingEmail": { "type": [ "string", "null" ] }, "btw": { "type": [ "string", "null" ] }, "code": { "type": [ "string", "null" ] }, "createdAt": { "type": "string", "format": "date-time" }, "currency": { "type": [ "string", "null" ] }, "defaultInventoryLocationId": { "anyOf": [ { "$ref": "#/components/schemas/inventoryLocationIdSchema" }, { "type": "null" } ] }, "defaultLeadTimeDays": { "type": [ "integer", "null" ] }, "email": { "type": [ "string", "null" ] }, "externalId": { "type": [ "string", "null" ] }, "id": { "$ref": "#/components/schemas/supplierIdSchema" }, "kvk": { "type": [ "string", "null" ] }, "name": { "type": "string", "minLength": 1, "maxLength": 240 }, "notes": { "type": [ "string", "null" ] }, "orderingEmail": { "type": [ "string", "null" ] }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "paymentTermsDays": { "type": [ "integer", "null" ] }, "phone": { "type": [ "string", "null" ] }, "source": { "type": [ "string", "null" ] }, "status": { "$ref": "#/components/schemas/supplierStatusSchema" }, "updatedAt": { "type": "string", "format": "date-time" }, "website": { "type": [ "string", "null" ] } }, "required": [ "accountNumber", "addressCity", "addressCountry", "addressLine1", "addressLine2", "addressPostalCode", "addressState", "billingEmail", "btw", "code", "createdAt", "currency", "defaultInventoryLocationId", "defaultLeadTimeDays", "email", "externalId", "id", "kvk", "name", "notes", "orderingEmail", "organizationId", "paymentTermsDays", "phone", "source", "status", "updatedAt", "website" ] }, "supplierStatusSchema": { "type": "string", "enum": [ "active", "inactive", "archived" ] } } ``` # Remove a product supplier link. (/docs/openapi/removeProductSupplierLink) Remove a supplier link from a product. ## DELETE /v1/organizations/{organizationId}/products/{productId}/suppliers/{linkId} ```json { "operationId": "removeProductSupplierLink", "summary": "Remove a product supplier link.", "description": "Remove a supplier link from a product.", "tags": [ "Assets" ], "parameters": [ { "in": "path", "name": "linkId", "required": true, "schema": { "type": "string", "format": "uuid" }, "example": "123e4567-e89b-12d3-a456-426614174000" }, { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" }, { "in": "path", "name": "productId", "required": true, "schema": { "$ref": "#/components/schemas/productIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "responses": { "200": { "description": "Returns a success payload when the supplier link is removed.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/publicApiSuccessResponseSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "organizationIdSchema": { "type": "string", "format": "uuid" }, "productIdSchema": { "type": "string", "format": "uuid" }, "publicApiSuccessResponseSchema": { "type": "object", "properties": { "success": { "type": "boolean", "enum": [ true ] } }, "required": [ "success" ] } } ``` # List organization audit events. (/docs/openapi/listOrganizationAuditEvents) Return a cursor-capable page of redacted organization audit event summaries. Restricted details and sensitive metadata are never returned. ## GET /v1/organizations/{organizationId}/audit-events ```json { "operationId": "listOrganizationAuditEvents", "summary": "List organization audit events.", "description": "Return a cursor-capable page of redacted organization audit event summaries. Restricted details and sensitive metadata are never returned.", "tags": [ "Audit Log" ], "parameters": [ { "in": "query", "name": "actorIds", "required": false, "schema": { "type": "string" }, "example": "example" }, { "in": "query", "name": "actorKinds", "required": false, "schema": { "type": "string" }, "example": "example" }, { "in": "query", "name": "categories", "required": false, "schema": { "type": "string" }, "example": "example" }, { "in": "query", "name": "correlationId", "required": false, "schema": { "type": "string" }, "example": "123" }, { "in": "query", "name": "cursor", "required": false, "schema": { "type": "string" }, "example": "example" }, { "in": "query", "name": "direction", "required": false, "schema": { "type": "string" }, "example": "example" }, { "in": "query", "name": "eventTypes", "required": false, "schema": { "type": "string" }, "example": "example" }, { "in": "query", "name": "from", "required": false, "schema": { "type": "string" }, "example": "example" }, { "in": "query", "name": "offset", "required": false, "schema": { "type": "string" }, "example": "example" }, { "in": "query", "name": "outcomes", "required": false, "schema": { "type": "string" }, "example": "example" }, { "in": "query", "name": "pageSize", "required": false, "schema": { "type": "string" }, "example": "example" }, { "in": "query", "name": "search", "required": false, "schema": { "type": "string" }, "example": "example" }, { "in": "query", "name": "sources", "required": false, "schema": { "type": "string" }, "example": "example" }, { "in": "query", "name": "targetId", "required": false, "schema": { "type": "string" }, "example": "123" }, { "in": "query", "name": "targetType", "required": false, "schema": { "type": "string" }, "example": "example" }, { "in": "query", "name": "to", "required": false, "schema": { "type": "string" }, "example": "example" }, { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "responses": { "200": { "description": "Returns redacted audit event summaries and the next cursor.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/auditLogEventListPageHttpSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "auditHttpActorKindSchema": { "type": "string", "enum": [ "user", "platform_admin", "service", "impersonation", "system", "api_key", "integration", "workflow", "webhook" ] }, "auditHttpCategorySchema": { "type": "string", "enum": [ "authentication", "users", "permissions", "billing", "data", "settings", "integrations", "security", "audit", "system" ] }, "auditHttpDateTimeSchema": { "type": "string", "format": "date-time" }, "auditHttpEventTypeSchema": { "type": "string", "minLength": 1, "maxLength": 160, "pattern": "^[a-z0-9]+(?<group1>[._-][a-z0-9]+)*$" }, "auditHttpOutcomeSchema": { "type": "string", "enum": [ "success", "failure", "denied", "attempted" ] }, "auditHttpSourceSchema": { "type": "string", "enum": [ "saas", "api", "mcp", "ai", "workflow", "webhook", "database", "system" ] }, "auditLogEventListPageHttpSchema": { "type": "object", "properties": { "items": { "type": "array", "items": { "$ref": "#/components/schemas/auditLogEventSummaryHttpSchema" } }, "nextCursor": { "type": [ "string", "null" ] }, "total": { "type": "integer", "minimum": 0 } }, "required": [ "items", "nextCursor", "total" ] }, "auditLogEventSummaryHttpSchema": { "type": "object", "properties": { "actor": { "type": "object", "properties": { "displayName": { "type": [ "string", "null" ] }, "id": { "type": [ "string", "null" ], "format": "uuid" }, "kind": { "$ref": "#/components/schemas/auditHttpActorKindSchema" } }, "required": [ "displayName", "id", "kind" ] }, "category": { "$ref": "#/components/schemas/auditHttpCategorySchema" }, "correlationId": { "type": [ "string", "null" ] }, "eventType": { "$ref": "#/components/schemas/auditHttpEventTypeSchema" }, "id": { "type": "string", "format": "uuid" }, "occurredAt": { "$ref": "#/components/schemas/auditHttpDateTimeSchema" }, "outcome": { "$ref": "#/components/schemas/auditHttpOutcomeSchema" }, "source": { "$ref": "#/components/schemas/auditHttpSourceSchema" }, "summary": { "type": [ "string", "null" ] }, "target": { "type": "object", "properties": { "displayName": { "type": [ "string", "null" ] }, "id": { "type": [ "string", "null" ] }, "type": { "type": [ "string", "null" ] } }, "required": [ "displayName", "id", "type" ] } }, "required": [ "actor", "category", "correlationId", "eventType", "id", "occurredAt", "outcome", "source", "summary", "target" ] }, "organizationIdSchema": { "type": "string", "format": "uuid" } } ``` # Create an organization audit export. (/docs/openapi/createOrganizationAuditExport) Queue an asynchronous CSV export of redacted organization audit events. Restricted details cannot be requested through this endpoint. ## POST /v1/organizations/{organizationId}/audit-exports ```json { "operationId": "createOrganizationAuditExport", "summary": "Create an organization audit export.", "description": "Queue an asynchronous CSV export of redacted organization audit events. Restricted details cannot be requested through this endpoint.", "tags": [ "Audit Log" ], "parameters": [ { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/organizationAuditExportRequestHttpSchema" } } }, "description": "Audit filters covering at most 90 days. Missing dates default to the latest 30 days.", "required": true }, "responses": { "202": { "description": "Returns the accepted asynchronous export job.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/auditExportJobHttpSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "auditExportJobHttpSchema": { "type": "object", "properties": { "completedAt": { "anyOf": [ { "$ref": "#/components/schemas/auditHttpDateTimeSchema" }, { "type": "null" } ] }, "downloadUrl": { "type": "string", "format": "uri" }, "expiresAt": { "anyOf": [ { "$ref": "#/components/schemas/auditHttpDateTimeSchema" }, { "type": "null" } ] }, "failureReason": { "type": [ "string", "null" ] }, "filters": { "$ref": "#/components/schemas/organizationAuditExportFiltersHttpSchema" }, "id": { "type": "string", "format": "uuid" }, "requestedAt": { "$ref": "#/components/schemas/auditHttpDateTimeSchema" }, "rowCount": { "type": [ "integer", "null" ], "minimum": 0 }, "status": { "$ref": "#/components/schemas/auditHttpExportStatusSchema" }, "updatedAt": { "$ref": "#/components/schemas/auditHttpDateTimeSchema" } }, "required": [ "completedAt", "expiresAt", "failureReason", "filters", "id", "requestedAt", "rowCount", "status", "updatedAt" ] }, "auditHttpActorKindSchema": { "type": "string", "enum": [ "user", "platform_admin", "service", "impersonation", "system", "api_key", "integration", "workflow", "webhook" ] }, "auditHttpCategorySchema": { "type": "string", "enum": [ "authentication", "users", "permissions", "billing", "data", "settings", "integrations", "security", "audit", "system" ] }, "auditHttpDateTimeSchema": { "type": "string", "format": "date-time" }, "auditHttpEventTypeSchema": { "type": "string", "minLength": 1, "maxLength": 160, "pattern": "^[a-z0-9]+(?<group1>[._-][a-z0-9]+)*$" }, "auditHttpExportStatusSchema": { "type": "string", "enum": [ "pending", "running", "completed", "failed", "expired" ] }, "auditHttpOutcomeSchema": { "type": "string", "enum": [ "success", "failure", "denied", "attempted" ] }, "auditHttpSourceSchema": { "type": "string", "enum": [ "saas", "api", "mcp", "ai", "workflow", "webhook", "database", "system" ] }, "organizationAuditExportFiltersHttpSchema": { "type": "object", "properties": { "actorIds": { "type": "array", "items": { "type": "string", "format": "uuid" }, "maxItems": 100 }, "actorKinds": { "type": "array", "items": { "$ref": "#/components/schemas/auditHttpActorKindSchema" }, "maxItems": 7 }, "categories": { "type": "array", "items": { "$ref": "#/components/schemas/auditHttpCategorySchema" }, "maxItems": 9 }, "correlationId": { "type": "string", "minLength": 1, "maxLength": 240 }, "direction": { "type": "string", "enum": [ "asc", "desc" ], "default": "desc" }, "eventTypes": { "type": "array", "items": { "$ref": "#/components/schemas/auditHttpEventTypeSchema" }, "maxItems": 100 }, "from": { "allOf": [ { "$ref": "#/components/schemas/auditHttpDateTimeSchema" } ] }, "outcomes": { "type": "array", "items": { "$ref": "#/components/schemas/auditHttpOutcomeSchema" }, "maxItems": 4 }, "search": { "type": "string", "minLength": 1, "maxLength": 200 }, "sources": { "type": "array", "items": { "$ref": "#/components/schemas/auditHttpSourceSchema" }, "maxItems": 8 }, "targetId": { "type": "string", "minLength": 1, "maxLength": 240 }, "targetType": { "type": "string", "minLength": 1, "maxLength": 120 }, "to": { "allOf": [ { "$ref": "#/components/schemas/auditHttpDateTimeSchema" } ] } }, "required": [ "direction" ] }, "organizationAuditExportRequestHttpSchema": { "type": "object", "properties": { "filters": { "type": "object", "properties": { "actorIds": { "type": "array", "items": { "type": "string", "format": "uuid" }, "maxItems": 100 }, "actorKinds": { "type": "array", "items": { "$ref": "#/components/schemas/auditHttpActorKindSchema" }, "maxItems": 7 }, "categories": { "type": "array", "items": { "$ref": "#/components/schemas/auditHttpCategorySchema" }, "maxItems": 9 }, "correlationId": { "type": "string", "minLength": 1, "maxLength": 240 }, "direction": { "type": "string", "enum": [ "asc", "desc" ], "default": "desc" }, "eventTypes": { "type": "array", "items": { "$ref": "#/components/schemas/auditHttpEventTypeSchema" }, "maxItems": 100 }, "from": { "allOf": [ { "$ref": "#/components/schemas/auditHttpDateTimeSchema" } ] }, "outcomes": { "type": "array", "items": { "$ref": "#/components/schemas/auditHttpOutcomeSchema" }, "maxItems": 4 }, "search": { "type": "string", "minLength": 1, "maxLength": 200 }, "sources": { "type": "array", "items": { "$ref": "#/components/schemas/auditHttpSourceSchema" }, "maxItems": 8 }, "targetId": { "type": "string", "minLength": 1, "maxLength": 240 }, "targetType": { "type": "string", "minLength": 1, "maxLength": 120 }, "to": { "allOf": [ { "$ref": "#/components/schemas/auditHttpDateTimeSchema" } ] } }, "required": [ "direction" ], "default": { "direction": "desc" } } }, "required": [ "filters" ] }, "organizationIdSchema": { "type": "string", "format": "uuid" } } ``` # Get an organization audit export. (/docs/openapi/getOrganizationAuditExport) Return one export job owned by the authenticated requester. A five-minute download URL is included only while a completed export remains available. ## GET /v1/organizations/{organizationId}/audit-exports/{exportId} ```json { "operationId": "getOrganizationAuditExport", "summary": "Get an organization audit export.", "description": "Return one export job owned by the authenticated requester. A five-minute download URL is included only while a completed export remains available.", "tags": [ "Audit Log" ], "parameters": [ { "in": "path", "name": "exportId", "required": true, "schema": { "type": "string", "format": "uuid" }, "example": "123e4567-e89b-12d3-a456-426614174000" }, { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "responses": { "200": { "description": "Returns safe export status and an eligible short-lived download URL.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/auditExportJobHttpSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "auditExportJobHttpSchema": { "type": "object", "properties": { "completedAt": { "anyOf": [ { "$ref": "#/components/schemas/auditHttpDateTimeSchema" }, { "type": "null" } ] }, "downloadUrl": { "type": "string", "format": "uri" }, "expiresAt": { "anyOf": [ { "$ref": "#/components/schemas/auditHttpDateTimeSchema" }, { "type": "null" } ] }, "failureReason": { "type": [ "string", "null" ] }, "filters": { "$ref": "#/components/schemas/organizationAuditExportFiltersHttpSchema" }, "id": { "type": "string", "format": "uuid" }, "requestedAt": { "$ref": "#/components/schemas/auditHttpDateTimeSchema" }, "rowCount": { "type": [ "integer", "null" ], "minimum": 0 }, "status": { "$ref": "#/components/schemas/auditHttpExportStatusSchema" }, "updatedAt": { "$ref": "#/components/schemas/auditHttpDateTimeSchema" } }, "required": [ "completedAt", "expiresAt", "failureReason", "filters", "id", "requestedAt", "rowCount", "status", "updatedAt" ] }, "auditHttpActorKindSchema": { "type": "string", "enum": [ "user", "platform_admin", "service", "impersonation", "system", "api_key", "integration", "workflow", "webhook" ] }, "auditHttpCategorySchema": { "type": "string", "enum": [ "authentication", "users", "permissions", "billing", "data", "settings", "integrations", "security", "audit", "system" ] }, "auditHttpDateTimeSchema": { "type": "string", "format": "date-time" }, "auditHttpEventTypeSchema": { "type": "string", "minLength": 1, "maxLength": 160, "pattern": "^[a-z0-9]+(?<group1>[._-][a-z0-9]+)*$" }, "auditHttpExportStatusSchema": { "type": "string", "enum": [ "pending", "running", "completed", "failed", "expired" ] }, "auditHttpOutcomeSchema": { "type": "string", "enum": [ "success", "failure", "denied", "attempted" ] }, "auditHttpSourceSchema": { "type": "string", "enum": [ "saas", "api", "mcp", "ai", "workflow", "webhook", "database", "system" ] }, "organizationAuditExportFiltersHttpSchema": { "type": "object", "properties": { "actorIds": { "type": "array", "items": { "type": "string", "format": "uuid" }, "maxItems": 100 }, "actorKinds": { "type": "array", "items": { "$ref": "#/components/schemas/auditHttpActorKindSchema" }, "maxItems": 7 }, "categories": { "type": "array", "items": { "$ref": "#/components/schemas/auditHttpCategorySchema" }, "maxItems": 9 }, "correlationId": { "type": "string", "minLength": 1, "maxLength": 240 }, "direction": { "type": "string", "enum": [ "asc", "desc" ], "default": "desc" }, "eventTypes": { "type": "array", "items": { "$ref": "#/components/schemas/auditHttpEventTypeSchema" }, "maxItems": 100 }, "from": { "allOf": [ { "$ref": "#/components/schemas/auditHttpDateTimeSchema" } ] }, "outcomes": { "type": "array", "items": { "$ref": "#/components/schemas/auditHttpOutcomeSchema" }, "maxItems": 4 }, "search": { "type": "string", "minLength": 1, "maxLength": 200 }, "sources": { "type": "array", "items": { "$ref": "#/components/schemas/auditHttpSourceSchema" }, "maxItems": 8 }, "targetId": { "type": "string", "minLength": 1, "maxLength": 240 }, "targetType": { "type": "string", "minLength": 1, "maxLength": 120 }, "to": { "allOf": [ { "$ref": "#/components/schemas/auditHttpDateTimeSchema" } ] } }, "required": [ "direction" ] }, "organizationIdSchema": { "type": "string", "format": "uuid" } } ``` # Log in with email and password. (/docs/openapi/loginWithPassword) Exchange an email and password for an authenticated session. ## POST /v1/auth/login ```json { "operationId": "loginWithPassword", "summary": "Log in with email and password.", "description": "Exchange an email and password for an authenticated session.", "tags": [ "Auth" ], "parameters": [], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/loginRequestSchema" } } }, "description": "Email and password used to create an authenticated session." }, "responses": { "200": { "description": "Returns the authenticated user.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/authUserSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "authUserIdSchema": { "type": "string", "format": "uuid" }, "authUserSchema": { "type": "object", "properties": { "createdAt": { "type": "string", "format": "date-time" }, "email": { "type": [ "string", "null" ], "format": "email" }, "emailVerified": { "type": "boolean" }, "id": { "$ref": "#/components/schemas/authUserIdSchema" }, "lastSignInAt": { "type": [ "string", "null" ], "format": "date-time" } }, "required": [ "createdAt", "email", "emailVerified", "id", "lastSignInAt" ] }, "loginRequestSchema": { "type": "object", "properties": { "email": { "$ref": "#/components/schemas/normalizedEmailSchema" }, "password": { "$ref": "#/components/schemas/passwordSchema" } }, "required": [ "email", "password" ], "examples": [ { "email": "test@example.com", "password": "Password123!" } ] }, "normalizedEmailSchema": { "type": "string", "format": "email" }, "passwordSchema": { "type": "string", "minLength": 8, "maxLength": 72 } } ``` # Log out the current session. (/docs/openapi/logoutCurrentSession) Clear the active authenticated session for the current requester. ## POST /v1/auth/logout ```json { "operationId": "logoutCurrentSession", "summary": "Log out the current session.", "description": "Clear the active authenticated session for the current requester.", "tags": [ "Auth" ], "parameters": [], "responses": { "200": { "description": "Returns a success flag when logout completes.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/logoutResponseSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "logoutResponseSchema": { "type": "object", "properties": { "success": { "type": "boolean", "enum": [ true ] } }, "required": [ "success" ] } } ``` # Sign up with email and password. (/docs/openapi/signUpWithPassword) Create a user account and start the standard confirmation flow. ## POST /v1/auth/signup ```json { "operationId": "signUpWithPassword", "summary": "Sign up with email and password.", "description": "Create a user account and start the standard confirmation flow.", "tags": [ "Auth" ], "parameters": [], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/signupRequestSchema" } } }, "description": "Email and password used to create a new user." }, "responses": { "201": { "description": "Returns the created user when the auth provider includes it.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/signupResponseSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "authUserIdSchema": { "type": "string", "format": "uuid" }, "authUserSchema": { "type": "object", "properties": { "createdAt": { "type": "string", "format": "date-time" }, "email": { "type": [ "string", "null" ], "format": "email" }, "emailVerified": { "type": "boolean" }, "id": { "$ref": "#/components/schemas/authUserIdSchema" }, "lastSignInAt": { "type": [ "string", "null" ], "format": "date-time" } }, "required": [ "createdAt", "email", "emailVerified", "id", "lastSignInAt" ] }, "normalizedEmailSchema": { "type": "string", "format": "email" }, "passwordSchema": { "type": "string", "minLength": 8, "maxLength": 72 }, "safeRedirectPathSchema": { "type": "string" }, "signupNameSchema": { "type": "string", "minLength": 1, "maxLength": 128 }, "signupRequestSchema": { "type": "object", "properties": { "email": { "$ref": "#/components/schemas/normalizedEmailSchema" }, "firstName": { "$ref": "#/components/schemas/signupNameSchema" }, "lastName": { "$ref": "#/components/schemas/signupNameSchema" }, "password": { "$ref": "#/components/schemas/passwordSchema" }, "redirectPath": { "allOf": [ { "$ref": "#/components/schemas/safeRedirectPathSchema" } ] } }, "required": [ "email", "firstName", "lastName", "password" ], "examples": [ { "email": "new.user@example.com", "firstName": "New", "lastName": "User", "password": "Password123!", "redirectPath": "/dashboard" } ] }, "signupResponseSchema": { "type": "object", "discriminator": { "propertyName": "status" }, "oneOf": [ { "type": "object", "properties": { "email": { "type": "string", "format": "email" }, "status": { "type": "string", "enum": [ "confirmation_sent" ] }, "user": { "anyOf": [ { "$ref": "#/components/schemas/authUserSchema" }, { "type": "null" } ] } }, "required": [ "email", "status", "user" ] }, { "type": "object", "properties": { "email": { "type": "string", "format": "email" }, "status": { "type": "string", "enum": [ "existing_account_challenge_sent" ] }, "user": { "type": "null" } }, "required": [ "email", "status", "user" ] } ] } } ``` # Start an OAuth provider flow. (/docs/openapi/startOAuthFlow) Create the provider authorization URL for an OAuth sign-in flow. ## GET /v1/auth/oauth/start ```json { "operationId": "startOAuthFlow", "summary": "Start an OAuth provider flow.", "description": "Create the provider authorization URL for an OAuth sign-in flow.", "tags": [ "Auth" ], "parameters": [ { "in": "query", "name": "provider", "required": true, "schema": { "$ref": "#/components/schemas/authProviderSchema" }, "example": "example" }, { "in": "query", "name": "redirectPath", "required": false, "schema": { "type": [ "string", "null" ] }, "example": "example" } ], "responses": { "200": { "description": "Returns the provider redirect URL.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/oauthStartResponseSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "authProviderSchema": { "type": "string", "enum": [ "apple", "azure", "bitbucket", "discord", "facebook", "figma", "github", "gitlab", "google", "kakao", "keycloak", "linkedin_oidc", "notion", "slack", "spotify", "twitch", "twitter", "workos", "zoom" ] }, "oauthStartResponseSchema": { "type": "object", "properties": { "authorizationUrl": { "type": "string", "format": "uri" }, "provider": { "$ref": "#/components/schemas/authProviderSchema" } }, "required": [ "authorizationUrl", "provider" ] } } ``` # Request a password reset email. (/docs/openapi/requestPasswordReset) Start the password recovery flow by sending a recovery email to the provided address. ## POST /v1/auth/password/reset ```json { "operationId": "requestPasswordReset", "summary": "Request a password reset email.", "description": "Start the password recovery flow by sending a recovery email to the provided address.", "tags": [ "Auth" ], "parameters": [], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/passwordResetRequestSchema" } } }, "description": "Email address and optional redirect path used for the recovery flow." }, "responses": { "200": { "description": "Returns the requested email address when the recovery email is accepted.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/passwordResetResponseSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "normalizedEmailSchema": { "type": "string", "format": "email" }, "passwordResetRequestSchema": { "type": "object", "properties": { "email": { "$ref": "#/components/schemas/normalizedEmailSchema" }, "redirectPath": { "allOf": [ { "$ref": "#/components/schemas/safeRedirectPathSchema" } ] } }, "required": [ "email" ], "examples": [ { "email": "test@example.com", "redirectPath": "/login" } ] }, "passwordResetResponseSchema": { "type": "object", "properties": { "email": { "type": "string", "format": "email" }, "success": { "type": "boolean", "enum": [ true ] } }, "required": [ "email", "success" ] }, "safeRedirectPathSchema": { "type": "string" } } ``` # Update the current user's password. (/docs/openapi/updatePassword) Update the password for the active authenticated session, including recovery sessions established through the password reset flow. ## POST /v1/auth/password/update ```json { "operationId": "updatePassword", "summary": "Update the current user's password.", "description": "Update the password for the active authenticated session, including recovery sessions established through the password reset flow.", "tags": [ "Auth" ], "parameters": [], "security": [ { "SessionCookie": [] } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/passwordUpdateRequestSchema" } } }, "description": "The replacement password for the current authenticated user." }, "responses": { "200": { "description": "Returns success when the password update is accepted.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/passwordUpdateResponseSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "passwordSchema": { "type": "string", "minLength": 8, "maxLength": 72 }, "passwordUpdateRequestSchema": { "type": "object", "properties": { "password": { "$ref": "#/components/schemas/passwordSchema" } }, "required": [ "password" ], "examples": [ { "password": "NewPassword123!" } ] }, "passwordUpdateResponseSchema": { "type": "object", "properties": { "success": { "type": "boolean", "enum": [ true ] } }, "required": [ "success" ] } } ``` # Download a public calendar feed by token. (/docs/openapi/downloadPublicCalendarFeed) Returns an iCalendar feed through a bearer-token public link. ## GET /v1/public/calendars/{token} ```json { "operationId": "downloadPublicCalendarFeed", "summary": "Download a public calendar feed by token.", "description": "Returns an iCalendar feed through a bearer-token public link.", "tags": [ "Calendars" ], "parameters": [ { "in": "path", "name": "token", "required": true, "schema": { "type": "string", "minLength": 1 }, "example": "example" } ], "responses": { "200": { "description": "Returns the calendar as an iCalendar document.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/publicCalendarFeedResponseSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "publicCalendarFeedResponseSchema": { "type": "string" } } ``` # Read a public shared chat by token. (/docs/openapi/readPublicSharedChat) Returns a paginated read-only chat through a bearer-token public link. ## GET /v1/public/chats/{token} ```json { "operationId": "readPublicSharedChat", "summary": "Read a public shared chat by token.", "description": "Returns a paginated read-only chat through a bearer-token public link.", "tags": [ "Chats" ], "parameters": [ { "in": "query", "name": "limit", "required": true, "schema": { "type": "integer", "minimum": 1, "maximum": 100, "default": 50 }, "example": 1 }, { "in": "query", "name": "offset", "required": true, "schema": { "default": 0, "type": "integer", "minimum": 0, "maximum": 9007199254740991 }, "example": 1 }, { "in": "path", "name": "token", "required": true, "schema": { "type": "string", "pattern": "^[a-f0-9]{64}$" }, "example": "example" } ], "responses": { "200": { "description": "Returns the shared thread and a chronological message page.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/publicSharedChatResponseSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "jsonValueSchema": { "anyOf": [ { "type": "string" }, { "type": "number" }, { "type": "boolean" }, { "type": "null" }, { "type": "array", "items": { "$ref": "#/components/schemas/jsonValueSchema" } }, { "type": "object", "additionalProperties": { "$ref": "#/components/schemas/jsonValueSchema" }, "propertyNames": { "type": "string" } } ] }, "publicChatMessageSchema": { "type": "object", "properties": { "clientMessageId": { "type": [ "string", "null" ], "minLength": 1, "maxLength": 240 }, "createdAt": { "type": "string", "format": "date-time" }, "createdBy": { "type": [ "string", "null" ], "format": "uuid" }, "id": { "type": "string", "format": "uuid" }, "metadata": { "type": "object", "additionalProperties": { "$ref": "#/components/schemas/jsonValueSchema" }, "propertyNames": { "type": "string" } }, "organizationId": { "type": [ "string", "null" ], "format": "uuid" }, "parts": { "type": "array", "items": {} }, "role": { "type": "string", "enum": [ "system", "user", "assistant", "tool" ] }, "status": { "type": "string", "enum": [ "complete", "partial", "failed" ] }, "threadId": { "type": "string", "format": "uuid" } }, "required": [ "clientMessageId", "createdAt", "createdBy", "id", "metadata", "organizationId", "parts", "role", "status", "threadId" ] }, "publicSharedChatResponseSchema": { "type": "object", "properties": { "messages": { "type": "array", "items": { "$ref": "#/components/schemas/publicChatMessageSchema" } }, "pagination": { "type": "object", "properties": { "hasMore": { "type": "boolean" }, "limit": { "type": "integer", "minimum": 1, "maximum": 100 }, "offset": { "type": "integer", "minimum": 0 }, "total": { "type": "integer", "minimum": 0 } }, "required": [ "hasMore", "limit", "offset", "total" ] }, "thread": { "type": "object", "properties": { "activeStreamId": { "type": [ "string", "null" ], "minLength": 1 }, "activeWorkflowRunId": { "type": [ "string", "null" ], "minLength": 1 }, "archivedAt": { "type": [ "string", "null" ], "format": "date-time" }, "createdAt": { "type": "string", "format": "date-time" }, "id": { "type": "string", "format": "uuid" }, "metadata": { "type": "object", "additionalProperties": { "$ref": "#/components/schemas/jsonValueSchema" }, "propertyNames": { "type": "string" } }, "mode": { "type": "string", "enum": [ "ask", "plan", "agent" ] }, "organizationId": { "type": [ "string", "null" ], "format": "uuid" }, "ownerUserId": { "type": "string", "format": "uuid" }, "pinnedAt": { "type": [ "string", "null" ], "format": "date-time" }, "streamStatus": { "type": "string", "enum": [ "idle", "submitted", "streaming", "interrupted", "stopping", "stopped", "completed", "failed" ] }, "title": { "type": [ "string", "null" ], "minLength": 1, "maxLength": 240 }, "updatedAt": { "type": "string", "format": "date-time" }, "visibility": { "type": "string", "enum": [ "private", "organization", "shared" ] } }, "required": [ "activeStreamId", "activeWorkflowRunId", "archivedAt", "createdAt", "id", "metadata", "mode", "organizationId", "ownerUserId", "pinnedAt", "streamStatus", "title", "updatedAt", "visibility" ] } }, "required": [ "messages", "pagination", "thread" ] } } ``` # Search collaboration references. (/docs/openapi/searchCollaborationReferences) ## GET /v1/organizations/{organizationId}/collaboration/references ```json { "operationId": "searchCollaborationReferences", "summary": "Search collaboration references.", "description": "", "tags": [ "Collaboration" ], "parameters": [ { "in": "query", "name": "kind", "required": false, "schema": { "type": "array", "items": { "$ref": "#/components/schemas/collaborationReferenceKindSchema" }, "maxItems": 8 }, "example": "example" }, { "in": "query", "name": "limit", "required": true, "schema": { "default": 20, "type": "integer", "minimum": 1, "maximum": 50 }, "example": 1 }, { "in": "query", "name": "q", "required": true, "schema": { "type": "string", "maxLength": 200, "default": "" }, "example": "example" }, { "in": "query", "name": "trigger", "required": true, "schema": { "$ref": "#/components/schemas/collaborationReferenceTriggerSchema" }, "example": "example" }, { "in": "path", "name": "organizationId", "required": true, "schema": { "type": "string", "format": "uuid" }, "example": "123e4567-e89b-12d3-a456-426614174000" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "responses": { "200": { "description": "Returns authorized collaboration reference candidates.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/collaborationReferenceSearchResponseSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "collaborationReferenceCandidateSchema": { "type": "object", "discriminator": { "propertyName": "kind" }, "oneOf": [ { "type": "object", "properties": { "description": { "type": [ "string", "null" ], "maxLength": 500 }, "id": { "type": "string", "format": "uuid" }, "imageUrl": { "type": [ "string", "null" ], "format": "uri" }, "kind": { "type": "string", "enum": [ "user" ] }, "label": { "type": "string", "minLength": 1, "maxLength": 500 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "path": { "type": "string", "pattern": "^/", "maxLength": 2000 }, "trigger": { "$ref": "#/components/schemas/collaborationReferenceTriggerSchema" } }, "required": [ "description", "id", "imageUrl", "kind", "label", "organizationId", "path", "trigger" ], "additionalProperties": false }, { "type": "object", "properties": { "description": { "type": [ "string", "null" ], "maxLength": 500 }, "id": { "type": "string", "format": "uuid" }, "imageUrl": { "type": [ "string", "null" ], "format": "uri" }, "kind": { "type": "string", "enum": [ "team" ] }, "label": { "type": "string", "minLength": 1, "maxLength": 500 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "path": { "type": "string", "pattern": "^/", "maxLength": 2000 }, "trigger": { "$ref": "#/components/schemas/collaborationReferenceTriggerSchema" } }, "required": [ "description", "id", "imageUrl", "kind", "label", "organizationId", "path", "trigger" ], "additionalProperties": false }, { "type": "object", "properties": { "description": { "type": [ "string", "null" ], "maxLength": 500 }, "id": { "type": "string", "format": "uuid" }, "imageUrl": { "type": [ "string", "null" ], "format": "uri" }, "kind": { "type": "string", "enum": [ "task" ] }, "label": { "type": "string", "minLength": 1, "maxLength": 500 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "path": { "type": "string", "pattern": "^/", "maxLength": 2000 }, "trigger": { "$ref": "#/components/schemas/collaborationReferenceTriggerSchema" } }, "required": [ "description", "id", "imageUrl", "kind", "label", "organizationId", "path", "trigger" ], "additionalProperties": false }, { "type": "object", "properties": { "description": { "type": [ "string", "null" ], "maxLength": 500 }, "id": { "type": "string", "format": "uuid" }, "imageUrl": { "type": [ "string", "null" ], "format": "uri" }, "kind": { "type": "string", "enum": [ "customer" ] }, "label": { "type": "string", "minLength": 1, "maxLength": 500 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "path": { "type": "string", "pattern": "^/", "maxLength": 2000 }, "trigger": { "$ref": "#/components/schemas/collaborationReferenceTriggerSchema" } }, "required": [ "description", "id", "imageUrl", "kind", "label", "organizationId", "path", "trigger" ], "additionalProperties": false }, { "type": "object", "properties": { "description": { "type": [ "string", "null" ], "maxLength": 500 }, "id": { "$ref": "#/components/schemas/quoteIdSchema" }, "imageUrl": { "type": [ "string", "null" ], "format": "uri" }, "kind": { "type": "string", "enum": [ "quote" ] }, "label": { "type": "string", "minLength": 1, "maxLength": 500 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "path": { "type": "string", "pattern": "^/", "maxLength": 2000 }, "trigger": { "$ref": "#/components/schemas/collaborationReferenceTriggerSchema" } }, "required": [ "description", "id", "imageUrl", "kind", "label", "organizationId", "path", "trigger" ], "additionalProperties": false }, { "type": "object", "properties": { "description": { "type": [ "string", "null" ], "maxLength": 500 }, "id": { "$ref": "#/components/schemas/invoiceIdSchema" }, "imageUrl": { "type": [ "string", "null" ], "format": "uri" }, "kind": { "type": "string", "enum": [ "invoice" ] }, "label": { "type": "string", "minLength": 1, "maxLength": 500 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "path": { "type": "string", "pattern": "^/", "maxLength": 2000 }, "trigger": { "$ref": "#/components/schemas/collaborationReferenceTriggerSchema" } }, "required": [ "description", "id", "imageUrl", "kind", "label", "organizationId", "path", "trigger" ], "additionalProperties": false }, { "type": "object", "properties": { "description": { "type": [ "string", "null" ], "maxLength": 500 }, "id": { "$ref": "#/components/schemas/productIdSchema" }, "imageUrl": { "type": [ "string", "null" ], "format": "uri" }, "kind": { "type": "string", "enum": [ "product" ] }, "label": { "type": "string", "minLength": 1, "maxLength": 500 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "path": { "type": "string", "pattern": "^/", "maxLength": 2000 }, "trigger": { "$ref": "#/components/schemas/collaborationReferenceTriggerSchema" } }, "required": [ "description", "id", "imageUrl", "kind", "label", "organizationId", "path", "trigger" ], "additionalProperties": false }, { "type": "object", "properties": { "description": { "type": [ "string", "null" ], "maxLength": 500 }, "id": { "type": "string", "format": "uuid" }, "imageUrl": { "type": [ "string", "null" ], "format": "uri" }, "kind": { "type": "string", "enum": [ "customer-asset" ] }, "label": { "type": "string", "minLength": 1, "maxLength": 500 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "path": { "type": "string", "pattern": "^/", "maxLength": 2000 }, "trigger": { "$ref": "#/components/schemas/collaborationReferenceTriggerSchema" } }, "required": [ "description", "id", "imageUrl", "kind", "label", "organizationId", "path", "trigger" ], "additionalProperties": false } ] }, "collaborationReferenceKindSchema": { "type": "string", "enum": [ "user", "team", "task", "customer", "quote", "invoice", "product", "customer-asset" ] }, "collaborationReferenceSearchResponseSchema": { "type": "array", "items": { "$ref": "#/components/schemas/collaborationReferenceCandidateSchema" } }, "collaborationReferenceTriggerSchema": { "type": "string", "enum": [ "@", "#" ] }, "invoiceIdSchema": { "type": "string", "format": "uuid" }, "organizationIdSchema": { "type": "string", "format": "uuid" }, "productIdSchema": { "type": "string", "format": "uuid" }, "quoteIdSchema": { "type": "string", "format": "uuid" } } ``` # Search records available to the command center. (/docs/openapi/searchCommandCenterRecords) Search authorized organization, customer portal, or platform administration records. ## GET /v1/command-center/search ```json { "operationId": "searchCommandCenterRecords", "summary": "Search records available to the command center.", "description": "Search authorized organization, customer portal, or platform administration records.", "tags": [ "Command Center" ], "parameters": [ { "in": "query", "name": "limit", "required": true, "schema": { "default": 24, "type": "integer", "minimum": 1, "maximum": 40 }, "example": 1 }, { "in": "query", "name": "mode", "required": true, "schema": { "$ref": "#/components/schemas/commandCenterModeSchema" }, "example": "example" }, { "in": "query", "name": "orgSlug", "required": false, "schema": { "allOf": [ { "$ref": "#/components/schemas/organizationSlugSchema" } ] }, "example": "example" }, { "in": "query", "name": "query", "required": true, "schema": { "type": "string", "minLength": 1, "maxLength": 160 }, "example": "example" } ], "responses": { "200": { "description": "Returns matching command center records.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/commandCenterSearchResponseSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "commandCenterModeSchema": { "type": "string", "enum": [ "organization", "customer", "admin" ] }, "commandCenterRecordHitSchema": { "type": "object", "properties": { "description": { "type": [ "string", "null" ] }, "href": { "type": "string", "minLength": 1 }, "id": { "type": "string", "minLength": 1 }, "kind": { "$ref": "#/components/schemas/commandCenterRecordKindSchema" }, "label": { "type": "string" }, "organizationId": { "type": [ "string", "null" ], "format": "uuid" } }, "required": [ "description", "href", "id", "kind", "label" ] }, "commandCenterRecordKindSchema": { "type": "string", "enum": [ "customer", "task", "product", "customer-asset", "quote", "invoice", "organization", "user", "admin-invoice", "workflow" ] }, "commandCenterSearchResponseSchema": { "type": "object", "properties": { "results": { "type": "array", "items": { "$ref": "#/components/schemas/commandCenterRecordHitSchema" } } }, "required": [ "results" ] }, "organizationSlugSchema": { "type": "string", "minLength": 2, "maxLength": 64, "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$" } } ``` # List customers. (/docs/openapi/listCustomers) Return customers for an organization the authenticated user can access. ## GET /v1/organizations/{organizationId}/customers ```json { "operationId": "listCustomers", "summary": "List customers.", "description": "Return customers for an organization the authenticated user can access.", "tags": [ "Customers" ], "parameters": [ { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "responses": { "200": { "description": "Returns paginated customers.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/customersListResponseSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "customersListResponseSchema": { "type": "object" }, "organizationIdSchema": { "type": "string", "format": "uuid" } } ``` # Create a customer. (/docs/openapi/createCustomer) Create a customer for the specified organization. ## POST /v1/organizations/{organizationId}/customers ```json { "operationId": "createCustomer", "summary": "Create a customer.", "description": "Create a customer for the specified organization.", "tags": [ "Customers" ], "parameters": [ { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/customerCreateInputSchema" } } }, "description": "The customer payload.", "required": true }, "responses": { "201": { "description": "Returns the created customer.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/customerSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "customerAddressSchema": { "type": "object", "properties": { "city": { "$ref": "#/components/schemas/nullableShortText" }, "country": { "$ref": "#/components/schemas/nullableShortText" }, "line1": { "$ref": "#/components/schemas/nullableShortText" }, "line2": { "$ref": "#/components/schemas/nullableShortText" }, "postalCode": { "$ref": "#/components/schemas/nullableShortText" }, "state": { "$ref": "#/components/schemas/nullableShortText" } }, "required": [ "city", "country", "line1", "line2", "postalCode", "state" ] }, "customerAssigneeSchema": { "type": "object", "properties": { "avatarUrl": { "type": [ "string", "null" ], "format": "uri" }, "disabledAt": { "type": [ "string", "null" ] }, "displayName": { "type": "string", "minLength": 1 }, "userId": { "type": "string", "format": "uuid" } }, "required": [ "avatarUrl", "disabledAt", "displayName", "userId" ] }, "customerContactInputSchema": { "type": "object", "properties": { "email": { "allOf": [ { "$ref": "#/components/schemas/nullableEmailInputSchema" } ] }, "firstName": { "type": [ "string", "null" ], "maxLength": 120 }, "isPrimary": { "type": "boolean" }, "jobTitle": { "type": [ "string", "null" ], "maxLength": 120 }, "lastName": { "type": [ "string", "null" ], "maxLength": 120 }, "phone": { "allOf": [ { "$ref": "#/components/schemas/nullablePhoneInputSchema" } ] } } }, "customerCreateInputSchema": { "type": "object", "properties": { "address": { "type": "object", "properties": { "city": { "$ref": "#/components/schemas/nullableShortText" }, "country": { "$ref": "#/components/schemas/nullableShortText" }, "line1": { "$ref": "#/components/schemas/nullableShortText" }, "line2": { "$ref": "#/components/schemas/nullableShortText" }, "postalCode": { "$ref": "#/components/schemas/nullableShortText" }, "state": { "$ref": "#/components/schemas/nullableShortText" } } }, "assigneeIds": { "type": "array", "items": { "type": "string", "format": "uuid" }, "maxItems": 50 }, "billingEmail": { "allOf": [ { "$ref": "#/components/schemas/nullableEmailInputSchema" } ] }, "billingName": { "type": [ "string", "null" ], "maxLength": 240 }, "btw": { "allOf": [ { "$ref": "#/components/schemas/nullableVatIdInputSchema" } ] }, "companyName": { "type": [ "string", "null" ], "maxLength": 240 }, "description": { "allOf": [ { "$ref": "#/components/schemas/nullableLongText" } ] }, "isBusiness": { "type": "boolean" }, "kvk": { "allOf": [ { "$ref": "#/components/schemas/nullableKvkNumberInputSchema" } ] }, "primaryContact": { "allOf": [ { "$ref": "#/components/schemas/customerContactInputSchema" } ] }, "primaryLocation": { "allOf": [ { "$ref": "#/components/schemas/customerLocationInputSchema" } ] }, "status": { "allOf": [ { "$ref": "#/components/schemas/customerStatusSchema" } ] }, "website": { "allOf": [ { "$ref": "#/components/schemas/nullableUrlInputSchema" } ] } } }, "customerIdSchema": { "type": "string", "format": "uuid" }, "customerLocationInputSchema": { "type": "object", "properties": { "address": { "type": "object", "properties": { "city": { "$ref": "#/components/schemas/nullableShortText" }, "country": { "$ref": "#/components/schemas/nullableShortText" }, "line1": { "$ref": "#/components/schemas/nullableShortText" }, "line2": { "$ref": "#/components/schemas/nullableShortText" }, "postalCode": { "$ref": "#/components/schemas/nullableShortText" }, "state": { "$ref": "#/components/schemas/nullableShortText" } } }, "isPrimary": { "type": "boolean" }, "name": { "type": [ "string", "null" ], "maxLength": 120 } } }, "customerSchema": { "type": "object", "properties": { "address": { "$ref": "#/components/schemas/customerAddressSchema" }, "archivedAt": { "type": [ "string", "null" ], "format": "date-time" }, "archivedReason": { "type": [ "string", "null" ] }, "assignees": { "type": "array", "items": { "$ref": "#/components/schemas/customerAssigneeSchema" } }, "billingEmail": { "type": [ "string", "null" ] }, "billingName": { "type": [ "string", "null" ] }, "btw": { "type": [ "string", "null" ] }, "companyName": { "type": [ "string", "null" ] }, "createdAt": { "type": "string", "format": "date-time" }, "createdBy": { "type": [ "string", "null" ], "format": "uuid" }, "description": { "type": [ "string", "null" ] }, "id": { "$ref": "#/components/schemas/customerIdSchema" }, "isBusiness": { "type": "boolean" }, "kvk": { "type": [ "string", "null" ] }, "logoUrl": { "type": [ "string", "null" ], "format": "uri" }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "quoteCount": { "type": "integer", "minimum": 0 }, "status": { "$ref": "#/components/schemas/customerStatusSchema" }, "tags": { "type": "array", "items": { "$ref": "#/components/schemas/organizationTagSchema" } }, "taskCount": { "type": "integer", "minimum": 0 }, "updatedAt": { "type": "string", "format": "date-time" }, "updatedBy": { "type": [ "string", "null" ], "format": "uuid" }, "website": { "type": [ "string", "null" ] } }, "required": [ "address", "archivedAt", "archivedReason", "assignees", "billingEmail", "billingName", "btw", "companyName", "createdAt", "createdBy", "description", "id", "isBusiness", "kvk", "logoUrl", "organizationId", "quoteCount", "status", "tags", "taskCount", "updatedAt", "updatedBy", "website" ] }, "customerStatusSchema": { "type": "string", "enum": [ "prospect", "active", "archived" ] }, "nullableEmailInputSchema": { "type": [ "string", "null" ], "format": "email", "maxLength": 320 }, "nullableKvkNumberInputSchema": { "type": [ "string", "null" ], "maxLength": 40 }, "nullableLongText": { "type": [ "string", "null" ], "maxLength": 4000 }, "nullablePhoneInputSchema": { "type": [ "string", "null" ], "maxLength": 20 }, "nullableShortText": { "type": [ "string", "null" ], "maxLength": 240 }, "nullableUrlInputSchema": { "anyOf": [ { "type": "string", "maxLength": 400, "format": "uri" }, { "type": "null" } ] }, "nullableVatIdInputSchema": { "type": [ "string", "null" ], "maxLength": 40 }, "organizationIdSchema": { "type": "string", "format": "uuid" }, "organizationTagSchema": { "type": "object", "properties": { "color": { "anyOf": [ { "$ref": "#/components/schemas/tagColorSchema" }, { "type": "null" } ] }, "createdAt": { "type": "string", "format": "date-time" }, "description": { "type": [ "string", "null" ] }, "id": { "$ref": "#/components/schemas/tagIdSchema" }, "kind": { "$ref": "#/components/schemas/tagKindSchema" }, "name": { "type": "string", "minLength": 1, "maxLength": 80 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "position": { "type": "integer", "minimum": 0 }, "slug": { "$ref": "#/components/schemas/tagSlugSchema" }, "updatedAt": { "type": "string", "format": "date-time" } }, "required": [ "color", "createdAt", "description", "id", "kind", "name", "organizationId", "position", "slug", "updatedAt" ] }, "tagColorSchema": { "type": "string", "pattern": "^#(?:[0-9a-f]{3}|[0-9a-f]{6})$" }, "tagIdSchema": { "type": "string", "format": "uuid" }, "tagKindSchema": { "type": "string", "enum": [ "task", "customer", "product" ] }, "tagSlugSchema": { "type": "string", "minLength": 1, "maxLength": 160, "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$" } } ``` # Get a customer. (/docs/openapi/getCustomer) Return a customer detail record for the specified organization. ## GET /v1/organizations/{organizationId}/customers/{customerId} ```json { "operationId": "getCustomer", "summary": "Get a customer.", "description": "Return a customer detail record for the specified organization.", "tags": [ "Customers" ], "parameters": [ { "in": "path", "name": "customerId", "required": true, "schema": { "$ref": "#/components/schemas/customerIdSchema" }, "example": "123" }, { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "responses": { "200": { "description": "Returns the requested customer.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/customerDetailSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "collaborationBlockSchema": { "type": "object", "discriminator": { "propertyName": "type" }, "oneOf": [ { "type": "object", "properties": { "content": { "$ref": "#/components/schemas/collaborationInlineContentSchema" }, "type": { "type": "string", "enum": [ "paragraph" ] } }, "required": [ "content", "type" ], "additionalProperties": false }, { "type": "object", "properties": { "items": { "type": "array", "items": { "$ref": "#/components/schemas/collaborationInlineContentSchema" }, "minItems": 1, "maxItems": 500 }, "type": { "type": "string", "enum": [ "bullet-list" ] } }, "required": [ "items", "type" ], "additionalProperties": false }, { "type": "object", "properties": { "items": { "type": "array", "items": { "$ref": "#/components/schemas/collaborationInlineContentSchema" }, "minItems": 1, "maxItems": 500 }, "type": { "type": "string", "enum": [ "numbered-list" ] } }, "required": [ "items", "type" ], "additionalProperties": false } ] }, "collaborationCommentAuthorSchema": { "type": "object", "properties": { "avatarUrl": { "type": [ "string", "null" ], "format": "uri" }, "displayName": { "type": "string", "minLength": 1 }, "userId": { "type": "string", "format": "uuid" } }, "required": [ "avatarUrl", "displayName", "userId" ] }, "collaborationDocumentSchema": { "type": "object", "properties": { "blocks": { "type": "array", "items": { "$ref": "#/components/schemas/collaborationBlockSchema" }, "minItems": 1, "maxItems": 1000 }, "version": { "type": "integer", "enum": [ 1 ] } }, "required": [ "blocks", "version" ], "additionalProperties": false }, "collaborationInlineContentSchema": { "type": "array", "items": { "$ref": "#/components/schemas/collaborationInlineSchema" }, "minItems": 1, "maxItems": 1000 }, "collaborationInlineSchema": { "type": "object", "discriminator": { "propertyName": "type" }, "oneOf": [ { "type": "object", "properties": { "text": { "type": "string", "minLength": 1, "maxLength": 20000 }, "type": { "type": "string", "enum": [ "text" ] } }, "required": [ "text", "type" ], "additionalProperties": false }, { "type": "object", "properties": { "reference": { "$ref": "#/components/schemas/collaborationReferenceSchema" }, "type": { "type": "string", "enum": [ "reference" ] } }, "required": [ "reference", "type" ], "additionalProperties": false } ] }, "collaborationReferenceSchema": { "type": "object", "discriminator": { "propertyName": "kind" }, "oneOf": [ { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "kind": { "type": "string", "enum": [ "user" ] }, "label": { "type": "string", "minLength": 1, "maxLength": 500 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "path": { "type": "string", "pattern": "^/", "maxLength": 2000 } }, "required": [ "id", "kind", "label", "organizationId", "path" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "kind": { "type": "string", "enum": [ "team" ] }, "label": { "type": "string", "minLength": 1, "maxLength": 500 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "path": { "type": "string", "pattern": "^/", "maxLength": 2000 } }, "required": [ "id", "kind", "label", "organizationId", "path" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "kind": { "type": "string", "enum": [ "task" ] }, "label": { "type": "string", "minLength": 1, "maxLength": 500 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "path": { "type": "string", "pattern": "^/", "maxLength": 2000 } }, "required": [ "id", "kind", "label", "organizationId", "path" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "kind": { "type": "string", "enum": [ "customer" ] }, "label": { "type": "string", "minLength": 1, "maxLength": 500 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "path": { "type": "string", "pattern": "^/", "maxLength": 2000 } }, "required": [ "id", "kind", "label", "organizationId", "path" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "$ref": "#/components/schemas/quoteIdSchema" }, "kind": { "type": "string", "enum": [ "quote" ] }, "label": { "type": "string", "minLength": 1, "maxLength": 500 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "path": { "type": "string", "pattern": "^/", "maxLength": 2000 } }, "required": [ "id", "kind", "label", "organizationId", "path" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "$ref": "#/components/schemas/invoiceIdSchema" }, "kind": { "type": "string", "enum": [ "invoice" ] }, "label": { "type": "string", "minLength": 1, "maxLength": 500 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "path": { "type": "string", "pattern": "^/", "maxLength": 2000 } }, "required": [ "id", "kind", "label", "organizationId", "path" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "$ref": "#/components/schemas/productIdSchema" }, "kind": { "type": "string", "enum": [ "product" ] }, "label": { "type": "string", "minLength": 1, "maxLength": 500 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "path": { "type": "string", "pattern": "^/", "maxLength": 2000 } }, "required": [ "id", "kind", "label", "organizationId", "path" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "kind": { "type": "string", "enum": [ "customer-asset" ] }, "label": { "type": "string", "minLength": 1, "maxLength": 500 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "path": { "type": "string", "pattern": "^/", "maxLength": 2000 } }, "required": [ "id", "kind", "label", "organizationId", "path" ], "additionalProperties": false } ] }, "contactMethodSchema": { "type": "object", "properties": { "createdAt": { "type": "string", "format": "date-time" }, "id": { "type": "string", "format": "uuid" }, "isPrimary": { "type": "boolean" }, "label": { "type": [ "string", "null" ], "maxLength": 120 }, "position": { "type": "integer", "minimum": 0 }, "type": { "$ref": "#/components/schemas/contactMethodTypeSchema" }, "updatedAt": { "type": "string", "format": "date-time" }, "value": { "type": "string", "minLength": 1, "maxLength": 320 }, "verifiedAt": { "type": [ "string", "null" ], "format": "date-time" } }, "required": [ "createdAt", "id", "isPrimary", "label", "position", "type", "updatedAt", "value", "verifiedAt" ] }, "contactMethodTypeSchema": { "type": "string", "enum": [ "email", "phone", "whatsapp" ] }, "contactProfileIdSchema": { "type": "string", "format": "uuid" }, "contactProfileSchema": { "type": "object", "properties": { "avatarUrl": { "type": [ "string", "null" ] }, "contactMethods": { "type": "array", "items": { "$ref": "#/components/schemas/contactMethodSchema" } }, "createdAt": { "type": "string", "format": "date-time" }, "displayName": { "type": [ "string", "null" ] }, "firstName": { "type": [ "string", "null" ] }, "id": { "$ref": "#/components/schemas/contactProfileIdSchema" }, "lastName": { "type": [ "string", "null" ] }, "updatedAt": { "type": "string", "format": "date-time" }, "userId": { "type": [ "string", "null" ], "format": "uuid" } }, "required": [ "avatarUrl", "contactMethods", "createdAt", "displayName", "firstName", "id", "lastName", "updatedAt", "userId" ] }, "customerAddressSchema": { "type": "object", "properties": { "city": { "$ref": "#/components/schemas/nullableShortText" }, "country": { "$ref": "#/components/schemas/nullableShortText" }, "line1": { "$ref": "#/components/schemas/nullableShortText" }, "line2": { "$ref": "#/components/schemas/nullableShortText" }, "postalCode": { "$ref": "#/components/schemas/nullableShortText" }, "state": { "$ref": "#/components/schemas/nullableShortText" } }, "required": [ "city", "country", "line1", "line2", "postalCode", "state" ] }, "customerAssigneeSchema": { "type": "object", "properties": { "avatarUrl": { "type": [ "string", "null" ], "format": "uri" }, "disabledAt": { "type": [ "string", "null" ] }, "displayName": { "type": "string", "minLength": 1 }, "userId": { "type": "string", "format": "uuid" } }, "required": [ "avatarUrl", "disabledAt", "displayName", "userId" ] }, "customerContactSchema": { "type": "object", "properties": { "contactProfile": { "$ref": "#/components/schemas/contactProfileSchema" }, "createdAt": { "type": "string", "format": "date-time" }, "customerId": { "$ref": "#/components/schemas/customerIdSchema" }, "id": { "type": "string", "format": "uuid" }, "isPrimary": { "type": "boolean" }, "jobTitle": { "type": [ "string", "null" ] }, "notes": { "type": [ "string", "null" ] }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "updatedAt": { "type": "string", "format": "date-time" } }, "required": [ "contactProfile", "createdAt", "customerId", "id", "isPrimary", "jobTitle", "notes", "organizationId", "updatedAt" ] }, "customerDetailSchema": { "type": "object", "properties": { "contacts": { "type": "array", "items": { "$ref": "#/components/schemas/customerContactSchema" } }, "customer": { "$ref": "#/components/schemas/customerSchema" }, "locations": { "type": "array", "items": { "$ref": "#/components/schemas/customerLocationSchema" } }, "notes": { "type": "array", "items": { "$ref": "#/components/schemas/customerNoteSchema" } } }, "required": [ "contacts", "customer", "locations", "notes" ] }, "customerIdSchema": { "type": "string", "format": "uuid" }, "customerLocationSchema": { "type": "object", "properties": { "address": { "$ref": "#/components/schemas/customerAddressSchema" }, "createdAt": { "type": "string", "format": "date-time" }, "customerId": { "$ref": "#/components/schemas/customerIdSchema" }, "id": { "type": "string", "format": "uuid" }, "isPrimary": { "type": "boolean" }, "name": { "type": "string", "minLength": 1, "maxLength": 120 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "updatedAt": { "type": "string", "format": "date-time" } }, "required": [ "address", "createdAt", "customerId", "id", "isPrimary", "name", "organizationId", "updatedAt" ] }, "customerNoteSchema": { "type": "object", "properties": { "author": { "anyOf": [ { "$ref": "#/components/schemas/collaborationCommentAuthorSchema" }, { "type": "null" } ] }, "contentDoc": { "$ref": "#/components/schemas/collaborationDocumentSchema" }, "contentText": { "type": "string" }, "createdAt": { "type": "string", "format": "date-time" }, "createdBy": { "type": [ "string", "null" ], "format": "uuid" }, "customerId": { "$ref": "#/components/schemas/customerIdSchema" }, "id": { "type": "string", "format": "uuid" }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "updatedAt": { "type": "string", "format": "date-time" } }, "required": [ "author", "contentDoc", "contentText", "createdAt", "createdBy", "customerId", "id", "organizationId", "updatedAt" ] }, "customerSchema": { "type": "object", "properties": { "address": { "$ref": "#/components/schemas/customerAddressSchema" }, "archivedAt": { "type": [ "string", "null" ], "format": "date-time" }, "archivedReason": { "type": [ "string", "null" ] }, "assignees": { "type": "array", "items": { "$ref": "#/components/schemas/customerAssigneeSchema" } }, "billingEmail": { "type": [ "string", "null" ] }, "billingName": { "type": [ "string", "null" ] }, "btw": { "type": [ "string", "null" ] }, "companyName": { "type": [ "string", "null" ] }, "createdAt": { "type": "string", "format": "date-time" }, "createdBy": { "type": [ "string", "null" ], "format": "uuid" }, "description": { "type": [ "string", "null" ] }, "id": { "$ref": "#/components/schemas/customerIdSchema" }, "isBusiness": { "type": "boolean" }, "kvk": { "type": [ "string", "null" ] }, "logoUrl": { "type": [ "string", "null" ], "format": "uri" }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "quoteCount": { "type": "integer", "minimum": 0 }, "status": { "$ref": "#/components/schemas/customerStatusSchema" }, "tags": { "type": "array", "items": { "$ref": "#/components/schemas/organizationTagSchema" } }, "taskCount": { "type": "integer", "minimum": 0 }, "updatedAt": { "type": "string", "format": "date-time" }, "updatedBy": { "type": [ "string", "null" ], "format": "uuid" }, "website": { "type": [ "string", "null" ] } }, "required": [ "address", "archivedAt", "archivedReason", "assignees", "billingEmail", "billingName", "btw", "companyName", "createdAt", "createdBy", "description", "id", "isBusiness", "kvk", "logoUrl", "organizationId", "quoteCount", "status", "tags", "taskCount", "updatedAt", "updatedBy", "website" ] }, "customerStatusSchema": { "type": "string", "enum": [ "prospect", "active", "archived" ] }, "invoiceIdSchema": { "type": "string", "format": "uuid" }, "nullableShortText": { "type": [ "string", "null" ], "maxLength": 240 }, "organizationIdSchema": { "type": "string", "format": "uuid" }, "organizationTagSchema": { "type": "object", "properties": { "color": { "anyOf": [ { "$ref": "#/components/schemas/tagColorSchema" }, { "type": "null" } ] }, "createdAt": { "type": "string", "format": "date-time" }, "description": { "type": [ "string", "null" ] }, "id": { "$ref": "#/components/schemas/tagIdSchema" }, "kind": { "$ref": "#/components/schemas/tagKindSchema" }, "name": { "type": "string", "minLength": 1, "maxLength": 80 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "position": { "type": "integer", "minimum": 0 }, "slug": { "$ref": "#/components/schemas/tagSlugSchema" }, "updatedAt": { "type": "string", "format": "date-time" } }, "required": [ "color", "createdAt", "description", "id", "kind", "name", "organizationId", "position", "slug", "updatedAt" ] }, "productIdSchema": { "type": "string", "format": "uuid" }, "quoteIdSchema": { "type": "string", "format": "uuid" }, "tagColorSchema": { "type": "string", "pattern": "^#(?:[0-9a-f]{3}|[0-9a-f]{6})$" }, "tagIdSchema": { "type": "string", "format": "uuid" }, "tagKindSchema": { "type": "string", "enum": [ "task", "customer", "product" ] }, "tagSlugSchema": { "type": "string", "minLength": 1, "maxLength": 160, "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$" } } ``` # Update a customer. (/docs/openapi/updateCustomer) Update a customer for the specified organization. ## PATCH /v1/organizations/{organizationId}/customers/{customerId} ```json { "operationId": "updateCustomer", "summary": "Update a customer.", "description": "Update a customer for the specified organization.", "tags": [ "Customers" ], "parameters": [ { "in": "path", "name": "customerId", "required": true, "schema": { "$ref": "#/components/schemas/customerIdSchema" }, "example": "123" }, { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/customerInputSchema" } } }, "description": "The customer payload.", "required": true }, "responses": { "200": { "description": "Returns the updated customer.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/customerSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "customerAddressSchema": { "type": "object", "properties": { "city": { "$ref": "#/components/schemas/nullableShortText" }, "country": { "$ref": "#/components/schemas/nullableShortText" }, "line1": { "$ref": "#/components/schemas/nullableShortText" }, "line2": { "$ref": "#/components/schemas/nullableShortText" }, "postalCode": { "$ref": "#/components/schemas/nullableShortText" }, "state": { "$ref": "#/components/schemas/nullableShortText" } }, "required": [ "city", "country", "line1", "line2", "postalCode", "state" ] }, "customerAssigneeSchema": { "type": "object", "properties": { "avatarUrl": { "type": [ "string", "null" ], "format": "uri" }, "disabledAt": { "type": [ "string", "null" ] }, "displayName": { "type": "string", "minLength": 1 }, "userId": { "type": "string", "format": "uuid" } }, "required": [ "avatarUrl", "disabledAt", "displayName", "userId" ] }, "customerContactInputSchema": { "type": "object", "properties": { "email": { "allOf": [ { "$ref": "#/components/schemas/nullableEmailInputSchema" } ] }, "firstName": { "type": [ "string", "null" ], "maxLength": 120 }, "isPrimary": { "type": "boolean" }, "jobTitle": { "type": [ "string", "null" ], "maxLength": 120 }, "lastName": { "type": [ "string", "null" ], "maxLength": 120 }, "phone": { "allOf": [ { "$ref": "#/components/schemas/nullablePhoneInputSchema" } ] } } }, "customerIdSchema": { "type": "string", "format": "uuid" }, "customerInputSchema": { "type": "object", "properties": { "address": { "type": "object", "properties": { "city": { "$ref": "#/components/schemas/nullableShortText" }, "country": { "$ref": "#/components/schemas/nullableShortText" }, "line1": { "$ref": "#/components/schemas/nullableShortText" }, "line2": { "$ref": "#/components/schemas/nullableShortText" }, "postalCode": { "$ref": "#/components/schemas/nullableShortText" }, "state": { "$ref": "#/components/schemas/nullableShortText" } } }, "assigneeIds": { "type": "array", "items": { "type": "string", "format": "uuid" }, "maxItems": 50 }, "billingEmail": { "allOf": [ { "$ref": "#/components/schemas/nullableEmailInputSchema" } ] }, "billingName": { "type": [ "string", "null" ], "maxLength": 240 }, "btw": { "allOf": [ { "$ref": "#/components/schemas/nullableVatIdInputSchema" } ] }, "companyName": { "type": [ "string", "null" ], "maxLength": 240 }, "description": { "allOf": [ { "$ref": "#/components/schemas/nullableLongText" } ] }, "isBusiness": { "type": "boolean" }, "kvk": { "allOf": [ { "$ref": "#/components/schemas/nullableKvkNumberInputSchema" } ] }, "primaryContact": { "allOf": [ { "$ref": "#/components/schemas/customerContactInputSchema" } ] }, "primaryLocation": { "allOf": [ { "$ref": "#/components/schemas/customerLocationInputSchema" } ] }, "status": { "allOf": [ { "$ref": "#/components/schemas/customerStatusSchema" } ] }, "website": { "allOf": [ { "$ref": "#/components/schemas/nullableUrlInputSchema" } ] } } }, "customerLocationInputSchema": { "type": "object", "properties": { "address": { "type": "object", "properties": { "city": { "$ref": "#/components/schemas/nullableShortText" }, "country": { "$ref": "#/components/schemas/nullableShortText" }, "line1": { "$ref": "#/components/schemas/nullableShortText" }, "line2": { "$ref": "#/components/schemas/nullableShortText" }, "postalCode": { "$ref": "#/components/schemas/nullableShortText" }, "state": { "$ref": "#/components/schemas/nullableShortText" } } }, "isPrimary": { "type": "boolean" }, "name": { "type": [ "string", "null" ], "maxLength": 120 } } }, "customerSchema": { "type": "object", "properties": { "address": { "$ref": "#/components/schemas/customerAddressSchema" }, "archivedAt": { "type": [ "string", "null" ], "format": "date-time" }, "archivedReason": { "type": [ "string", "null" ] }, "assignees": { "type": "array", "items": { "$ref": "#/components/schemas/customerAssigneeSchema" } }, "billingEmail": { "type": [ "string", "null" ] }, "billingName": { "type": [ "string", "null" ] }, "btw": { "type": [ "string", "null" ] }, "companyName": { "type": [ "string", "null" ] }, "createdAt": { "type": "string", "format": "date-time" }, "createdBy": { "type": [ "string", "null" ], "format": "uuid" }, "description": { "type": [ "string", "null" ] }, "id": { "$ref": "#/components/schemas/customerIdSchema" }, "isBusiness": { "type": "boolean" }, "kvk": { "type": [ "string", "null" ] }, "logoUrl": { "type": [ "string", "null" ], "format": "uri" }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "quoteCount": { "type": "integer", "minimum": 0 }, "status": { "$ref": "#/components/schemas/customerStatusSchema" }, "tags": { "type": "array", "items": { "$ref": "#/components/schemas/organizationTagSchema" } }, "taskCount": { "type": "integer", "minimum": 0 }, "updatedAt": { "type": "string", "format": "date-time" }, "updatedBy": { "type": [ "string", "null" ], "format": "uuid" }, "website": { "type": [ "string", "null" ] } }, "required": [ "address", "archivedAt", "archivedReason", "assignees", "billingEmail", "billingName", "btw", "companyName", "createdAt", "createdBy", "description", "id", "isBusiness", "kvk", "logoUrl", "organizationId", "quoteCount", "status", "tags", "taskCount", "updatedAt", "updatedBy", "website" ] }, "customerStatusSchema": { "type": "string", "enum": [ "prospect", "active", "archived" ] }, "nullableEmailInputSchema": { "type": [ "string", "null" ], "format": "email", "maxLength": 320 }, "nullableKvkNumberInputSchema": { "type": [ "string", "null" ], "maxLength": 40 }, "nullableLongText": { "type": [ "string", "null" ], "maxLength": 4000 }, "nullablePhoneInputSchema": { "type": [ "string", "null" ], "maxLength": 20 }, "nullableShortText": { "type": [ "string", "null" ], "maxLength": 240 }, "nullableUrlInputSchema": { "anyOf": [ { "type": "string", "maxLength": 400, "format": "uri" }, { "type": "null" } ] }, "nullableVatIdInputSchema": { "type": [ "string", "null" ], "maxLength": 40 }, "organizationIdSchema": { "type": "string", "format": "uuid" }, "organizationTagSchema": { "type": "object", "properties": { "color": { "anyOf": [ { "$ref": "#/components/schemas/tagColorSchema" }, { "type": "null" } ] }, "createdAt": { "type": "string", "format": "date-time" }, "description": { "type": [ "string", "null" ] }, "id": { "$ref": "#/components/schemas/tagIdSchema" }, "kind": { "$ref": "#/components/schemas/tagKindSchema" }, "name": { "type": "string", "minLength": 1, "maxLength": 80 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "position": { "type": "integer", "minimum": 0 }, "slug": { "$ref": "#/components/schemas/tagSlugSchema" }, "updatedAt": { "type": "string", "format": "date-time" } }, "required": [ "color", "createdAt", "description", "id", "kind", "name", "organizationId", "position", "slug", "updatedAt" ] }, "tagColorSchema": { "type": "string", "pattern": "^#(?:[0-9a-f]{3}|[0-9a-f]{6})$" }, "tagIdSchema": { "type": "string", "format": "uuid" }, "tagKindSchema": { "type": "string", "enum": [ "task", "customer", "product" ] }, "tagSlugSchema": { "type": "string", "minLength": 1, "maxLength": 160, "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$" } } ``` # Delete a customer. (/docs/openapi/deleteCustomer) Delete a customer for the specified organization. ## DELETE /v1/organizations/{organizationId}/customers/{customerId} ```json { "operationId": "deleteCustomer", "summary": "Delete a customer.", "description": "Delete a customer for the specified organization.", "tags": [ "Customers" ], "parameters": [ { "in": "path", "name": "customerId", "required": true, "schema": { "$ref": "#/components/schemas/customerIdSchema" }, "example": "123" }, { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "responses": { "200": { "description": "Returns a success payload when the customer is deleted.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/publicApiSuccessResponseSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "customerIdSchema": { "type": "string", "format": "uuid" }, "organizationIdSchema": { "type": "string", "format": "uuid" }, "publicApiSuccessResponseSchema": { "type": "object", "properties": { "success": { "type": "boolean", "enum": [ true ] } }, "required": [ "success" ] } } ``` # Add a customer contact. (/docs/openapi/addCustomerContact) Add a linked contact to a customer. ## POST /v1/organizations/{organizationId}/customers/{customerId}/contacts ```json { "operationId": "addCustomerContact", "summary": "Add a customer contact.", "description": "Add a linked contact to a customer.", "tags": [ "Customers" ], "parameters": [ { "in": "path", "name": "customerId", "required": true, "schema": { "type": "string", "format": "uuid" }, "example": "123e4567-e89b-12d3-a456-426614174000" }, { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/customerContactInputSchema" } } }, "description": "The customer contact payload." }, "responses": { "201": { "description": "Returns the created customer contact.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/customerContactSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "contactMethodSchema": { "type": "object", "properties": { "createdAt": { "type": "string", "format": "date-time" }, "id": { "type": "string", "format": "uuid" }, "isPrimary": { "type": "boolean" }, "label": { "type": [ "string", "null" ], "maxLength": 120 }, "position": { "type": "integer", "minimum": 0 }, "type": { "$ref": "#/components/schemas/contactMethodTypeSchema" }, "updatedAt": { "type": "string", "format": "date-time" }, "value": { "type": "string", "minLength": 1, "maxLength": 320 }, "verifiedAt": { "type": [ "string", "null" ], "format": "date-time" } }, "required": [ "createdAt", "id", "isPrimary", "label", "position", "type", "updatedAt", "value", "verifiedAt" ] }, "contactMethodTypeSchema": { "type": "string", "enum": [ "email", "phone", "whatsapp" ] }, "contactProfileIdSchema": { "type": "string", "format": "uuid" }, "contactProfileSchema": { "type": "object", "properties": { "avatarUrl": { "type": [ "string", "null" ] }, "contactMethods": { "type": "array", "items": { "$ref": "#/components/schemas/contactMethodSchema" } }, "createdAt": { "type": "string", "format": "date-time" }, "displayName": { "type": [ "string", "null" ] }, "firstName": { "type": [ "string", "null" ] }, "id": { "$ref": "#/components/schemas/contactProfileIdSchema" }, "lastName": { "type": [ "string", "null" ] }, "updatedAt": { "type": "string", "format": "date-time" }, "userId": { "type": [ "string", "null" ], "format": "uuid" } }, "required": [ "avatarUrl", "contactMethods", "createdAt", "displayName", "firstName", "id", "lastName", "updatedAt", "userId" ] }, "customerContactInputSchema": { "type": "object", "properties": { "email": { "allOf": [ { "$ref": "#/components/schemas/nullableEmailInputSchema" } ] }, "firstName": { "type": [ "string", "null" ], "maxLength": 120 }, "isPrimary": { "type": "boolean" }, "jobTitle": { "type": [ "string", "null" ], "maxLength": 120 }, "lastName": { "type": [ "string", "null" ], "maxLength": 120 }, "phone": { "allOf": [ { "$ref": "#/components/schemas/nullablePhoneInputSchema" } ] } } }, "customerContactSchema": { "type": "object", "properties": { "contactProfile": { "$ref": "#/components/schemas/contactProfileSchema" }, "createdAt": { "type": "string", "format": "date-time" }, "customerId": { "$ref": "#/components/schemas/customerIdSchema" }, "id": { "type": "string", "format": "uuid" }, "isPrimary": { "type": "boolean" }, "jobTitle": { "type": [ "string", "null" ] }, "notes": { "type": [ "string", "null" ] }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "updatedAt": { "type": "string", "format": "date-time" } }, "required": [ "contactProfile", "createdAt", "customerId", "id", "isPrimary", "jobTitle", "notes", "organizationId", "updatedAt" ] }, "customerIdSchema": { "type": "string", "format": "uuid" }, "nullableEmailInputSchema": { "type": [ "string", "null" ], "format": "email", "maxLength": 320 }, "nullablePhoneInputSchema": { "type": [ "string", "null" ], "maxLength": 20 }, "organizationIdSchema": { "type": "string", "format": "uuid" } } ``` # Add a customer location. (/docs/openapi/addCustomerLocation) Add a service or billing location to a customer. ## POST /v1/organizations/{organizationId}/customers/{customerId}/locations ```json { "operationId": "addCustomerLocation", "summary": "Add a customer location.", "description": "Add a service or billing location to a customer.", "tags": [ "Customers" ], "parameters": [ { "in": "path", "name": "customerId", "required": true, "schema": { "type": "string", "format": "uuid" }, "example": "123e4567-e89b-12d3-a456-426614174000" }, { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/customerLocationInputSchema" } } }, "description": "The customer location payload." }, "responses": { "201": { "description": "Returns the created customer location.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/customerLocationSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "customerAddressSchema": { "type": "object", "properties": { "city": { "$ref": "#/components/schemas/nullableShortText" }, "country": { "$ref": "#/components/schemas/nullableShortText" }, "line1": { "$ref": "#/components/schemas/nullableShortText" }, "line2": { "$ref": "#/components/schemas/nullableShortText" }, "postalCode": { "$ref": "#/components/schemas/nullableShortText" }, "state": { "$ref": "#/components/schemas/nullableShortText" } }, "required": [ "city", "country", "line1", "line2", "postalCode", "state" ] }, "customerIdSchema": { "type": "string", "format": "uuid" }, "customerLocationInputSchema": { "type": "object", "properties": { "address": { "type": "object", "properties": { "city": { "$ref": "#/components/schemas/nullableShortText" }, "country": { "$ref": "#/components/schemas/nullableShortText" }, "line1": { "$ref": "#/components/schemas/nullableShortText" }, "line2": { "$ref": "#/components/schemas/nullableShortText" }, "postalCode": { "$ref": "#/components/schemas/nullableShortText" }, "state": { "$ref": "#/components/schemas/nullableShortText" } } }, "isPrimary": { "type": "boolean" }, "name": { "type": [ "string", "null" ], "maxLength": 120 } } }, "customerLocationSchema": { "type": "object", "properties": { "address": { "$ref": "#/components/schemas/customerAddressSchema" }, "createdAt": { "type": "string", "format": "date-time" }, "customerId": { "$ref": "#/components/schemas/customerIdSchema" }, "id": { "type": "string", "format": "uuid" }, "isPrimary": { "type": "boolean" }, "name": { "type": "string", "minLength": 1, "maxLength": 120 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "updatedAt": { "type": "string", "format": "date-time" } }, "required": [ "address", "createdAt", "customerId", "id", "isPrimary", "name", "organizationId", "updatedAt" ] }, "nullableShortText": { "type": [ "string", "null" ], "maxLength": 240 }, "organizationIdSchema": { "type": "string", "format": "uuid" } } ``` # Add a customer note. (/docs/openapi/addCustomerNote) Add a note to a customer timeline. ## POST /v1/organizations/{organizationId}/customers/{customerId}/notes ```json { "operationId": "addCustomerNote", "summary": "Add a customer note.", "description": "Add a note to a customer timeline.", "tags": [ "Customers" ], "parameters": [ { "in": "path", "name": "customerId", "required": true, "schema": { "type": "string", "format": "uuid" }, "example": "123e4567-e89b-12d3-a456-426614174000" }, { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/customerNoteInputSchema" } } }, "description": "The customer note payload." }, "responses": { "201": { "description": "Returns the created customer note.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/customerNoteSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "collaborationBlockSchema": { "type": "object", "discriminator": { "propertyName": "type" }, "oneOf": [ { "type": "object", "properties": { "content": { "$ref": "#/components/schemas/collaborationInlineContentSchema" }, "type": { "type": "string", "enum": [ "paragraph" ] } }, "required": [ "content", "type" ], "additionalProperties": false }, { "type": "object", "properties": { "items": { "type": "array", "items": { "$ref": "#/components/schemas/collaborationInlineContentSchema" }, "minItems": 1, "maxItems": 500 }, "type": { "type": "string", "enum": [ "bullet-list" ] } }, "required": [ "items", "type" ], "additionalProperties": false }, { "type": "object", "properties": { "items": { "type": "array", "items": { "$ref": "#/components/schemas/collaborationInlineContentSchema" }, "minItems": 1, "maxItems": 500 }, "type": { "type": "string", "enum": [ "numbered-list" ] } }, "required": [ "items", "type" ], "additionalProperties": false } ] }, "collaborationCommentAuthorSchema": { "type": "object", "properties": { "avatarUrl": { "type": [ "string", "null" ], "format": "uri" }, "displayName": { "type": "string", "minLength": 1 }, "userId": { "type": "string", "format": "uuid" } }, "required": [ "avatarUrl", "displayName", "userId" ] }, "collaborationComposerInputSchema": { "type": "object", "properties": { "selections": { "type": "array", "items": { "$ref": "#/components/schemas/collaborationComposerSelectionSchema" }, "maxItems": 100 }, "text": { "type": "string", "minLength": 1, "maxLength": 20000 } }, "required": [ "selections", "text" ], "additionalProperties": false }, "collaborationComposerSelectionSchema": { "type": "object", "properties": { "end": { "type": "integer", "exclusiveMinimum": 0, "maximum": 20000 }, "reference": { "$ref": "#/components/schemas/collaborationReferenceIdSchema" }, "start": { "type": "integer", "minimum": 0, "maximum": 20000 } }, "required": [ "end", "reference", "start" ], "additionalProperties": false }, "collaborationDocumentSchema": { "type": "object", "properties": { "blocks": { "type": "array", "items": { "$ref": "#/components/schemas/collaborationBlockSchema" }, "minItems": 1, "maxItems": 1000 }, "version": { "type": "integer", "enum": [ 1 ] } }, "required": [ "blocks", "version" ], "additionalProperties": false }, "collaborationInlineContentSchema": { "type": "array", "items": { "$ref": "#/components/schemas/collaborationInlineSchema" }, "minItems": 1, "maxItems": 1000 }, "collaborationInlineSchema": { "type": "object", "discriminator": { "propertyName": "type" }, "oneOf": [ { "type": "object", "properties": { "text": { "type": "string", "minLength": 1, "maxLength": 20000 }, "type": { "type": "string", "enum": [ "text" ] } }, "required": [ "text", "type" ], "additionalProperties": false }, { "type": "object", "properties": { "reference": { "$ref": "#/components/schemas/collaborationReferenceSchema" }, "type": { "type": "string", "enum": [ "reference" ] } }, "required": [ "reference", "type" ], "additionalProperties": false } ] }, "collaborationReferenceIdSchema": { "type": "object", "discriminator": { "propertyName": "kind" }, "oneOf": [ { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "kind": { "type": "string", "enum": [ "user" ] } }, "required": [ "id", "kind" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "kind": { "type": "string", "enum": [ "team" ] } }, "required": [ "id", "kind" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "kind": { "type": "string", "enum": [ "task" ] } }, "required": [ "id", "kind" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "kind": { "type": "string", "enum": [ "customer" ] } }, "required": [ "id", "kind" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "$ref": "#/components/schemas/quoteIdSchema" }, "kind": { "type": "string", "enum": [ "quote" ] } }, "required": [ "id", "kind" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "$ref": "#/components/schemas/invoiceIdSchema" }, "kind": { "type": "string", "enum": [ "invoice" ] } }, "required": [ "id", "kind" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "$ref": "#/components/schemas/productIdSchema" }, "kind": { "type": "string", "enum": [ "product" ] } }, "required": [ "id", "kind" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "kind": { "type": "string", "enum": [ "customer-asset" ] } }, "required": [ "id", "kind" ], "additionalProperties": false } ] }, "collaborationReferenceSchema": { "type": "object", "discriminator": { "propertyName": "kind" }, "oneOf": [ { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "kind": { "type": "string", "enum": [ "user" ] }, "label": { "type": "string", "minLength": 1, "maxLength": 500 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "path": { "type": "string", "pattern": "^/", "maxLength": 2000 } }, "required": [ "id", "kind", "label", "organizationId", "path" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "kind": { "type": "string", "enum": [ "team" ] }, "label": { "type": "string", "minLength": 1, "maxLength": 500 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "path": { "type": "string", "pattern": "^/", "maxLength": 2000 } }, "required": [ "id", "kind", "label", "organizationId", "path" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "kind": { "type": "string", "enum": [ "task" ] }, "label": { "type": "string", "minLength": 1, "maxLength": 500 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "path": { "type": "string", "pattern": "^/", "maxLength": 2000 } }, "required": [ "id", "kind", "label", "organizationId", "path" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "kind": { "type": "string", "enum": [ "customer" ] }, "label": { "type": "string", "minLength": 1, "maxLength": 500 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "path": { "type": "string", "pattern": "^/", "maxLength": 2000 } }, "required": [ "id", "kind", "label", "organizationId", "path" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "$ref": "#/components/schemas/quoteIdSchema" }, "kind": { "type": "string", "enum": [ "quote" ] }, "label": { "type": "string", "minLength": 1, "maxLength": 500 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "path": { "type": "string", "pattern": "^/", "maxLength": 2000 } }, "required": [ "id", "kind", "label", "organizationId", "path" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "$ref": "#/components/schemas/invoiceIdSchema" }, "kind": { "type": "string", "enum": [ "invoice" ] }, "label": { "type": "string", "minLength": 1, "maxLength": 500 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "path": { "type": "string", "pattern": "^/", "maxLength": 2000 } }, "required": [ "id", "kind", "label", "organizationId", "path" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "$ref": "#/components/schemas/productIdSchema" }, "kind": { "type": "string", "enum": [ "product" ] }, "label": { "type": "string", "minLength": 1, "maxLength": 500 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "path": { "type": "string", "pattern": "^/", "maxLength": 2000 } }, "required": [ "id", "kind", "label", "organizationId", "path" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "kind": { "type": "string", "enum": [ "customer-asset" ] }, "label": { "type": "string", "minLength": 1, "maxLength": 500 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "path": { "type": "string", "pattern": "^/", "maxLength": 2000 } }, "required": [ "id", "kind", "label", "organizationId", "path" ], "additionalProperties": false } ] }, "customerIdSchema": { "type": "string", "format": "uuid" }, "customerNoteInputSchema": { "$ref": "#/components/schemas/collaborationComposerInputSchema" }, "customerNoteSchema": { "type": "object", "properties": { "author": { "anyOf": [ { "$ref": "#/components/schemas/collaborationCommentAuthorSchema" }, { "type": "null" } ] }, "contentDoc": { "$ref": "#/components/schemas/collaborationDocumentSchema" }, "contentText": { "type": "string" }, "createdAt": { "type": "string", "format": "date-time" }, "createdBy": { "type": [ "string", "null" ], "format": "uuid" }, "customerId": { "$ref": "#/components/schemas/customerIdSchema" }, "id": { "type": "string", "format": "uuid" }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "updatedAt": { "type": "string", "format": "date-time" } }, "required": [ "author", "contentDoc", "contentText", "createdAt", "createdBy", "customerId", "id", "organizationId", "updatedAt" ] }, "invoiceIdSchema": { "type": "string", "format": "uuid" }, "organizationIdSchema": { "type": "string", "format": "uuid" }, "productIdSchema": { "type": "string", "format": "uuid" }, "quoteIdSchema": { "type": "string", "format": "uuid" } } ``` # Get a customer contact. (/docs/openapi/getCustomerContact) Return a linked contact for a customer. ## GET /v1/organizations/{organizationId}/customers/{customerId}/contacts/{contactId} ```json { "operationId": "getCustomerContact", "summary": "Get a customer contact.", "description": "Return a linked contact for a customer.", "tags": [ "Customers" ], "parameters": [ { "in": "path", "name": "contactId", "required": true, "schema": { "type": "string", "format": "uuid" }, "example": "123e4567-e89b-12d3-a456-426614174000" }, { "in": "path", "name": "customerId", "required": true, "schema": { "type": "string", "format": "uuid" }, "example": "123e4567-e89b-12d3-a456-426614174000" }, { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "responses": { "200": { "description": "Returns the requested customer contact.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/customerContactSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "contactMethodSchema": { "type": "object", "properties": { "createdAt": { "type": "string", "format": "date-time" }, "id": { "type": "string", "format": "uuid" }, "isPrimary": { "type": "boolean" }, "label": { "type": [ "string", "null" ], "maxLength": 120 }, "position": { "type": "integer", "minimum": 0 }, "type": { "$ref": "#/components/schemas/contactMethodTypeSchema" }, "updatedAt": { "type": "string", "format": "date-time" }, "value": { "type": "string", "minLength": 1, "maxLength": 320 }, "verifiedAt": { "type": [ "string", "null" ], "format": "date-time" } }, "required": [ "createdAt", "id", "isPrimary", "label", "position", "type", "updatedAt", "value", "verifiedAt" ] }, "contactMethodTypeSchema": { "type": "string", "enum": [ "email", "phone", "whatsapp" ] }, "contactProfileIdSchema": { "type": "string", "format": "uuid" }, "contactProfileSchema": { "type": "object", "properties": { "avatarUrl": { "type": [ "string", "null" ] }, "contactMethods": { "type": "array", "items": { "$ref": "#/components/schemas/contactMethodSchema" } }, "createdAt": { "type": "string", "format": "date-time" }, "displayName": { "type": [ "string", "null" ] }, "firstName": { "type": [ "string", "null" ] }, "id": { "$ref": "#/components/schemas/contactProfileIdSchema" }, "lastName": { "type": [ "string", "null" ] }, "updatedAt": { "type": "string", "format": "date-time" }, "userId": { "type": [ "string", "null" ], "format": "uuid" } }, "required": [ "avatarUrl", "contactMethods", "createdAt", "displayName", "firstName", "id", "lastName", "updatedAt", "userId" ] }, "customerContactSchema": { "type": "object", "properties": { "contactProfile": { "$ref": "#/components/schemas/contactProfileSchema" }, "createdAt": { "type": "string", "format": "date-time" }, "customerId": { "$ref": "#/components/schemas/customerIdSchema" }, "id": { "type": "string", "format": "uuid" }, "isPrimary": { "type": "boolean" }, "jobTitle": { "type": [ "string", "null" ] }, "notes": { "type": [ "string", "null" ] }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "updatedAt": { "type": "string", "format": "date-time" } }, "required": [ "contactProfile", "createdAt", "customerId", "id", "isPrimary", "jobTitle", "notes", "organizationId", "updatedAt" ] }, "customerIdSchema": { "type": "string", "format": "uuid" }, "organizationIdSchema": { "type": "string", "format": "uuid" } } ``` # Update a customer contact. (/docs/openapi/updateCustomerContact) Update a linked contact for a customer. ## PATCH /v1/organizations/{organizationId}/customers/{customerId}/contacts/{contactId} ```json { "operationId": "updateCustomerContact", "summary": "Update a customer contact.", "description": "Update a linked contact for a customer.", "tags": [ "Customers" ], "parameters": [ { "in": "path", "name": "contactId", "required": true, "schema": { "type": "string", "format": "uuid" }, "example": "123e4567-e89b-12d3-a456-426614174000" }, { "in": "path", "name": "customerId", "required": true, "schema": { "type": "string", "format": "uuid" }, "example": "123e4567-e89b-12d3-a456-426614174000" }, { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/customerContactInputSchema" } } }, "description": "The customer contact payload." }, "responses": { "200": { "description": "Returns the updated customer contact.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/customerContactSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "contactMethodSchema": { "type": "object", "properties": { "createdAt": { "type": "string", "format": "date-time" }, "id": { "type": "string", "format": "uuid" }, "isPrimary": { "type": "boolean" }, "label": { "type": [ "string", "null" ], "maxLength": 120 }, "position": { "type": "integer", "minimum": 0 }, "type": { "$ref": "#/components/schemas/contactMethodTypeSchema" }, "updatedAt": { "type": "string", "format": "date-time" }, "value": { "type": "string", "minLength": 1, "maxLength": 320 }, "verifiedAt": { "type": [ "string", "null" ], "format": "date-time" } }, "required": [ "createdAt", "id", "isPrimary", "label", "position", "type", "updatedAt", "value", "verifiedAt" ] }, "contactMethodTypeSchema": { "type": "string", "enum": [ "email", "phone", "whatsapp" ] }, "contactProfileIdSchema": { "type": "string", "format": "uuid" }, "contactProfileSchema": { "type": "object", "properties": { "avatarUrl": { "type": [ "string", "null" ] }, "contactMethods": { "type": "array", "items": { "$ref": "#/components/schemas/contactMethodSchema" } }, "createdAt": { "type": "string", "format": "date-time" }, "displayName": { "type": [ "string", "null" ] }, "firstName": { "type": [ "string", "null" ] }, "id": { "$ref": "#/components/schemas/contactProfileIdSchema" }, "lastName": { "type": [ "string", "null" ] }, "updatedAt": { "type": "string", "format": "date-time" }, "userId": { "type": [ "string", "null" ], "format": "uuid" } }, "required": [ "avatarUrl", "contactMethods", "createdAt", "displayName", "firstName", "id", "lastName", "updatedAt", "userId" ] }, "customerContactInputSchema": { "type": "object", "properties": { "email": { "allOf": [ { "$ref": "#/components/schemas/nullableEmailInputSchema" } ] }, "firstName": { "type": [ "string", "null" ], "maxLength": 120 }, "isPrimary": { "type": "boolean" }, "jobTitle": { "type": [ "string", "null" ], "maxLength": 120 }, "lastName": { "type": [ "string", "null" ], "maxLength": 120 }, "phone": { "allOf": [ { "$ref": "#/components/schemas/nullablePhoneInputSchema" } ] } } }, "customerContactSchema": { "type": "object", "properties": { "contactProfile": { "$ref": "#/components/schemas/contactProfileSchema" }, "createdAt": { "type": "string", "format": "date-time" }, "customerId": { "$ref": "#/components/schemas/customerIdSchema" }, "id": { "type": "string", "format": "uuid" }, "isPrimary": { "type": "boolean" }, "jobTitle": { "type": [ "string", "null" ] }, "notes": { "type": [ "string", "null" ] }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "updatedAt": { "type": "string", "format": "date-time" } }, "required": [ "contactProfile", "createdAt", "customerId", "id", "isPrimary", "jobTitle", "notes", "organizationId", "updatedAt" ] }, "customerIdSchema": { "type": "string", "format": "uuid" }, "nullableEmailInputSchema": { "type": [ "string", "null" ], "format": "email", "maxLength": 320 }, "nullablePhoneInputSchema": { "type": [ "string", "null" ], "maxLength": 20 }, "organizationIdSchema": { "type": "string", "format": "uuid" } } ``` # Remove a customer contact. (/docs/openapi/removeCustomerContact) Remove a linked contact from a customer. ## DELETE /v1/organizations/{organizationId}/customers/{customerId}/contacts/{contactId} ```json { "operationId": "removeCustomerContact", "summary": "Remove a customer contact.", "description": "Remove a linked contact from a customer.", "tags": [ "Customers" ], "parameters": [ { "in": "path", "name": "contactId", "required": true, "schema": { "type": "string", "format": "uuid" }, "example": "123e4567-e89b-12d3-a456-426614174000" }, { "in": "path", "name": "customerId", "required": true, "schema": { "type": "string", "format": "uuid" }, "example": "123e4567-e89b-12d3-a456-426614174000" }, { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "responses": { "200": { "description": "Returns a success payload when the contact is removed.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/publicApiSuccessResponseSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "organizationIdSchema": { "type": "string", "format": "uuid" }, "publicApiSuccessResponseSchema": { "type": "object", "properties": { "success": { "type": "boolean", "enum": [ true ] } }, "required": [ "success" ] } } ``` # Get a customer location. (/docs/openapi/getCustomerLocation) Return a customer location. ## GET /v1/organizations/{organizationId}/customers/{customerId}/locations/{locationId} ```json { "operationId": "getCustomerLocation", "summary": "Get a customer location.", "description": "Return a customer location.", "tags": [ "Customers" ], "parameters": [ { "in": "path", "name": "customerId", "required": true, "schema": { "type": "string", "format": "uuid" }, "example": "123e4567-e89b-12d3-a456-426614174000" }, { "in": "path", "name": "locationId", "required": true, "schema": { "type": "string", "format": "uuid" }, "example": "123e4567-e89b-12d3-a456-426614174000" }, { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "responses": { "200": { "description": "Returns the requested customer location.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/customerLocationSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "customerAddressSchema": { "type": "object", "properties": { "city": { "$ref": "#/components/schemas/nullableShortText" }, "country": { "$ref": "#/components/schemas/nullableShortText" }, "line1": { "$ref": "#/components/schemas/nullableShortText" }, "line2": { "$ref": "#/components/schemas/nullableShortText" }, "postalCode": { "$ref": "#/components/schemas/nullableShortText" }, "state": { "$ref": "#/components/schemas/nullableShortText" } }, "required": [ "city", "country", "line1", "line2", "postalCode", "state" ] }, "customerIdSchema": { "type": "string", "format": "uuid" }, "customerLocationSchema": { "type": "object", "properties": { "address": { "$ref": "#/components/schemas/customerAddressSchema" }, "createdAt": { "type": "string", "format": "date-time" }, "customerId": { "$ref": "#/components/schemas/customerIdSchema" }, "id": { "type": "string", "format": "uuid" }, "isPrimary": { "type": "boolean" }, "name": { "type": "string", "minLength": 1, "maxLength": 120 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "updatedAt": { "type": "string", "format": "date-time" } }, "required": [ "address", "createdAt", "customerId", "id", "isPrimary", "name", "organizationId", "updatedAt" ] }, "nullableShortText": { "type": [ "string", "null" ], "maxLength": 240 }, "organizationIdSchema": { "type": "string", "format": "uuid" } } ``` # Update a customer location. (/docs/openapi/updateCustomerLocation) Update a customer location. ## PATCH /v1/organizations/{organizationId}/customers/{customerId}/locations/{locationId} ```json { "operationId": "updateCustomerLocation", "summary": "Update a customer location.", "description": "Update a customer location.", "tags": [ "Customers" ], "parameters": [ { "in": "path", "name": "customerId", "required": true, "schema": { "type": "string", "format": "uuid" }, "example": "123e4567-e89b-12d3-a456-426614174000" }, { "in": "path", "name": "locationId", "required": true, "schema": { "type": "string", "format": "uuid" }, "example": "123e4567-e89b-12d3-a456-426614174000" }, { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/customerLocationInputSchema" } } }, "description": "The customer location payload." }, "responses": { "200": { "description": "Returns the updated customer location.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/customerLocationSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "customerAddressSchema": { "type": "object", "properties": { "city": { "$ref": "#/components/schemas/nullableShortText" }, "country": { "$ref": "#/components/schemas/nullableShortText" }, "line1": { "$ref": "#/components/schemas/nullableShortText" }, "line2": { "$ref": "#/components/schemas/nullableShortText" }, "postalCode": { "$ref": "#/components/schemas/nullableShortText" }, "state": { "$ref": "#/components/schemas/nullableShortText" } }, "required": [ "city", "country", "line1", "line2", "postalCode", "state" ] }, "customerIdSchema": { "type": "string", "format": "uuid" }, "customerLocationInputSchema": { "type": "object", "properties": { "address": { "type": "object", "properties": { "city": { "$ref": "#/components/schemas/nullableShortText" }, "country": { "$ref": "#/components/schemas/nullableShortText" }, "line1": { "$ref": "#/components/schemas/nullableShortText" }, "line2": { "$ref": "#/components/schemas/nullableShortText" }, "postalCode": { "$ref": "#/components/schemas/nullableShortText" }, "state": { "$ref": "#/components/schemas/nullableShortText" } } }, "isPrimary": { "type": "boolean" }, "name": { "type": [ "string", "null" ], "maxLength": 120 } } }, "customerLocationSchema": { "type": "object", "properties": { "address": { "$ref": "#/components/schemas/customerAddressSchema" }, "createdAt": { "type": "string", "format": "date-time" }, "customerId": { "$ref": "#/components/schemas/customerIdSchema" }, "id": { "type": "string", "format": "uuid" }, "isPrimary": { "type": "boolean" }, "name": { "type": "string", "minLength": 1, "maxLength": 120 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "updatedAt": { "type": "string", "format": "date-time" } }, "required": [ "address", "createdAt", "customerId", "id", "isPrimary", "name", "organizationId", "updatedAt" ] }, "nullableShortText": { "type": [ "string", "null" ], "maxLength": 240 }, "organizationIdSchema": { "type": "string", "format": "uuid" } } ``` # Remove a customer location. (/docs/openapi/removeCustomerLocation) Remove a customer location. ## DELETE /v1/organizations/{organizationId}/customers/{customerId}/locations/{locationId} ```json { "operationId": "removeCustomerLocation", "summary": "Remove a customer location.", "description": "Remove a customer location.", "tags": [ "Customers" ], "parameters": [ { "in": "path", "name": "customerId", "required": true, "schema": { "type": "string", "format": "uuid" }, "example": "123e4567-e89b-12d3-a456-426614174000" }, { "in": "path", "name": "locationId", "required": true, "schema": { "type": "string", "format": "uuid" }, "example": "123e4567-e89b-12d3-a456-426614174000" }, { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "responses": { "200": { "description": "Returns a success payload when the location is removed.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/publicApiSuccessResponseSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "organizationIdSchema": { "type": "string", "format": "uuid" }, "publicApiSuccessResponseSchema": { "type": "object", "properties": { "success": { "type": "boolean", "enum": [ true ] } }, "required": [ "success" ] } } ``` # Update a customer note. (/docs/openapi/updateCustomerNote) Update a note on a customer timeline. ## PATCH /v1/organizations/{organizationId}/customers/{customerId}/notes/{noteId} ```json { "operationId": "updateCustomerNote", "summary": "Update a customer note.", "description": "Update a note on a customer timeline.", "tags": [ "Customers" ], "parameters": [ { "in": "path", "name": "customerId", "required": true, "schema": { "type": "string", "format": "uuid" }, "example": "123e4567-e89b-12d3-a456-426614174000" }, { "in": "path", "name": "noteId", "required": true, "schema": { "type": "string", "format": "uuid" }, "example": "123e4567-e89b-12d3-a456-426614174000" }, { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/customerNoteInputSchema" } } }, "description": "The customer note payload." }, "responses": { "200": { "description": "Returns the updated customer note.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/customerNoteSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "collaborationBlockSchema": { "type": "object", "discriminator": { "propertyName": "type" }, "oneOf": [ { "type": "object", "properties": { "content": { "$ref": "#/components/schemas/collaborationInlineContentSchema" }, "type": { "type": "string", "enum": [ "paragraph" ] } }, "required": [ "content", "type" ], "additionalProperties": false }, { "type": "object", "properties": { "items": { "type": "array", "items": { "$ref": "#/components/schemas/collaborationInlineContentSchema" }, "minItems": 1, "maxItems": 500 }, "type": { "type": "string", "enum": [ "bullet-list" ] } }, "required": [ "items", "type" ], "additionalProperties": false }, { "type": "object", "properties": { "items": { "type": "array", "items": { "$ref": "#/components/schemas/collaborationInlineContentSchema" }, "minItems": 1, "maxItems": 500 }, "type": { "type": "string", "enum": [ "numbered-list" ] } }, "required": [ "items", "type" ], "additionalProperties": false } ] }, "collaborationCommentAuthorSchema": { "type": "object", "properties": { "avatarUrl": { "type": [ "string", "null" ], "format": "uri" }, "displayName": { "type": "string", "minLength": 1 }, "userId": { "type": "string", "format": "uuid" } }, "required": [ "avatarUrl", "displayName", "userId" ] }, "collaborationComposerInputSchema": { "type": "object", "properties": { "selections": { "type": "array", "items": { "$ref": "#/components/schemas/collaborationComposerSelectionSchema" }, "maxItems": 100 }, "text": { "type": "string", "minLength": 1, "maxLength": 20000 } }, "required": [ "selections", "text" ], "additionalProperties": false }, "collaborationComposerSelectionSchema": { "type": "object", "properties": { "end": { "type": "integer", "exclusiveMinimum": 0, "maximum": 20000 }, "reference": { "$ref": "#/components/schemas/collaborationReferenceIdSchema" }, "start": { "type": "integer", "minimum": 0, "maximum": 20000 } }, "required": [ "end", "reference", "start" ], "additionalProperties": false }, "collaborationDocumentSchema": { "type": "object", "properties": { "blocks": { "type": "array", "items": { "$ref": "#/components/schemas/collaborationBlockSchema" }, "minItems": 1, "maxItems": 1000 }, "version": { "type": "integer", "enum": [ 1 ] } }, "required": [ "blocks", "version" ], "additionalProperties": false }, "collaborationInlineContentSchema": { "type": "array", "items": { "$ref": "#/components/schemas/collaborationInlineSchema" }, "minItems": 1, "maxItems": 1000 }, "collaborationInlineSchema": { "type": "object", "discriminator": { "propertyName": "type" }, "oneOf": [ { "type": "object", "properties": { "text": { "type": "string", "minLength": 1, "maxLength": 20000 }, "type": { "type": "string", "enum": [ "text" ] } }, "required": [ "text", "type" ], "additionalProperties": false }, { "type": "object", "properties": { "reference": { "$ref": "#/components/schemas/collaborationReferenceSchema" }, "type": { "type": "string", "enum": [ "reference" ] } }, "required": [ "reference", "type" ], "additionalProperties": false } ] }, "collaborationReferenceIdSchema": { "type": "object", "discriminator": { "propertyName": "kind" }, "oneOf": [ { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "kind": { "type": "string", "enum": [ "user" ] } }, "required": [ "id", "kind" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "kind": { "type": "string", "enum": [ "team" ] } }, "required": [ "id", "kind" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "kind": { "type": "string", "enum": [ "task" ] } }, "required": [ "id", "kind" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "kind": { "type": "string", "enum": [ "customer" ] } }, "required": [ "id", "kind" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "$ref": "#/components/schemas/quoteIdSchema" }, "kind": { "type": "string", "enum": [ "quote" ] } }, "required": [ "id", "kind" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "$ref": "#/components/schemas/invoiceIdSchema" }, "kind": { "type": "string", "enum": [ "invoice" ] } }, "required": [ "id", "kind" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "$ref": "#/components/schemas/productIdSchema" }, "kind": { "type": "string", "enum": [ "product" ] } }, "required": [ "id", "kind" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "kind": { "type": "string", "enum": [ "customer-asset" ] } }, "required": [ "id", "kind" ], "additionalProperties": false } ] }, "collaborationReferenceSchema": { "type": "object", "discriminator": { "propertyName": "kind" }, "oneOf": [ { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "kind": { "type": "string", "enum": [ "user" ] }, "label": { "type": "string", "minLength": 1, "maxLength": 500 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "path": { "type": "string", "pattern": "^/", "maxLength": 2000 } }, "required": [ "id", "kind", "label", "organizationId", "path" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "kind": { "type": "string", "enum": [ "team" ] }, "label": { "type": "string", "minLength": 1, "maxLength": 500 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "path": { "type": "string", "pattern": "^/", "maxLength": 2000 } }, "required": [ "id", "kind", "label", "organizationId", "path" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "kind": { "type": "string", "enum": [ "task" ] }, "label": { "type": "string", "minLength": 1, "maxLength": 500 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "path": { "type": "string", "pattern": "^/", "maxLength": 2000 } }, "required": [ "id", "kind", "label", "organizationId", "path" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "kind": { "type": "string", "enum": [ "customer" ] }, "label": { "type": "string", "minLength": 1, "maxLength": 500 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "path": { "type": "string", "pattern": "^/", "maxLength": 2000 } }, "required": [ "id", "kind", "label", "organizationId", "path" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "$ref": "#/components/schemas/quoteIdSchema" }, "kind": { "type": "string", "enum": [ "quote" ] }, "label": { "type": "string", "minLength": 1, "maxLength": 500 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "path": { "type": "string", "pattern": "^/", "maxLength": 2000 } }, "required": [ "id", "kind", "label", "organizationId", "path" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "$ref": "#/components/schemas/invoiceIdSchema" }, "kind": { "type": "string", "enum": [ "invoice" ] }, "label": { "type": "string", "minLength": 1, "maxLength": 500 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "path": { "type": "string", "pattern": "^/", "maxLength": 2000 } }, "required": [ "id", "kind", "label", "organizationId", "path" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "$ref": "#/components/schemas/productIdSchema" }, "kind": { "type": "string", "enum": [ "product" ] }, "label": { "type": "string", "minLength": 1, "maxLength": 500 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "path": { "type": "string", "pattern": "^/", "maxLength": 2000 } }, "required": [ "id", "kind", "label", "organizationId", "path" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "kind": { "type": "string", "enum": [ "customer-asset" ] }, "label": { "type": "string", "minLength": 1, "maxLength": 500 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "path": { "type": "string", "pattern": "^/", "maxLength": 2000 } }, "required": [ "id", "kind", "label", "organizationId", "path" ], "additionalProperties": false } ] }, "customerIdSchema": { "type": "string", "format": "uuid" }, "customerNoteInputSchema": { "$ref": "#/components/schemas/collaborationComposerInputSchema" }, "customerNoteSchema": { "type": "object", "properties": { "author": { "anyOf": [ { "$ref": "#/components/schemas/collaborationCommentAuthorSchema" }, { "type": "null" } ] }, "contentDoc": { "$ref": "#/components/schemas/collaborationDocumentSchema" }, "contentText": { "type": "string" }, "createdAt": { "type": "string", "format": "date-time" }, "createdBy": { "type": [ "string", "null" ], "format": "uuid" }, "customerId": { "$ref": "#/components/schemas/customerIdSchema" }, "id": { "type": "string", "format": "uuid" }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "updatedAt": { "type": "string", "format": "date-time" } }, "required": [ "author", "contentDoc", "contentText", "createdAt", "createdBy", "customerId", "id", "organizationId", "updatedAt" ] }, "invoiceIdSchema": { "type": "string", "format": "uuid" }, "organizationIdSchema": { "type": "string", "format": "uuid" }, "productIdSchema": { "type": "string", "format": "uuid" }, "quoteIdSchema": { "type": "string", "format": "uuid" } } ``` # Delete a customer note. (/docs/openapi/deleteCustomerNote) Delete a note from a customer timeline. ## DELETE /v1/organizations/{organizationId}/customers/{customerId}/notes/{noteId} ```json { "operationId": "deleteCustomerNote", "summary": "Delete a customer note.", "description": "Delete a note from a customer timeline.", "tags": [ "Customers" ], "parameters": [ { "in": "path", "name": "customerId", "required": true, "schema": { "type": "string", "format": "uuid" }, "example": "123e4567-e89b-12d3-a456-426614174000" }, { "in": "path", "name": "noteId", "required": true, "schema": { "type": "string", "format": "uuid" }, "example": "123e4567-e89b-12d3-a456-426614174000" }, { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "responses": { "200": { "description": "Returns a success payload when the note is deleted.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/publicApiSuccessResponseSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "organizationIdSchema": { "type": "string", "format": "uuid" }, "publicApiSuccessResponseSchema": { "type": "object", "properties": { "success": { "type": "boolean", "enum": [ true ] } }, "required": [ "success" ] } } ``` # Set primary customer contact. (/docs/openapi/setPrimaryCustomerContact) Mark a linked contact as the customer primary contact. ## POST /v1/organizations/{organizationId}/customers/{customerId}/contacts/{contactId}/primary ```json { "operationId": "setPrimaryCustomerContact", "summary": "Set primary customer contact.", "description": "Mark a linked contact as the customer primary contact.", "tags": [ "Customers" ], "parameters": [ { "in": "path", "name": "contactId", "required": true, "schema": { "type": "string", "format": "uuid" }, "example": "123e4567-e89b-12d3-a456-426614174000" }, { "in": "path", "name": "customerId", "required": true, "schema": { "type": "string", "format": "uuid" }, "example": "123e4567-e89b-12d3-a456-426614174000" }, { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "responses": { "200": { "description": "Returns the primary customer contact.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/customerContactSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "contactMethodSchema": { "type": "object", "properties": { "createdAt": { "type": "string", "format": "date-time" }, "id": { "type": "string", "format": "uuid" }, "isPrimary": { "type": "boolean" }, "label": { "type": [ "string", "null" ], "maxLength": 120 }, "position": { "type": "integer", "minimum": 0 }, "type": { "$ref": "#/components/schemas/contactMethodTypeSchema" }, "updatedAt": { "type": "string", "format": "date-time" }, "value": { "type": "string", "minLength": 1, "maxLength": 320 }, "verifiedAt": { "type": [ "string", "null" ], "format": "date-time" } }, "required": [ "createdAt", "id", "isPrimary", "label", "position", "type", "updatedAt", "value", "verifiedAt" ] }, "contactMethodTypeSchema": { "type": "string", "enum": [ "email", "phone", "whatsapp" ] }, "contactProfileIdSchema": { "type": "string", "format": "uuid" }, "contactProfileSchema": { "type": "object", "properties": { "avatarUrl": { "type": [ "string", "null" ] }, "contactMethods": { "type": "array", "items": { "$ref": "#/components/schemas/contactMethodSchema" } }, "createdAt": { "type": "string", "format": "date-time" }, "displayName": { "type": [ "string", "null" ] }, "firstName": { "type": [ "string", "null" ] }, "id": { "$ref": "#/components/schemas/contactProfileIdSchema" }, "lastName": { "type": [ "string", "null" ] }, "updatedAt": { "type": "string", "format": "date-time" }, "userId": { "type": [ "string", "null" ], "format": "uuid" } }, "required": [ "avatarUrl", "contactMethods", "createdAt", "displayName", "firstName", "id", "lastName", "updatedAt", "userId" ] }, "customerContactSchema": { "type": "object", "properties": { "contactProfile": { "$ref": "#/components/schemas/contactProfileSchema" }, "createdAt": { "type": "string", "format": "date-time" }, "customerId": { "$ref": "#/components/schemas/customerIdSchema" }, "id": { "type": "string", "format": "uuid" }, "isPrimary": { "type": "boolean" }, "jobTitle": { "type": [ "string", "null" ] }, "notes": { "type": [ "string", "null" ] }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "updatedAt": { "type": "string", "format": "date-time" } }, "required": [ "contactProfile", "createdAt", "customerId", "id", "isPrimary", "jobTitle", "notes", "organizationId", "updatedAt" ] }, "customerIdSchema": { "type": "string", "format": "uuid" }, "organizationIdSchema": { "type": "string", "format": "uuid" } } ``` # Set primary customer location. (/docs/openapi/setPrimaryCustomerLocation) Mark a location as the customer's primary location. ## POST /v1/organizations/{organizationId}/customers/{customerId}/locations/{locationId}/primary ```json { "operationId": "setPrimaryCustomerLocation", "summary": "Set primary customer location.", "description": "Mark a location as the customer's primary location.", "tags": [ "Customers" ], "parameters": [ { "in": "path", "name": "customerId", "required": true, "schema": { "type": "string", "format": "uuid" }, "example": "123e4567-e89b-12d3-a456-426614174000" }, { "in": "path", "name": "locationId", "required": true, "schema": { "type": "string", "format": "uuid" }, "example": "123e4567-e89b-12d3-a456-426614174000" }, { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "responses": { "200": { "description": "Returns the primary customer location.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/customerLocationSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "customerAddressSchema": { "type": "object", "properties": { "city": { "$ref": "#/components/schemas/nullableShortText" }, "country": { "$ref": "#/components/schemas/nullableShortText" }, "line1": { "$ref": "#/components/schemas/nullableShortText" }, "line2": { "$ref": "#/components/schemas/nullableShortText" }, "postalCode": { "$ref": "#/components/schemas/nullableShortText" }, "state": { "$ref": "#/components/schemas/nullableShortText" } }, "required": [ "city", "country", "line1", "line2", "postalCode", "state" ] }, "customerIdSchema": { "type": "string", "format": "uuid" }, "customerLocationSchema": { "type": "object", "properties": { "address": { "$ref": "#/components/schemas/customerAddressSchema" }, "createdAt": { "type": "string", "format": "date-time" }, "customerId": { "$ref": "#/components/schemas/customerIdSchema" }, "id": { "type": "string", "format": "uuid" }, "isPrimary": { "type": "boolean" }, "name": { "type": "string", "minLength": 1, "maxLength": 120 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "updatedAt": { "type": "string", "format": "date-time" } }, "required": [ "address", "createdAt", "customerId", "id", "isPrimary", "name", "organizationId", "updatedAt" ] }, "nullableShortText": { "type": [ "string", "null" ], "maxLength": 240 }, "organizationIdSchema": { "type": "string", "format": "uuid" } } ``` # List expense claims. (/docs/openapi/listExpenses) Return expense claims for a scope: the caller's own, everything, awaiting the caller's approval, or ready to reimburse. ## GET /v1/organizations/{organizationId}/expenses ```json { "operationId": "listExpenses", "summary": "List expense claims.", "description": "Return expense claims for a scope: the caller's own, everything, awaiting the caller's approval, or ready to reimburse.", "tags": [ "Expenses" ], "parameters": [ { "in": "query", "name": "employeeId", "required": false, "schema": { "type": "string", "format": "uuid" }, "example": "123e4567-e89b-12d3-a456-426614174000" }, { "in": "query", "name": "from", "required": false, "schema": { "type": "string", "format": "date" }, "example": "example" }, { "in": "query", "name": "limit", "required": true, "schema": { "default": 50, "type": "integer", "minimum": 1, "maximum": 100 }, "example": 1 }, { "in": "query", "name": "offset", "required": true, "schema": { "default": 0, "type": "integer", "minimum": 0, "maximum": 9007199254740991 }, "example": 1 }, { "in": "query", "name": "q", "required": false, "schema": { "type": "string", "maxLength": 200 }, "example": "example" }, { "in": "query", "name": "scope", "required": true, "schema": { "type": "string", "enum": [ "mine", "all", "to-approve", "to-reimburse" ], "default": "mine" }, "example": "mine" }, { "in": "query", "name": "status", "required": false, "schema": { "allOf": [ { "$ref": "#/components/schemas/expenseStatusSchema" } ] }, "example": "example" }, { "in": "query", "name": "to", "required": false, "schema": { "type": "string", "format": "date" }, "example": "example" }, { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "responses": { "200": { "description": "Returns expense claims.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/expensesResponseSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "customerIdSchema": { "type": "string", "format": "uuid" }, "expenseFlagKindSchema": { "type": "string", "enum": [ "limit_exceeded", "receipt_missing", "duplicate", "per_diem_overlap" ] }, "expenseFlagSchema": { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "kind": { "$ref": "#/components/schemas/expenseFlagKindSchema" }, "message": { "type": [ "string", "null" ] }, "relatedExpenseId": { "type": [ "string", "null" ], "format": "uuid" }, "resolvedAt": { "type": [ "string", "null" ] } }, "required": [ "id", "kind", "message", "relatedExpenseId", "resolvedAt" ] }, "expenseKindSchema": { "type": "string", "enum": [ "company", "claim" ] }, "expensePaymentMethodSchema": { "type": "string", "enum": [ "company_card", "out_of_pocket", "mileage", "per_diem" ] }, "expensePersonSchema": { "type": "object", "properties": { "displayName": { "type": "string" }, "employeeId": { "type": "string", "format": "uuid" }, "userId": { "type": [ "string", "null" ], "format": "uuid" } }, "required": [ "displayName", "employeeId", "userId" ] }, "expenseReceiptStatusSchema": { "type": "string", "enum": [ "attached", "missing", "not_required" ] }, "expenseSchema": { "type": "object", "properties": { "amount": { "type": "number" }, "amountBase": { "type": "number" }, "billable": { "type": "boolean" }, "canDecide": { "type": "boolean" }, "canEdit": { "type": "boolean" }, "categoryId": { "type": [ "string", "null" ], "format": "uuid" }, "categoryName": { "type": [ "string", "null" ] }, "createdAt": { "type": "string" }, "currency": { "type": "string" }, "customerId": { "anyOf": [ { "$ref": "#/components/schemas/customerIdSchema" }, { "type": "null" } ] }, "customerName": { "type": [ "string", "null" ] }, "decidedAt": { "type": [ "string", "null" ] }, "decisionNote": { "type": [ "string", "null" ] }, "description": { "type": [ "string", "null" ] }, "distanceKm": { "type": [ "number", "null" ] }, "employee": { "anyOf": [ { "$ref": "#/components/schemas/expensePersonSchema" }, { "type": "null" } ] }, "exchangeRate": { "type": "number" }, "extractionConfidence": { "type": [ "number", "null" ] }, "flags": { "type": "array", "items": { "$ref": "#/components/schemas/expenseFlagSchema" } }, "id": { "type": "string", "format": "uuid" }, "invoiceId": { "type": [ "string", "null" ], "format": "uuid" }, "kind": { "$ref": "#/components/schemas/expenseKindSchema" }, "merchant": { "type": [ "string", "null" ] }, "mileageRate": { "type": [ "number", "null" ] }, "paymentMethod": { "$ref": "#/components/schemas/expensePaymentMethodSchema" }, "perDiemDays": { "type": [ "number", "null" ] }, "receiptFileName": { "type": [ "string", "null" ] }, "receiptStatus": { "$ref": "#/components/schemas/expenseReceiptStatusSchema" }, "reimbursedAt": { "type": [ "string", "null" ] }, "reimbursementReference": { "type": [ "string", "null" ] }, "roundTrip": { "type": "boolean" }, "routeFrom": { "type": [ "string", "null" ] }, "routeTo": { "type": [ "string", "null" ] }, "source": { "$ref": "#/components/schemas/expenseSourceSchema" }, "spentOn": { "$ref": "#/components/schemas/isoDateSchema" }, "status": { "$ref": "#/components/schemas/expenseStatusSchema" }, "submittedAt": { "type": [ "string", "null" ] }, "taskId": { "type": [ "string", "null" ], "format": "uuid" }, "taskTitle": { "type": [ "string", "null" ] }, "tripId": { "type": [ "string", "null" ], "format": "uuid" }, "tripName": { "type": [ "string", "null" ] }, "vatAmount": { "type": [ "number", "null" ] }, "vatRate": { "type": [ "number", "null" ] } }, "required": [ "amount", "amountBase", "billable", "canDecide", "canEdit", "categoryId", "categoryName", "createdAt", "currency", "customerId", "customerName", "decidedAt", "decisionNote", "description", "distanceKm", "employee", "exchangeRate", "extractionConfidence", "flags", "id", "invoiceId", "kind", "merchant", "mileageRate", "paymentMethod", "perDiemDays", "receiptFileName", "receiptStatus", "reimbursedAt", "reimbursementReference", "roundTrip", "routeFrom", "routeTo", "source", "spentOn", "status", "submittedAt", "taskId", "taskTitle", "tripId", "tripName", "vatAmount", "vatRate" ] }, "expenseSourceSchema": { "type": "string", "enum": [ "manual", "email", "photo", "card_feed" ] }, "expenseStatusSchema": { "type": "string", "enum": [ "draft", "submitted", "approved", "rejected", "reimbursed" ] }, "expensesResponseSchema": { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/components/schemas/expenseSchema" } }, "pagination": { "$ref": "#/components/schemas/listPaginationSchema" } }, "required": [ "data", "pagination" ] }, "isoDateSchema": { "type": "string", "format": "date" }, "listPaginationSchema": { "type": "object", "properties": { "hasMore": { "type": "boolean" }, "limit": { "type": "integer", "minimum": 1, "maximum": 100 }, "offset": { "type": "integer", "minimum": 0 }, "total": { "type": "integer", "minimum": 0 }, "totalIsEstimate": { "type": "boolean" } }, "required": [ "hasMore", "limit", "offset", "total" ] }, "organizationIdSchema": { "type": "string", "format": "uuid" } } ``` # Create an expense claim. (/docs/openapi/createExpense) Create a draft claim, mileage entry, or per diem. Upload receipts in the app; this endpoint accepts JSON only. ## POST /v1/organizations/{organizationId}/expenses ```json { "operationId": "createExpense", "summary": "Create an expense claim.", "description": "Create a draft claim, mileage entry, or per diem. Upload receipts in the app; this endpoint accepts JSON only.", "tags": [ "Expenses" ], "parameters": [ { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/expenseInputSchema" } } }, "description": "The expense claim." }, "responses": { "201": { "description": "Returns the created claim.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/expenseSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "customerIdSchema": { "type": "string", "format": "uuid" }, "expenseFlagKindSchema": { "type": "string", "enum": [ "limit_exceeded", "receipt_missing", "duplicate", "per_diem_overlap" ] }, "expenseFlagSchema": { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "kind": { "$ref": "#/components/schemas/expenseFlagKindSchema" }, "message": { "type": [ "string", "null" ] }, "relatedExpenseId": { "type": [ "string", "null" ], "format": "uuid" }, "resolvedAt": { "type": [ "string", "null" ] } }, "required": [ "id", "kind", "message", "relatedExpenseId", "resolvedAt" ] }, "expenseInputSchema": { "type": "object", "properties": { "amount": { "type": "number", "minimum": 0, "maximum": 999999999, "default": 0 }, "billable": { "type": "boolean", "default": false }, "categoryId": { "type": [ "string", "null" ], "format": "uuid" }, "currency": { "type": "string", "pattern": "^[A-Z]{3}$", "default": "EUR" }, "customerId": { "anyOf": [ { "$ref": "#/components/schemas/customerIdSchema" }, { "type": "null" } ] }, "description": { "type": [ "string", "null" ], "maxLength": 2000 }, "distanceKm": { "type": [ "number", "null" ], "exclusiveMinimum": 0, "maximum": 20000 }, "employeeId": { "type": [ "string", "null" ], "format": "uuid" }, "exchangeRate": { "type": "number", "exclusiveMinimum": 0, "maximum": 1000000, "default": 1 }, "extractionConfidence": { "type": [ "number", "null" ], "minimum": 0, "maximum": 1 }, "kind": { "type": "string", "enum": [ "company", "claim" ], "default": "claim" }, "merchant": { "type": [ "string", "null" ], "maxLength": 200 }, "paymentMethod": { "type": "string", "enum": [ "company_card", "out_of_pocket", "mileage", "per_diem" ], "default": "out_of_pocket" }, "perDiemDays": { "type": [ "number", "null" ], "exclusiveMinimum": 0, "maximum": 366 }, "roundTrip": { "type": "boolean", "default": false }, "routeFrom": { "type": [ "string", "null" ], "maxLength": 300 }, "routeTo": { "type": [ "string", "null" ], "maxLength": 300 }, "source": { "type": "string", "enum": [ "manual", "email", "photo", "card_feed" ], "default": "manual" }, "spentOn": { "$ref": "#/components/schemas/isoDateSchema" }, "taskId": { "type": [ "string", "null" ], "format": "uuid" }, "tripId": { "type": [ "string", "null" ], "format": "uuid" }, "vatAmount": { "type": [ "number", "null" ], "minimum": 0 }, "vatRate": { "type": [ "number", "null" ], "minimum": 0, "maximum": 100 } }, "required": [ "amount", "billable", "currency", "description", "exchangeRate", "kind", "merchant", "paymentMethod", "roundTrip", "routeFrom", "routeTo", "source", "spentOn" ] }, "expenseKindSchema": { "type": "string", "enum": [ "company", "claim" ] }, "expensePaymentMethodSchema": { "type": "string", "enum": [ "company_card", "out_of_pocket", "mileage", "per_diem" ] }, "expensePersonSchema": { "type": "object", "properties": { "displayName": { "type": "string" }, "employeeId": { "type": "string", "format": "uuid" }, "userId": { "type": [ "string", "null" ], "format": "uuid" } }, "required": [ "displayName", "employeeId", "userId" ] }, "expenseReceiptStatusSchema": { "type": "string", "enum": [ "attached", "missing", "not_required" ] }, "expenseSchema": { "type": "object", "properties": { "amount": { "type": "number" }, "amountBase": { "type": "number" }, "billable": { "type": "boolean" }, "canDecide": { "type": "boolean" }, "canEdit": { "type": "boolean" }, "categoryId": { "type": [ "string", "null" ], "format": "uuid" }, "categoryName": { "type": [ "string", "null" ] }, "createdAt": { "type": "string" }, "currency": { "type": "string" }, "customerId": { "anyOf": [ { "$ref": "#/components/schemas/customerIdSchema" }, { "type": "null" } ] }, "customerName": { "type": [ "string", "null" ] }, "decidedAt": { "type": [ "string", "null" ] }, "decisionNote": { "type": [ "string", "null" ] }, "description": { "type": [ "string", "null" ] }, "distanceKm": { "type": [ "number", "null" ] }, "employee": { "anyOf": [ { "$ref": "#/components/schemas/expensePersonSchema" }, { "type": "null" } ] }, "exchangeRate": { "type": "number" }, "extractionConfidence": { "type": [ "number", "null" ] }, "flags": { "type": "array", "items": { "$ref": "#/components/schemas/expenseFlagSchema" } }, "id": { "type": "string", "format": "uuid" }, "invoiceId": { "type": [ "string", "null" ], "format": "uuid" }, "kind": { "$ref": "#/components/schemas/expenseKindSchema" }, "merchant": { "type": [ "string", "null" ] }, "mileageRate": { "type": [ "number", "null" ] }, "paymentMethod": { "$ref": "#/components/schemas/expensePaymentMethodSchema" }, "perDiemDays": { "type": [ "number", "null" ] }, "receiptFileName": { "type": [ "string", "null" ] }, "receiptStatus": { "$ref": "#/components/schemas/expenseReceiptStatusSchema" }, "reimbursedAt": { "type": [ "string", "null" ] }, "reimbursementReference": { "type": [ "string", "null" ] }, "roundTrip": { "type": "boolean" }, "routeFrom": { "type": [ "string", "null" ] }, "routeTo": { "type": [ "string", "null" ] }, "source": { "$ref": "#/components/schemas/expenseSourceSchema" }, "spentOn": { "$ref": "#/components/schemas/isoDateSchema" }, "status": { "$ref": "#/components/schemas/expenseStatusSchema" }, "submittedAt": { "type": [ "string", "null" ] }, "taskId": { "type": [ "string", "null" ], "format": "uuid" }, "taskTitle": { "type": [ "string", "null" ] }, "tripId": { "type": [ "string", "null" ], "format": "uuid" }, "tripName": { "type": [ "string", "null" ] }, "vatAmount": { "type": [ "number", "null" ] }, "vatRate": { "type": [ "number", "null" ] } }, "required": [ "amount", "amountBase", "billable", "canDecide", "canEdit", "categoryId", "categoryName", "createdAt", "currency", "customerId", "customerName", "decidedAt", "decisionNote", "description", "distanceKm", "employee", "exchangeRate", "extractionConfidence", "flags", "id", "invoiceId", "kind", "merchant", "mileageRate", "paymentMethod", "perDiemDays", "receiptFileName", "receiptStatus", "reimbursedAt", "reimbursementReference", "roundTrip", "routeFrom", "routeTo", "source", "spentOn", "status", "submittedAt", "taskId", "taskTitle", "tripId", "tripName", "vatAmount", "vatRate" ] }, "expenseSourceSchema": { "type": "string", "enum": [ "manual", "email", "photo", "card_feed" ] }, "expenseStatusSchema": { "type": "string", "enum": [ "draft", "submitted", "approved", "rejected", "reimbursed" ] }, "isoDateSchema": { "type": "string", "format": "date" }, "organizationIdSchema": { "type": "string", "format": "uuid" } } ``` # Download a public shared file by token. (/docs/openapi/downloadPublicSharedFile) Redirects a valid bearer-token file link to its signed download URL. ## GET /v1/public/files/{token} ```json { "operationId": "downloadPublicSharedFile", "summary": "Download a public shared file by token.", "description": "Redirects a valid bearer-token file link to its signed download URL.", "tags": [ "Files" ], "parameters": [ { "in": "path", "name": "token", "required": true, "schema": { "type": "string", "minLength": 32 }, "example": "example" } ], "responses": { "302": { "description": "Redirects to the signed file download URL.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/publicFileRedirectResponseSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "publicFileRedirectResponseSchema": { "type": "string", "format": "uri" } } ``` # Copy Files nodes recursively. (/docs/openapi/copyFileNodes) Transactionally clone selected folder trees and resume current-version Storage copies through a durable idempotent operation. ## POST /v1/organizations/{organizationId}/files/copy ```json { "operationId": "copyFileNodes", "summary": "Copy Files nodes recursively.", "description": "Transactionally clone selected folder trees and resume current-version Storage copies through a durable idempotent operation.", "tags": [ "Files" ], "parameters": [ { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/fileCopyBodySchema" } } } }, "responses": { "200": { "description": "Returns the durable copy operation and progress.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/fileCopyOperationResponseSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "fileCopyBodySchema": { "type": "object", "properties": { "conflictStrategy": { "type": "string", "enum": [ "fail", "rename" ], "default": "rename" }, "destinationDriveId": { "type": "string", "format": "uuid" }, "destinationParentId": { "$ref": "#/components/schemas/nullableUuidSchema" }, "idempotencyKey": { "type": "string", "minLength": 1, "maxLength": 200 }, "nodeIds": { "type": "array", "items": { "type": "string", "format": "uuid" }, "minItems": 1, "maxItems": 200 } }, "required": [ "conflictStrategy", "destinationDriveId", "destinationParentId", "idempotencyKey", "nodeIds" ] }, "fileCopyOperationResponseSchema": { "type": "object", "properties": { "completedAt": { "type": [ "string", "null" ] }, "completedItems": { "type": "integer", "minimum": 0 }, "createdAt": { "type": "string" }, "destinationDriveId": { "type": "string", "format": "uuid" }, "destinationParentId": { "type": [ "string", "null" ], "format": "uuid" }, "errorMessage": { "type": [ "string", "null" ] }, "id": { "type": "string", "format": "uuid" }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "sourceNodeIds": { "type": "array", "items": { "type": "string", "format": "uuid" } }, "status": { "type": "string", "enum": [ "metadata-ready", "copying", "completed", "failed" ] }, "totalItems": { "type": "integer", "minimum": 0 }, "updatedAt": { "type": "string" } }, "required": [ "completedAt", "completedItems", "createdAt", "destinationDriveId", "destinationParentId", "errorMessage", "id", "organizationId", "sourceNodeIds", "status", "totalItems", "updatedAt" ] }, "nullableUuidSchema": { "type": [ "string", "null" ], "format": "uuid" }, "organizationIdSchema": { "type": "string", "format": "uuid" } } ``` # List accessible Files drives. (/docs/openapi/listFileDrives) Returns company, customer, personal, and shared drives visible to the caller through inherited Files access. ## GET /v1/organizations/{organizationId}/files/drives ```json { "operationId": "listFileDrives", "summary": "List accessible Files drives.", "description": "Returns company, customer, personal, and shared drives visible to the caller through inherited Files access.", "tags": [ "Files" ], "parameters": [ { "in": "query", "name": "driveType", "required": false, "schema": { "allOf": [ { "$ref": "#/components/schemas/fileDriveTypeSchema" } ] }, "example": "example" }, { "in": "query", "name": "includeArchived", "required": true, "schema": { "anyOf": [ { "type": "boolean" }, { "type": "boolean" } ], "default": false }, "example": "example" }, { "in": "query", "name": "sortBy", "required": true, "schema": { "type": "string", "enum": [ "driveType", "name", "createdAt", "updatedAt" ], "default": "name" }, "example": "driveType" }, { "in": "query", "name": "sortDirection", "required": true, "schema": { "type": "string", "enum": [ "asc", "desc" ], "default": "asc" }, "example": "asc" }, { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "responses": { "200": { "description": "Returns a bounded page of accessible Files drives.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/fileDrivesHttpResponseSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "fileDriveTypeSchema": { "type": "string", "enum": [ "company", "personal", "shared", "customers" ] }, "fileDrivesHttpResponseSchema": { "type": "object" }, "organizationIdSchema": { "type": "string", "format": "uuid" } } ``` # Create a direct resumable Files upload. (/docs/openapi/createFileUpload) Reserve organization storage quota and return a short-lived signed TUS transport. File bytes are uploaded directly to Storage. ## POST /v1/organizations/{organizationId}/files/uploads ```json { "operationId": "createFileUpload", "summary": "Create a direct resumable Files upload.", "description": "Reserve organization storage quota and return a short-lived signed TUS transport. File bytes are uploaded directly to Storage.", "tags": [ "Files" ], "parameters": [ { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/fileUploadCreateBodySchema" } } }, "description": "File metadata and destination for the upload reservation.", "required": true }, "responses": { "201": { "description": "Returns the signed direct Storage upload transport.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/fileUploadTransportResponseSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "fileUploadCreateBodySchema": { "type": "object", "properties": { "driveId": { "type": "string", "format": "uuid" }, "expectedBytes": { "type": "integer", "exclusiveMinimum": 0 }, "fileName": { "type": "string", "minLength": 1, "maxLength": 240 }, "idempotencyKey": { "type": "string", "minLength": 1, "maxLength": 200 }, "mediaType": { "type": "string", "minLength": 1, "maxLength": 240 }, "nodeId": { "type": [ "string", "null" ], "format": "uuid", "default": null }, "parentId": { "type": [ "string", "null" ], "format": "uuid", "default": null } }, "required": [ "driveId", "expectedBytes", "fileName", "idempotencyKey", "mediaType", "nodeId", "parentId" ] }, "fileUploadTransportResponseSchema": { "type": "object", "properties": { "bucket": { "type": "string", "minLength": 1, "maxLength": 80 }, "expiresAt": { "type": "string" }, "path": { "type": "string", "minLength": 1, "maxLength": 1024 }, "reservationId": { "type": "string", "format": "uuid" }, "token": { "type": "string", "minLength": 1 }, "tusEndpoint": { "type": "string", "format": "uri" } }, "required": [ "bucket", "expiresAt", "path", "reservationId", "token", "tusEndpoint" ] }, "organizationIdSchema": { "type": "string", "format": "uuid" } } ``` # Download selected Files as ZIP. (/docs/openapi/createFileZipExport) Streams small authorized selections immediately and queues larger selections as expiring private artifacts. ## POST /v1/organizations/{organizationId}/files/zip-exports ```json { "operationId": "createFileZipExport", "summary": "Download selected Files as ZIP.", "description": "Streams small authorized selections immediately and queues larger selections as expiring private artifacts.", "tags": [ "Files" ], "parameters": [ { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/fileZipExportBodySchema" } } } }, "responses": { "202": { "description": "Returns an asynchronous ZIP job when the direct streaming threshold is exceeded. Small selections return application/zip.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/fileZipExportResponseSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "fileZipExportBodySchema": { "type": "object", "properties": { "fileName": { "type": "string", "minLength": 1, "maxLength": 240 }, "idempotencyKey": { "type": "string", "minLength": 1, "maxLength": 200 }, "nodeIds": { "type": "array", "items": { "type": "string", "format": "uuid" }, "minItems": 1, "maxItems": 200 } }, "required": [ "fileName", "idempotencyKey", "nodeIds" ] }, "fileZipExportResponseSchema": { "type": "object", "properties": { "completedItems": { "type": "integer", "minimum": 0 }, "deliveryMode": { "type": "string", "enum": [ "direct", "async" ] }, "downloadUrl": { "type": [ "string", "null" ], "format": "uri" }, "errorMessage": { "type": [ "string", "null" ] }, "expiresAt": { "type": [ "string", "null" ] }, "fileName": { "type": "string" }, "id": { "type": "string", "format": "uuid" }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "status": { "type": "string", "enum": [ "queued", "processing", "ready", "failed", "expired" ] }, "totalBytes": { "type": "integer", "minimum": 0 }, "totalItems": { "type": "integer", "minimum": 0 } }, "required": [ "completedItems", "deliveryMode", "downloadUrl", "errorMessage", "expiresAt", "fileName", "id", "organizationId", "status", "totalBytes", "totalItems" ] }, "organizationIdSchema": { "type": "string", "format": "uuid" } } ``` # Get a Files node. (/docs/openapi/getFileNode) Returns safe file or folder metadata after authoritative inherited node access checks. ## GET /v1/organizations/{organizationId}/files/nodes/{nodeId} ```json { "operationId": "getFileNode", "summary": "Get a Files node.", "description": "Returns safe file or folder metadata after authoritative inherited node access checks.", "tags": [ "Files" ], "parameters": [ { "in": "path", "name": "nodeId", "required": true, "schema": { "type": "string", "format": "uuid" }, "example": "123e4567-e89b-12d3-a456-426614174000" }, { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "responses": { "200": { "description": "Returns the accessible Files node without Storage object details.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/fileNodeHttpSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "fileDateTimeSchema": { "type": "string" }, "fileNodeHttpSchema": { "type": "object", "properties": { "createdAt": { "$ref": "#/components/schemas/fileDateTimeSchema" }, "createdBy": { "$ref": "#/components/schemas/nullableUuidSchema" }, "currentVersion": { "type": [ "object", "null" ], "properties": { "byteSize": { "type": "integer", "minimum": 0 }, "mediaType": { "type": "string", "minLength": 1, "maxLength": 240 } }, "required": [ "byteSize", "mediaType" ] }, "customerId": { "type": [ "string", "null" ], "format": "uuid" }, "deletedAt": { "anyOf": [ { "$ref": "#/components/schemas/fileDateTimeSchema" }, { "type": "null" } ] }, "driveId": { "type": "string", "format": "uuid" }, "id": { "type": "string", "format": "uuid" }, "kind": { "$ref": "#/components/schemas/fileNodeKindSchema" }, "name": { "type": "string", "minLength": 1, "maxLength": 240 }, "organizationId": { "type": "string", "format": "uuid" }, "parentId": { "$ref": "#/components/schemas/nullableUuidSchema" }, "source": { "$ref": "#/components/schemas/fileNodeSourceSchema" }, "updatedAt": { "$ref": "#/components/schemas/fileDateTimeSchema" }, "userState": { "anyOf": [ { "$ref": "#/components/schemas/fileNodeUserStateSchema" }, { "type": "null" } ] } }, "required": [ "createdAt", "createdBy", "deletedAt", "driveId", "id", "kind", "name", "organizationId", "parentId", "source", "updatedAt" ] }, "fileNodeKindSchema": { "type": "string", "enum": [ "folder", "file" ] }, "fileNodeSourceSchema": { "type": "string", "enum": [ "internal", "google-drive", "onedrive" ] }, "fileNodeUserStateSchema": { "type": "object", "properties": { "lastOpenedAt": { "anyOf": [ { "$ref": "#/components/schemas/fileDateTimeSchema" }, { "type": "null" } ] }, "nodeId": { "type": "string", "format": "uuid" }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "starredAt": { "anyOf": [ { "$ref": "#/components/schemas/fileDateTimeSchema" }, { "type": "null" } ] }, "updatedAt": { "$ref": "#/components/schemas/fileDateTimeSchema" }, "userId": { "type": "string", "format": "uuid" } }, "required": [ "lastOpenedAt", "nodeId", "organizationId", "starredAt", "updatedAt", "userId" ] }, "nullableUuidSchema": { "type": [ "string", "null" ], "format": "uuid" }, "organizationIdSchema": { "type": "string", "format": "uuid" } } ``` # Get a Files ZIP export. (/docs/openapi/getFileZipExport) Poll an asynchronous ZIP job. Ready jobs include a short-lived signed URL only for the authorized requester. ## GET /v1/organizations/{organizationId}/files/zip-exports/{exportId} ```json { "operationId": "getFileZipExport", "summary": "Get a Files ZIP export.", "description": "Poll an asynchronous ZIP job. Ready jobs include a short-lived signed URL only for the authorized requester.", "tags": [ "Files" ], "parameters": [ { "in": "path", "name": "exportId", "required": true, "schema": { "type": "string", "format": "uuid" }, "example": "123e4567-e89b-12d3-a456-426614174000" }, { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "responses": { "200": { "description": "Returns ZIP job progress and an authorized signed URL when ready.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/fileZipExportStatusResponseSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "fileZipExportStatusResponseSchema": { "type": "object", "properties": { "completedItems": { "type": "integer", "minimum": 0 }, "deliveryMode": { "type": "string", "enum": [ "direct", "async" ] }, "downloadUrl": { "type": [ "string", "null" ], "format": "uri" }, "errorMessage": { "type": [ "string", "null" ] }, "expiresAt": { "type": [ "string", "null" ] }, "fileName": { "type": "string" }, "id": { "type": "string", "format": "uuid" }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "status": { "type": "string", "enum": [ "queued", "processing", "ready", "failed", "expired" ] }, "totalBytes": { "type": "integer", "minimum": 0 }, "totalItems": { "type": "integer", "minimum": 0 } }, "required": [ "completedItems", "deliveryMode", "downloadUrl", "errorMessage", "expiresAt", "fileName", "id", "organizationId", "status", "totalBytes", "totalItems" ] }, "organizationIdSchema": { "type": "string", "format": "uuid" } } ``` # List Files node children. (/docs/openapi/listFileNodeChildren) Returns an authorized page of direct children for a folder, including explicitly requested trash visibility. ## GET /v1/organizations/{organizationId}/files/nodes/{nodeId}/children ```json { "operationId": "listFileNodeChildren", "summary": "List Files node children.", "description": "Returns an authorized page of direct children for a folder, including explicitly requested trash visibility.", "tags": [ "Files" ], "parameters": [ { "in": "query", "name": "kind", "required": false, "schema": { "allOf": [ { "$ref": "#/components/schemas/fileNodeKindSchema" } ] }, "example": "example" }, { "in": "query", "name": "sortBy", "required": true, "schema": { "type": "string", "enum": [ "kind", "name", "createdAt", "updatedAt" ], "default": "name" }, "example": "kind" }, { "in": "query", "name": "sortDirection", "required": true, "schema": { "type": "string", "enum": [ "asc", "desc" ], "default": "asc" }, "example": "asc" }, { "in": "query", "name": "trash", "required": true, "schema": { "type": "string", "enum": [ "exclude", "only" ], "default": "exclude" }, "example": "exclude" }, { "in": "path", "name": "nodeId", "required": true, "schema": { "type": "string", "format": "uuid" }, "example": "123e4567-e89b-12d3-a456-426614174000" }, { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "responses": { "200": { "description": "Returns a bounded page of accessible direct child nodes.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/fileNodeChildrenHttpResponseSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "fileNodeChildrenHttpResponseSchema": { "type": "object" }, "fileNodeKindSchema": { "type": "string", "enum": [ "folder", "file" ] }, "organizationIdSchema": { "type": "string", "format": "uuid" } } ``` # Create a Files download URL. (/docs/openapi/createFileDownload) Authorizes the file node and returns a short-lived signed download URL without exposing its Storage object path. ## GET /v1/organizations/{organizationId}/files/nodes/{nodeId}/download ```json { "operationId": "createFileDownload", "summary": "Create a Files download URL.", "description": "Authorizes the file node and returns a short-lived signed download URL without exposing its Storage object path.", "tags": [ "Files" ], "parameters": [ { "in": "path", "name": "nodeId", "required": true, "schema": { "type": "string", "format": "uuid" }, "example": "123e4567-e89b-12d3-a456-426614174000" }, { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "responses": { "200": { "description": "Returns a short-lived signed URL and expiry metadata for the file.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/fileDownloadHttpSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "fileDateTimeSchema": { "type": "string" }, "fileDownloadHttpSchema": { "type": "object", "properties": { "expiresAt": { "$ref": "#/components/schemas/fileDateTimeSchema" }, "fileName": { "type": "string", "minLength": 1 }, "mediaType": { "type": [ "string", "null" ] }, "url": { "type": "string", "format": "uri" } }, "required": [ "expiresAt", "fileName", "mediaType", "url" ] }, "organizationIdSchema": { "type": "string", "format": "uuid" } } ``` # Cancel a direct resumable Files upload. (/docs/openapi/cancelFileUpload) Release the upload reservation and delete its Storage object when present. ## POST /v1/organizations/{organizationId}/files/uploads/{reservationId}/cancel ```json { "operationId": "cancelFileUpload", "summary": "Cancel a direct resumable Files upload.", "description": "Release the upload reservation and delete its Storage object when present.", "tags": [ "Files" ], "parameters": [ { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" }, { "in": "path", "name": "reservationId", "required": true, "schema": { "type": "string", "format": "uuid" }, "example": "123e4567-e89b-12d3-a456-426614174000" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "responses": { "200": { "description": "Confirms the upload was cancelled.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/publicApiSuccessResponseSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "organizationIdSchema": { "type": "string", "format": "uuid" }, "publicApiSuccessResponseSchema": { "type": "object", "properties": { "success": { "type": "boolean", "enum": [ true ] } }, "required": [ "success" ] } } ``` # Finalize a direct resumable Files upload. (/docs/openapi/finalizeFileUpload) Verify the uploaded Storage object, its byte size, and detectable file signature before atomically creating the Files node version. ## POST /v1/organizations/{organizationId}/files/uploads/{reservationId}/finalize ```json { "operationId": "finalizeFileUpload", "summary": "Finalize a direct resumable Files upload.", "description": "Verify the uploaded Storage object, its byte size, and detectable file signature before atomically creating the Files node version.", "tags": [ "Files" ], "parameters": [ { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" }, { "in": "path", "name": "reservationId", "required": true, "schema": { "type": "string", "format": "uuid" }, "example": "123e4567-e89b-12d3-a456-426614174000" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "responses": { "200": { "description": "Returns the finalized upload reservation.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/fileUploadFinalizeResponseSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "fileUploadFinalizeResponseSchema": { "type": "object", "properties": { "actualBytes": { "type": [ "integer", "null" ], "minimum": 0 }, "checksum": { "type": [ "string", "null" ] }, "createdAt": { "type": "string" }, "createdBy": { "type": "string", "format": "uuid" }, "driveId": { "type": "string", "format": "uuid" }, "etag": { "type": [ "string", "null" ] }, "expectedBytes": { "type": "integer", "exclusiveMinimum": 0 }, "expiresAt": { "type": "string" }, "fileName": { "type": "string" }, "finalizedAt": { "type": [ "string", "null" ] }, "finalizedVersionId": { "type": [ "string", "null" ], "format": "uuid" }, "id": { "type": "string", "format": "uuid" }, "idempotencyKey": { "type": "string" }, "mediaType": { "type": "string" }, "nodeId": { "type": [ "string", "null" ], "format": "uuid" }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "parentId": { "type": [ "string", "null" ], "format": "uuid" }, "status": { "type": "string", "enum": [ "reserved", "uploaded", "finalized", "canceled", "expired", "failed" ] }, "storageBucket": { "type": "string" }, "storagePath": { "type": "string" }, "updatedAt": { "type": "string" } }, "required": [ "actualBytes", "checksum", "createdAt", "createdBy", "driveId", "etag", "expectedBytes", "expiresAt", "fileName", "finalizedAt", "finalizedVersionId", "id", "idempotencyKey", "mediaType", "nodeId", "organizationId", "parentId", "status", "storageBucket", "storagePath", "updatedAt" ] }, "organizationIdSchema": { "type": "string", "format": "uuid" } } ``` # Versioned health check for the API app. (/docs/openapi/getApiHealthV1) Return the versioned health status for the API app. ## GET /v1/health ```json { "operationId": "getApiHealthV1", "summary": "Versioned health check for the API app.", "description": "Return the versioned health status for the API app.", "tags": [ "Health" ], "parameters": [], "responses": { "200": { "description": "Returns the API service health.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/healthResponseSchema" } } } } } } ``` ## Referenced schemas ```json { "healthResponseSchema": { "type": "object", "properties": { "ok": { "type": "boolean", "enum": [ true ] }, "service": { "type": "string", "enum": [ "api" ] }, "version": { "type": "string", "enum": [ "v1" ] } }, "required": [ "ok", "service", "version" ] } } ``` # List jobs. (/docs/openapi/listHiringJobs) Return hiring jobs (vacancies) for an organization. ## GET /v1/organizations/{organizationId}/hiring/jobs ```json { "operationId": "listHiringJobs", "summary": "List jobs.", "description": "Return hiring jobs (vacancies) for an organization.", "tags": [ "Hiring" ], "parameters": [ { "in": "query", "name": "status", "required": false, "schema": { "allOf": [ { "$ref": "#/components/schemas/hiringJobStatusSchema" } ] }, "example": "example" }, { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "responses": { "200": { "description": "Returns jobs.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/hiringJobsResponseSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "employeeContractTypeSchema": { "type": "string", "enum": [ "permanent", "fixed_term", "on_call", "freelance" ] }, "employeeKindSchema": { "type": "string", "enum": [ "employee", "contractor", "intern" ] }, "hiringJobSchema": { "type": "object", "properties": { "activeApplicationCount": { "type": "integer" }, "closedAt": { "type": [ "string", "null" ] }, "contractType": { "anyOf": [ { "$ref": "#/components/schemas/employeeContractTypeSchema" }, { "type": "null" } ] }, "createdAt": { "type": "string" }, "currency": { "type": "string" }, "description": { "type": [ "string", "null" ] }, "employmentKind": { "$ref": "#/components/schemas/employeeKindSchema" }, "hiringManager": { "anyOf": [ { "$ref": "#/components/schemas/personSummarySchema" }, { "type": "null" } ] }, "hoursPerWeek": { "type": [ "number", "null" ] }, "id": { "type": "string", "format": "uuid" }, "location": { "type": [ "string", "null" ] }, "openedAt": { "type": [ "string", "null" ] }, "publishedOnCareersPage": { "type": "boolean" }, "salaryMax": { "type": [ "number", "null" ] }, "salaryMin": { "type": [ "number", "null" ] }, "slug": { "type": "string" }, "status": { "$ref": "#/components/schemas/hiringJobStatusSchema" }, "teamId": { "anyOf": [ { "$ref": "#/components/schemas/teamIdSchema" }, { "type": "null" } ] }, "title": { "type": "string" } }, "required": [ "activeApplicationCount", "closedAt", "contractType", "createdAt", "currency", "description", "employmentKind", "hiringManager", "hoursPerWeek", "id", "location", "openedAt", "publishedOnCareersPage", "salaryMax", "salaryMin", "slug", "status", "teamId", "title" ] }, "hiringJobStatusSchema": { "type": "string", "enum": [ "draft", "open", "closed" ] }, "hiringJobsResponseSchema": { "type": "array", "items": { "$ref": "#/components/schemas/hiringJobSchema" } }, "organizationIdSchema": { "type": "string", "format": "uuid" }, "personSummarySchema": { "type": "object", "properties": { "avatarUrl": { "type": [ "string", "null" ] }, "displayName": { "type": "string" }, "employeeId": { "type": "string", "format": "uuid" }, "jobTitle": { "type": [ "string", "null" ] }, "userId": { "type": [ "string", "null" ], "format": "uuid" } }, "required": [ "avatarUrl", "displayName", "employeeId", "jobTitle", "userId" ] }, "teamIdSchema": { "type": "string", "format": "uuid" } } ``` # List applications for a job. (/docs/openapi/listHiringApplications) Return candidate applications for a job with their current stage and average rating. ## GET /v1/organizations/{organizationId}/hiring/jobs/{jobId}/applications ```json { "operationId": "listHiringApplications", "summary": "List applications for a job.", "description": "Return candidate applications for a job with their current stage and average rating.", "tags": [ "Hiring" ], "parameters": [ { "in": "query", "name": "status", "required": false, "schema": { "allOf": [ { "$ref": "#/components/schemas/hiringApplicationStatusSchema" } ] }, "example": "example" }, { "in": "path", "name": "jobId", "required": true, "schema": { "type": "string", "format": "uuid" }, "example": "123e4567-e89b-12d3-a456-426614174000" }, { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "responses": { "200": { "description": "Returns applications.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/hiringApplicationsResponseSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "appLocaleSchema": { "type": "string", "enum": [ "en", "nl", "de" ] }, "hiringApplicationSchema": { "type": "object", "properties": { "averageRating": { "type": [ "number", "null" ] }, "candidate": { "$ref": "#/components/schemas/hiringCandidateSchema" }, "coverLetter": { "type": [ "string", "null" ] }, "createdAt": { "type": "string" }, "daysInStage": { "type": "integer" }, "hiredAt": { "type": [ "string", "null" ] }, "id": { "type": "string", "format": "uuid" }, "isStalled": { "type": "boolean" }, "jobId": { "type": "string", "format": "uuid" }, "jobTitle": { "type": "string" }, "nextInterviewAt": { "type": [ "string", "null" ] }, "position": { "type": "number" }, "rejectionReason": { "type": [ "string", "null" ] }, "scorecardCount": { "type": "integer" }, "source": { "$ref": "#/components/schemas/hiringApplicationSourceSchema" }, "stageEnteredAt": { "type": "string" }, "stageId": { "type": "string", "format": "uuid" }, "stageName": { "type": "string" }, "stageSystemKey": { "anyOf": [ { "$ref": "#/components/schemas/hiringStageSystemKeySchema" }, { "type": "null" } ] }, "status": { "$ref": "#/components/schemas/hiringApplicationStatusSchema" } }, "required": [ "averageRating", "candidate", "coverLetter", "createdAt", "daysInStage", "hiredAt", "id", "isStalled", "jobId", "jobTitle", "nextInterviewAt", "position", "rejectionReason", "scorecardCount", "source", "stageEnteredAt", "stageId", "stageName", "stageSystemKey", "status" ] }, "hiringApplicationSourceSchema": { "type": "string", "enum": [ "careers_page", "manual", "referral", "agency" ] }, "hiringApplicationStatusSchema": { "type": "string", "enum": [ "active", "rejected", "hired", "withdrawn" ] }, "hiringApplicationsResponseSchema": { "type": "array", "items": { "$ref": "#/components/schemas/hiringApplicationSchema" } }, "hiringCandidateSchema": { "type": "object", "properties": { "anonymizedAt": { "type": [ "string", "null" ] }, "consentGivenAt": { "type": [ "string", "null" ] }, "createdAt": { "type": "string" }, "cvExperienceYears": { "type": [ "number", "null" ] }, "cvFileName": { "type": [ "string", "null" ] }, "cvSkills": { "type": "array", "items": { "type": "string" } }, "cvSummary": { "type": [ "string", "null" ] }, "displayName": { "type": "string" }, "email": { "type": [ "string", "null" ] }, "employeeId": { "type": [ "string", "null" ], "format": "uuid" }, "firstName": { "type": "string" }, "hasCv": { "type": "boolean" }, "id": { "type": "string", "format": "uuid" }, "lastName": { "type": [ "string", "null" ] }, "linkedinUrl": { "type": [ "string", "null" ] }, "locale": { "anyOf": [ { "$ref": "#/components/schemas/hiringLocaleSchema" }, { "type": "null" } ] }, "phone": { "type": [ "string", "null" ] }, "retentionUntil": { "type": "object" }, "talentPoolConsentUntil": { "type": "object" } }, "required": [ "anonymizedAt", "consentGivenAt", "createdAt", "cvExperienceYears", "cvFileName", "cvSkills", "cvSummary", "displayName", "email", "employeeId", "firstName", "hasCv", "id", "lastName", "linkedinUrl", "locale", "phone", "retentionUntil", "talentPoolConsentUntil" ] }, "hiringLocaleSchema": { "$ref": "#/components/schemas/appLocaleSchema" }, "hiringStageSystemKeySchema": { "type": "string", "enum": [ "applied", "screening", "interview", "offer", "hired" ] }, "organizationIdSchema": { "type": "string", "format": "uuid" } } ``` # Upload an inbox attachment. (/docs/openapi/uploadInboxAttachment) ## POST /v1/organizations/{organizationId}/inbox/attachments ```json { "operationId": "uploadInboxAttachment", "summary": "Upload an inbox attachment.", "description": "", "tags": [ "Inbox" ], "parameters": [ { "in": "path", "name": "organizationId", "required": true, "schema": { "type": "string", "format": "uuid" }, "example": "123e4567-e89b-12d3-a456-426614174000" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "requestBody": { "content": { "multipart/form-data": { "schema": { "$ref": "#/components/schemas/inboxAttachmentUploadSchema" }, "encoding": { "file": { "contentType": "application/octet-stream" } } } }, "description": "Multipart inbox attachment and target external thread id.", "required": true }, "responses": { "200": { "description": "Returns the recorded attachment metadata.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/inboxAttachmentUploadResponseSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "inboxAttachmentUploadResponseSchema": { "type": "object", "properties": { "attachmentId": { "type": "string", "format": "uuid" }, "mediaType": { "type": "string", "minLength": 1 }, "name": { "type": "string", "minLength": 1 } }, "required": [ "attachmentId", "mediaType", "name" ] }, "inboxAttachmentUploadSchema": { "type": "object", "properties": { "file": { "type": "string", "contentMediaType": "application/octet-stream" }, "threadId": { "type": "string", "format": "uuid" } }, "required": [ "file", "threadId" ] } } ``` # List inbox threads. (/docs/openapi/listInboxThreads) ## GET /v1/organizations/{organizationId}/inbox/threads ```json { "operationId": "listInboxThreads", "summary": "List inbox threads.", "description": "", "tags": [ "Inbox" ], "parameters": [ { "in": "query", "name": "limit", "required": true, "schema": { "type": "integer", "minimum": 1, "maximum": 100, "default": 30 }, "example": 1 }, { "in": "query", "name": "offset", "required": true, "schema": { "default": 0, "type": "integer", "minimum": 0, "maximum": 9007199254740991 }, "example": 1 }, { "in": "query", "name": "q", "required": false, "schema": { "type": "string", "minLength": 1, "maxLength": 120 }, "example": "example" }, { "in": "path", "name": "organizationId", "required": true, "schema": { "type": "string", "format": "uuid" }, "example": "123e4567-e89b-12d3-a456-426614174000" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "responses": { "200": { "description": "Returns inbox threads and the unread count.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/inboxThreadsResponseSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "channelConnectionStatusSchema": { "type": "string", "enum": [ "draft", "active", "disabled", "error" ] }, "channelDeliveryStatusSchema": { "type": "string", "enum": [ "queued", "sent", "delivered", "failed", "dead_lettered", "canceled" ] }, "channelInboxThreadSchema": { "type": "object", "properties": { "assignedTeamId": { "type": [ "string", "null" ], "format": "uuid", "default": null }, "assignedUserId": { "type": [ "string", "null" ], "format": "uuid", "default": null }, "chatThreadId": { "type": [ "string", "null" ], "format": "uuid" }, "connectionId": { "type": "string", "format": "uuid" }, "continuationCursor": { "type": [ "string", "null" ], "maxLength": 500, "default": null }, "createdAt": { "$ref": "#/components/schemas/dateTimeSchema" }, "customerId": { "type": [ "string", "null" ], "format": "uuid" }, "externalActorId": { "type": [ "string", "null" ], "minLength": 1, "maxLength": 240 }, "externalChannelId": { "type": [ "string", "null" ], "minLength": 1, "maxLength": 240 }, "externalThreadId": { "type": "string", "minLength": 1, "maxLength": 240 }, "id": { "type": "string", "format": "uuid" }, "lastInboundMessageId": { "type": [ "string", "null" ], "minLength": 1, "maxLength": 240, "default": null }, "manuallyUnread": { "type": "boolean", "default": false }, "metadata": { "$ref": "#/components/schemas/metadataSchema" }, "organizationId": { "type": "string", "format": "uuid" }, "readAt": { "anyOf": [ { "$ref": "#/components/schemas/dateTimeSchema" }, { "type": "null" } ], "default": null }, "takeoverAt": { "anyOf": [ { "$ref": "#/components/schemas/dateTimeSchema" }, { "type": "null" } ], "default": null }, "takeoverReason": { "type": [ "string", "null" ], "maxLength": 1000, "default": null }, "takeoverStatus": { "type": "string", "enum": [ "ai", "requested", "human", "resolved" ], "default": "ai" }, "updatedAt": { "$ref": "#/components/schemas/dateTimeSchema" }, "archivedAt": { "anyOf": [ { "$ref": "#/components/schemas/dateTimeSchema" }, { "type": "null" } ] }, "chatThreadTitle": { "type": [ "string", "null" ], "minLength": 1, "maxLength": 240 }, "connectionKind": { "$ref": "#/components/schemas/channelKindSchema" }, "connectionName": { "type": "string", "minLength": 1, "maxLength": 160 }, "connectionStatus": { "$ref": "#/components/schemas/channelConnectionStatusSchema" }, "contactAvatarUrl": { "type": [ "string", "null" ], "minLength": 1, "maxLength": 2048 }, "contactDisplayName": { "type": [ "string", "null" ], "minLength": 1, "maxLength": 240 }, "contactEmail": { "type": [ "string", "null" ], "minLength": 1, "maxLength": 320 }, "contactFirstName": { "type": [ "string", "null" ], "minLength": 1, "maxLength": 120 }, "contactLastName": { "type": [ "string", "null" ], "minLength": 1, "maxLength": 120 }, "contactPhone": { "type": [ "string", "null" ], "minLength": 1, "maxLength": 80 }, "lastDeliveryError": { "type": [ "string", "null" ], "maxLength": 2000 }, "lastDeliveryStatus": { "anyOf": [ { "$ref": "#/components/schemas/channelDeliveryStatusSchema" }, { "type": "null" } ] }, "lastMessageAt": { "anyOf": [ { "$ref": "#/components/schemas/dateTimeSchema" }, { "type": "null" } ] }, "lastMessageId": { "type": [ "string", "null" ], "format": "uuid" }, "lastMessageRole": { "anyOf": [ { "$ref": "#/components/schemas/chatMessageRoleSchema" }, { "type": "null" } ] }, "lastMessageText": { "type": [ "string", "null" ] }, "pinnedAt": { "anyOf": [ { "$ref": "#/components/schemas/dateTimeSchema" }, { "type": "null" } ] }, "streamStatus": { "anyOf": [ { "$ref": "#/components/schemas/chatStreamStatusSchema" }, { "type": "null" } ] }, "unread": { "type": "boolean" } }, "required": [ "assignedTeamId", "assignedUserId", "chatThreadId", "connectionId", "continuationCursor", "createdAt", "customerId", "externalActorId", "externalChannelId", "externalThreadId", "id", "lastInboundMessageId", "manuallyUnread", "metadata", "organizationId", "readAt", "takeoverAt", "takeoverReason", "takeoverStatus", "updatedAt", "archivedAt", "chatThreadTitle", "connectionKind", "connectionName", "connectionStatus", "contactAvatarUrl", "contactDisplayName", "contactEmail", "contactFirstName", "contactLastName", "contactPhone", "lastDeliveryError", "lastDeliveryStatus", "lastMessageAt", "lastMessageId", "lastMessageRole", "lastMessageText", "pinnedAt", "streamStatus", "unread" ] }, "channelKindSchema": { "type": "string", "enum": [ "slack", "teams", "google-chat", "discord", "telegram", "github", "linear", "whatsapp", "email" ] }, "chatMessageRoleSchema": { "type": "string", "enum": [ "system", "user", "assistant", "tool" ] }, "chatStreamStatusSchema": { "type": "string", "enum": [ "idle", "submitted", "streaming", "interrupted", "stopping", "stopped", "completed", "failed" ] }, "dateTimeSchema": { "type": "string" }, "inboxPaginationSchema": { "type": "object", "properties": { "hasMore": { "type": "boolean" }, "limit": { "type": "integer", "minimum": 1, "maximum": 100 }, "offset": { "type": "integer", "minimum": 0 }, "total": { "type": "integer", "minimum": 0 } }, "required": [ "hasMore", "limit", "offset", "total" ] }, "inboxThreadsResponseSchema": { "type": "object", "properties": { "pagination": { "$ref": "#/components/schemas/inboxPaginationSchema" }, "threads": { "type": "array", "items": { "$ref": "#/components/schemas/channelInboxThreadSchema" } }, "unreadCount": { "type": "integer", "minimum": 0 } }, "required": [ "pagination", "threads", "unreadCount" ] }, "jsonValueSchema": { "anyOf": [ { "type": "string" }, { "type": "number" }, { "type": "boolean" }, { "type": "null" }, { "type": "array", "items": { "$ref": "#/components/schemas/jsonValueSchema" } }, { "type": "object", "additionalProperties": { "$ref": "#/components/schemas/jsonValueSchema" }, "propertyNames": { "type": "string" } } ] }, "metadataSchema": { "type": "object", "additionalProperties": { "$ref": "#/components/schemas/jsonValueSchema" }, "propertyNames": { "type": "string" } } } ``` # Download an inbox attachment. (/docs/openapi/downloadInboxAttachment) ## GET /v1/organizations/{organizationId}/inbox/attachments/{attachmentId} ```json { "operationId": "downloadInboxAttachment", "summary": "Download an inbox attachment.", "description": "", "tags": [ "Inbox" ], "parameters": [ { "in": "path", "name": "organizationId", "required": true, "schema": { "type": "string", "format": "uuid" }, "example": "123e4567-e89b-12d3-a456-426614174000" }, { "in": "path", "name": "attachmentId", "required": true, "schema": { "type": "string", "format": "uuid" }, "example": "123e4567-e89b-12d3-a456-426614174000" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "responses": { "302": { "description": "Redirects to a short-lived signed attachment URL.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/inboxAttachmentRedirectResponseSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "inboxAttachmentRedirectResponseSchema": { "type": "string", "format": "uri" } } ``` # Get inbox delivery status. (/docs/openapi/getInboxDelivery) ## GET /v1/organizations/{organizationId}/inbox/deliveries/{deliveryId} ```json { "operationId": "getInboxDelivery", "summary": "Get inbox delivery status.", "description": "", "tags": [ "Inbox" ], "parameters": [ { "in": "path", "name": "organizationId", "required": true, "schema": { "type": "string", "format": "uuid" }, "example": "123e4567-e89b-12d3-a456-426614174000" }, { "in": "path", "name": "deliveryId", "required": true, "schema": { "type": "string", "format": "uuid" }, "example": "123e4567-e89b-12d3-a456-426614174000" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "responses": { "200": { "description": "Returns delivery state and attempt history.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/inboxDeliveryResponseSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "channelDeliveryAttemptSchema": { "type": "object", "properties": { "attempt": { "type": "integer", "exclusiveMinimum": 0 }, "completedAt": { "anyOf": [ { "$ref": "#/components/schemas/dateTimeSchema" }, { "type": "null" } ] }, "createdAt": { "$ref": "#/components/schemas/dateTimeSchema" }, "deliveryId": { "type": "string", "format": "uuid" }, "errorCode": { "type": [ "string", "null" ], "maxLength": 240 }, "errorMessage": { "type": [ "string", "null" ], "maxLength": 2000 }, "id": { "type": "string", "format": "uuid" }, "organizationId": { "type": "string", "format": "uuid" }, "providerStatus": { "type": [ "string", "null" ], "maxLength": 240 }, "responseMetadata": { "$ref": "#/components/schemas/metadataSchema" }, "startedAt": { "$ref": "#/components/schemas/dateTimeSchema" }, "status": { "type": "string", "enum": [ "queued", "running", "sent", "delivered", "failed", "canceled" ] } }, "required": [ "attempt", "completedAt", "createdAt", "deliveryId", "errorCode", "errorMessage", "id", "organizationId", "providerStatus", "responseMetadata", "startedAt", "status" ] }, "channelDeliveryStatusSchema": { "type": "string", "enum": [ "queued", "sent", "delivered", "failed", "dead_lettered", "canceled" ] }, "channelMessageDeliverySchema": { "type": "object", "properties": { "canceledAt": { "anyOf": [ { "$ref": "#/components/schemas/dateTimeSchema" }, { "type": "null" } ], "default": null }, "chatMessageId": { "type": [ "string", "null" ], "format": "uuid" }, "connectionId": { "type": "string", "format": "uuid" }, "createdAt": { "$ref": "#/components/schemas/dateTimeSchema" }, "direction": { "type": "string", "enum": [ "inbound", "outbound" ] }, "errorMessage": { "type": [ "string", "null" ], "maxLength": 2000 }, "externalMessageId": { "type": [ "string", "null" ], "minLength": 1, "maxLength": 240 }, "id": { "type": "string", "format": "uuid" }, "idempotencyKey": { "type": [ "string", "null" ], "minLength": 1, "maxLength": 240 }, "metadata": { "$ref": "#/components/schemas/metadataSchema" }, "nextAttemptAt": { "anyOf": [ { "$ref": "#/components/schemas/dateTimeSchema" }, { "type": "null" } ], "default": null }, "organizationId": { "type": "string", "format": "uuid" }, "retryCount": { "type": "integer", "minimum": 0, "default": 0 }, "status": { "$ref": "#/components/schemas/channelDeliveryStatusSchema" }, "updatedAt": { "$ref": "#/components/schemas/dateTimeSchema" } }, "required": [ "canceledAt", "chatMessageId", "connectionId", "createdAt", "direction", "errorMessage", "externalMessageId", "id", "idempotencyKey", "metadata", "nextAttemptAt", "organizationId", "retryCount", "status", "updatedAt" ] }, "dateTimeSchema": { "type": "string" }, "inboxDeliveryResponseSchema": { "type": "object", "properties": { "attempts": { "type": "array", "items": { "$ref": "#/components/schemas/channelDeliveryAttemptSchema" } }, "delivery": { "$ref": "#/components/schemas/channelMessageDeliverySchema" } }, "required": [ "attempts", "delivery" ] }, "jsonValueSchema": { "anyOf": [ { "type": "string" }, { "type": "number" }, { "type": "boolean" }, { "type": "null" }, { "type": "array", "items": { "$ref": "#/components/schemas/jsonValueSchema" } }, { "type": "object", "additionalProperties": { "$ref": "#/components/schemas/jsonValueSchema" }, "propertyNames": { "type": "string" } } ] }, "metadataSchema": { "type": "object", "additionalProperties": { "$ref": "#/components/schemas/jsonValueSchema" }, "propertyNames": { "type": "string" } } } ``` # List inbox thread messages. (/docs/openapi/listInboxThreadMessages) ## GET /v1/organizations/{organizationId}/inbox/threads/{threadId}/messages ```json { "operationId": "listInboxThreadMessages", "summary": "List inbox thread messages.", "description": "", "tags": [ "Inbox" ], "parameters": [ { "in": "query", "name": "limit", "required": true, "schema": { "type": "integer", "minimum": 1, "maximum": 100, "default": 50 }, "example": 1 }, { "in": "query", "name": "offset", "required": true, "schema": { "default": 0, "type": "integer", "minimum": 0, "maximum": 9007199254740991 }, "example": 1 }, { "in": "path", "name": "organizationId", "required": true, "schema": { "type": "string", "format": "uuid" }, "example": "123e4567-e89b-12d3-a456-426614174000" }, { "in": "path", "name": "threadId", "required": true, "schema": { "type": "string", "format": "uuid" }, "example": "123e4567-e89b-12d3-a456-426614174000" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "responses": { "200": { "description": "Returns messages, delivery state, and pagination.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/inboxMessagesResponseSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "channelDeliveryStatusSchema": { "type": "string", "enum": [ "queued", "sent", "delivered", "failed", "dead_lettered", "canceled" ] }, "channelDraftSuggestionSchema": { "type": "object", "properties": { "channelThreadId": { "type": "string", "format": "uuid" }, "confidence": { "type": [ "number", "null" ], "minimum": 0, "maximum": 1 }, "createdAt": { "$ref": "#/components/schemas/dateTimeSchema" }, "decidedAt": { "anyOf": [ { "$ref": "#/components/schemas/dateTimeSchema" }, { "type": "null" } ] }, "decidedBy": { "type": [ "string", "null" ], "format": "uuid" }, "decision": { "type": [ "string", "null" ], "enum": [ "send", "discard" ] }, "editedBy": { "type": [ "string", "null" ], "format": "uuid" }, "escalationReason": { "type": [ "string", "null" ], "maxLength": 1000 }, "id": { "type": "string", "format": "uuid" }, "model": { "type": [ "string", "null" ], "maxLength": 160 }, "organizationId": { "type": "string", "format": "uuid" }, "sourceMessageId": { "type": [ "string", "null" ], "format": "uuid" }, "status": { "type": "string", "enum": [ "pending", "sent", "discarded", "superseded" ] }, "text": { "type": "string", "minLength": 1, "maxLength": 4000 }, "updatedAt": { "$ref": "#/components/schemas/dateTimeSchema" } }, "required": [ "channelThreadId", "confidence", "createdAt", "decidedAt", "decidedBy", "decision", "editedBy", "escalationReason", "id", "model", "organizationId", "sourceMessageId", "status", "text", "updatedAt" ] }, "channelMessageDeliverySchema": { "type": "object", "properties": { "canceledAt": { "anyOf": [ { "$ref": "#/components/schemas/dateTimeSchema" }, { "type": "null" } ], "default": null }, "chatMessageId": { "type": [ "string", "null" ], "format": "uuid" }, "connectionId": { "type": "string", "format": "uuid" }, "createdAt": { "$ref": "#/components/schemas/dateTimeSchema" }, "direction": { "type": "string", "enum": [ "inbound", "outbound" ] }, "errorMessage": { "type": [ "string", "null" ], "maxLength": 2000 }, "externalMessageId": { "type": [ "string", "null" ], "minLength": 1, "maxLength": 240 }, "id": { "type": "string", "format": "uuid" }, "idempotencyKey": { "type": [ "string", "null" ], "minLength": 1, "maxLength": 240 }, "metadata": { "$ref": "#/components/schemas/metadataSchema" }, "nextAttemptAt": { "anyOf": [ { "$ref": "#/components/schemas/dateTimeSchema" }, { "type": "null" } ], "default": null }, "organizationId": { "type": "string", "format": "uuid" }, "retryCount": { "type": "integer", "minimum": 0, "default": 0 }, "status": { "$ref": "#/components/schemas/channelDeliveryStatusSchema" }, "updatedAt": { "$ref": "#/components/schemas/dateTimeSchema" } }, "required": [ "canceledAt", "chatMessageId", "connectionId", "createdAt", "direction", "errorMessage", "externalMessageId", "id", "idempotencyKey", "metadata", "nextAttemptAt", "organizationId", "retryCount", "status", "updatedAt" ] }, "chatMentionKindSchema": { "type": "string", "enum": [ "user", "team", "customer", "customer-asset", "contact", "product", "task", "quote", "invoice" ] }, "chatMessageFeedbackRatingSchema": { "type": "string", "enum": [ "positive", "negative" ] }, "chatMessageFeedbackSchema": { "type": "object", "properties": { "createdAt": { "$ref": "#/components/schemas/dateTimeSchema" }, "messageId": { "type": "string", "format": "uuid" }, "modelId": { "type": [ "string", "null" ], "minLength": 1, "maxLength": 160 }, "organizationId": { "type": "string", "format": "uuid" }, "rating": { "$ref": "#/components/schemas/chatMessageFeedbackRatingSchema" }, "runId": { "type": [ "string", "null" ], "format": "uuid" }, "updatedAt": { "$ref": "#/components/schemas/dateTimeSchema" }, "userId": { "type": "string", "format": "uuid" } }, "required": [ "createdAt", "messageId", "modelId", "organizationId", "rating", "runId", "updatedAt", "userId" ] }, "chatMessagePartSchema": { "type": "object", "discriminator": { "propertyName": "type" }, "oneOf": [ { "$ref": "#/components/schemas/contextualAiActionChatPartSchema" }, { "type": "object", "properties": { "text": { "type": "string" }, "type": { "type": "string", "enum": [ "text" ] } }, "required": [ "text", "type" ] }, { "type": "object", "properties": { "data": { "$ref": "#/components/schemas/metadataSchema" }, "type": { "type": "string", "enum": [ "data" ] } }, "required": [ "data", "type" ] }, { "type": "object", "properties": { "approval": { "allOf": [ { "$ref": "#/components/schemas/metadataSchema" } ] }, "errorText": { "type": "string", "maxLength": 1000 }, "input": { "allOf": [ { "$ref": "#/components/schemas/jsonValueSchema" } ] }, "metadata": { "allOf": [ { "$ref": "#/components/schemas/metadataSchema" } ] }, "output": { "allOf": [ { "$ref": "#/components/schemas/jsonValueSchema" } ] }, "state": { "type": "string", "enum": [ "input-streaming", "input-available", "approval-requested", "approval-responded", "output-available", "output-error", "output-denied" ] }, "toolCallId": { "type": "string", "minLength": 1 }, "toolName": { "type": "string", "minLength": 1 }, "type": { "type": "string", "enum": [ "tool" ] } }, "required": [ "state", "toolCallId", "toolName", "type" ] }, { "type": "object", "properties": { "sourceId": { "type": "string", "minLength": 1, "maxLength": 240 }, "title": { "type": "string", "minLength": 1, "maxLength": 240 }, "type": { "type": "string", "enum": [ "source-url" ] }, "url": { "type": "string", "format": "uri" } }, "required": [ "type", "url" ] }, { "type": "object", "properties": { "attachmentId": { "type": "string", "format": "uuid" }, "mediaType": { "type": "string", "minLength": 1 }, "name": { "type": "string", "minLength": 1, "maxLength": 240 }, "type": { "type": "string", "enum": [ "file" ] } }, "required": [ "attachmentId", "mediaType", "name", "type" ] }, { "type": "object", "properties": { "href": { "type": "string", "minLength": 1, "maxLength": 1024 }, "id": { "type": "string", "minLength": 1, "maxLength": 240 }, "kind": { "$ref": "#/components/schemas/chatMentionKindSchema" }, "label": { "type": "string", "minLength": 1, "maxLength": 240 }, "metadata": { "allOf": [ { "$ref": "#/components/schemas/metadataSchema" } ] }, "type": { "type": "string", "enum": [ "mention" ] } }, "required": [ "id", "kind", "label", "type" ] }, { "type": "object", "properties": { "command": { "type": "string", "minLength": 1, "maxLength": 80 }, "end": { "type": "integer", "exclusiveMinimum": 0 }, "error": { "type": "string", "minLength": 1, "maxLength": 1000 }, "label": { "type": "string", "minLength": 1, "maxLength": 240 }, "runId": { "type": "string", "minLength": 1, "maxLength": 240 }, "start": { "type": "integer", "minimum": 0 }, "status": { "type": "string", "enum": [ "selected", "executed", "rejected" ] }, "type": { "type": "string", "enum": [ "command" ] }, "workflowId": { "type": "string", "format": "uuid" } }, "required": [ "command", "label", "status", "type" ] } ] }, "chatMessageRoleSchema": { "type": "string", "enum": [ "system", "user", "assistant", "tool" ] }, "chatMessageSchema": { "type": "object", "properties": { "clientMessageId": { "type": [ "string", "null" ], "minLength": 1, "maxLength": 240, "default": null }, "createdAt": { "$ref": "#/components/schemas/dateTimeSchema" }, "createdBy": { "type": [ "string", "null" ], "format": "uuid" }, "id": { "type": "string", "format": "uuid" }, "metadata": { "$ref": "#/components/schemas/metadataSchema" }, "organizationId": { "type": [ "string", "null" ], "format": "uuid" }, "parts": { "type": "array", "items": { "$ref": "#/components/schemas/chatMessagePartSchema" } }, "role": { "$ref": "#/components/schemas/chatMessageRoleSchema" }, "status": { "type": "string", "enum": [ "complete", "partial", "failed" ] }, "threadId": { "type": "string", "format": "uuid" }, "viewerFeedback": { "anyOf": [ { "$ref": "#/components/schemas/chatMessageFeedbackSchema" }, { "type": "null" } ] } }, "required": [ "clientMessageId", "createdAt", "createdBy", "id", "metadata", "organizationId", "parts", "role", "status", "threadId" ] }, "contextualAiActionChatPartSchema": { "type": "object", "properties": { "actionId": { "$ref": "#/components/schemas/contextualAiActionIdSchema" }, "actionVersion": { "$ref": "#/components/schemas/contextualAiActionVersionSchema" }, "approvalConfirmed": { "type": "boolean" }, "contextFingerprint": { "$ref": "#/components/schemas/contextualAiActionFingerprintSchema" }, "contextKinds": { "type": "array", "items": { "$ref": "#/components/schemas/contextualAiActionContextKindSchema" }, "minItems": 1, "maxItems": 8 }, "intakeAnswers": { "type": "array", "items": { "$ref": "#/components/schemas/contextualAiActionIntakeAnswerSchema" }, "maxItems": 12 }, "route": { "type": "string", "minLength": 1, "maxLength": 2048, "pattern": "^/" }, "safeReferences": { "type": "array", "items": { "$ref": "#/components/schemas/contextualAiActionSafeReferenceSchema" }, "maxItems": 20 }, "type": { "type": "string", "enum": [ "action" ] } }, "required": [ "actionId", "actionVersion", "approvalConfirmed", "contextFingerprint", "contextKinds", "route", "safeReferences", "type" ] }, "contextualAiActionContextKindSchema": { "type": "string", "enum": [ "page", "organization", "entity", "selection", "form", "thread", "collection", "workflow", "filter", "schedule", "template" ] }, "contextualAiActionFingerprintSchema": { "type": "string", "minLength": 8, "maxLength": 160, "pattern": "^sha256:[A-Za-z0-9_-]+$" }, "contextualAiActionIdSchema": { "type": "string", "minLength": 1, "maxLength": 120, "pattern": "^[a-z0-9]+(?:[.-][a-z0-9]+)*$" }, "contextualAiActionIntakeAnswerSchema": { "type": "object", "properties": { "fieldId": { "$ref": "#/components/schemas/contextualAiActionIdSchema" }, "value": { "$ref": "#/components/schemas/contextualAiActionIntakeValueSchema" } }, "required": [ "fieldId", "value" ], "additionalProperties": false }, "contextualAiActionIntakeValueSchema": { "anyOf": [ { "type": "string", "maxLength": 10000 }, { "type": "array", "items": { "type": "string", "maxLength": 1000 }, "maxItems": 12 } ] }, "contextualAiActionSafeReferenceSchema": { "type": "object", "properties": { "id": { "type": "string", "minLength": 1, "maxLength": 160 }, "kind": { "$ref": "#/components/schemas/contextualAiActionIdSchema" }, "label": { "type": "string", "minLength": 1, "maxLength": 160 }, "organizationId": { "type": "string", "format": "uuid" }, "path": { "type": "string", "minLength": 1, "maxLength": 1024, "pattern": "^/" } }, "required": [ "id", "kind", "label" ], "additionalProperties": false }, "contextualAiActionVersionSchema": { "type": "string", "minLength": 1, "maxLength": 40, "pattern": "^[a-z0-9]+(?:[.-][a-z0-9]+)*$" }, "dateTimeSchema": { "type": "string" }, "inboxMessagesResponseSchema": { "type": "object", "properties": { "deliveries": { "type": "array", "items": { "$ref": "#/components/schemas/channelMessageDeliverySchema" } }, "draftSuggestion": { "anyOf": [ { "$ref": "#/components/schemas/channelDraftSuggestionSchema" }, { "type": "null" } ] }, "messages": { "type": "array", "items": { "$ref": "#/components/schemas/chatMessageSchema" } }, "pagination": { "$ref": "#/components/schemas/inboxPaginationSchema" }, "senderProfiles": { "type": "object", "additionalProperties": { "type": "object", "properties": { "avatarUrl": { "type": [ "string", "null" ], "format": "uri" }, "displayName": { "type": "string" }, "userId": { "type": "string", "format": "uuid" } }, "required": [ "avatarUrl", "displayName", "userId" ] }, "propertyNames": { "type": "string" } }, "streamStatus": { "type": "string" } }, "required": [ "deliveries", "draftSuggestion", "messages", "pagination", "senderProfiles", "streamStatus" ] }, "inboxPaginationSchema": { "type": "object", "properties": { "hasMore": { "type": "boolean" }, "limit": { "type": "integer", "minimum": 1, "maximum": 100 }, "offset": { "type": "integer", "minimum": 0 }, "total": { "type": "integer", "minimum": 0 } }, "required": [ "hasMore", "limit", "offset", "total" ] }, "jsonValueSchema": { "anyOf": [ { "type": "string" }, { "type": "number" }, { "type": "boolean" }, { "type": "null" }, { "type": "array", "items": { "$ref": "#/components/schemas/jsonValueSchema" } }, { "type": "object", "additionalProperties": { "$ref": "#/components/schemas/jsonValueSchema" }, "propertyNames": { "type": "string" } } ] }, "metadataSchema": { "type": "object", "additionalProperties": { "$ref": "#/components/schemas/jsonValueSchema" }, "propertyNames": { "type": "string" } } } ``` # Send a staff reply to an inbox thread. (/docs/openapi/sendInboxThreadReply) ## POST /v1/organizations/{organizationId}/inbox/threads/{threadId}/messages ```json { "operationId": "sendInboxThreadReply", "summary": "Send a staff reply to an inbox thread.", "description": "", "tags": [ "Inbox" ], "parameters": [ { "in": "path", "name": "organizationId", "required": true, "schema": { "type": "string", "format": "uuid" }, "example": "123e4567-e89b-12d3-a456-426614174000" }, { "in": "path", "name": "threadId", "required": true, "schema": { "type": "string", "format": "uuid" }, "example": "123e4567-e89b-12d3-a456-426614174000" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/inboxReplyRequestSchema" } } }, "description": "Text and existing inbox attachment references to deliver.", "required": true }, "responses": { "201": { "description": "Returns the persisted message and external delivery state.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/inboxReplyResponseSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "channelDeliveryStatusSchema": { "type": "string", "enum": [ "queued", "sent", "delivered", "failed", "dead_lettered", "canceled" ] }, "channelMessageDeliverySchema": { "type": "object", "properties": { "canceledAt": { "anyOf": [ { "$ref": "#/components/schemas/dateTimeSchema" }, { "type": "null" } ], "default": null }, "chatMessageId": { "type": [ "string", "null" ], "format": "uuid" }, "connectionId": { "type": "string", "format": "uuid" }, "createdAt": { "$ref": "#/components/schemas/dateTimeSchema" }, "direction": { "type": "string", "enum": [ "inbound", "outbound" ] }, "errorMessage": { "type": [ "string", "null" ], "maxLength": 2000 }, "externalMessageId": { "type": [ "string", "null" ], "minLength": 1, "maxLength": 240 }, "id": { "type": "string", "format": "uuid" }, "idempotencyKey": { "type": [ "string", "null" ], "minLength": 1, "maxLength": 240 }, "metadata": { "$ref": "#/components/schemas/metadataSchema" }, "nextAttemptAt": { "anyOf": [ { "$ref": "#/components/schemas/dateTimeSchema" }, { "type": "null" } ], "default": null }, "organizationId": { "type": "string", "format": "uuid" }, "retryCount": { "type": "integer", "minimum": 0, "default": 0 }, "status": { "$ref": "#/components/schemas/channelDeliveryStatusSchema" }, "updatedAt": { "$ref": "#/components/schemas/dateTimeSchema" } }, "required": [ "canceledAt", "chatMessageId", "connectionId", "createdAt", "direction", "errorMessage", "externalMessageId", "id", "idempotencyKey", "metadata", "nextAttemptAt", "organizationId", "retryCount", "status", "updatedAt" ] }, "chatMentionKindSchema": { "type": "string", "enum": [ "user", "team", "customer", "customer-asset", "contact", "product", "task", "quote", "invoice" ] }, "chatMessageFeedbackRatingSchema": { "type": "string", "enum": [ "positive", "negative" ] }, "chatMessageFeedbackSchema": { "type": "object", "properties": { "createdAt": { "$ref": "#/components/schemas/dateTimeSchema" }, "messageId": { "type": "string", "format": "uuid" }, "modelId": { "type": [ "string", "null" ], "minLength": 1, "maxLength": 160 }, "organizationId": { "type": "string", "format": "uuid" }, "rating": { "$ref": "#/components/schemas/chatMessageFeedbackRatingSchema" }, "runId": { "type": [ "string", "null" ], "format": "uuid" }, "updatedAt": { "$ref": "#/components/schemas/dateTimeSchema" }, "userId": { "type": "string", "format": "uuid" } }, "required": [ "createdAt", "messageId", "modelId", "organizationId", "rating", "runId", "updatedAt", "userId" ] }, "chatMessagePartSchema": { "type": "object", "discriminator": { "propertyName": "type" }, "oneOf": [ { "$ref": "#/components/schemas/contextualAiActionChatPartSchema" }, { "type": "object", "properties": { "text": { "type": "string" }, "type": { "type": "string", "enum": [ "text" ] } }, "required": [ "text", "type" ] }, { "type": "object", "properties": { "data": { "$ref": "#/components/schemas/metadataSchema" }, "type": { "type": "string", "enum": [ "data" ] } }, "required": [ "data", "type" ] }, { "type": "object", "properties": { "approval": { "allOf": [ { "$ref": "#/components/schemas/metadataSchema" } ] }, "errorText": { "type": "string", "maxLength": 1000 }, "input": { "allOf": [ { "$ref": "#/components/schemas/jsonValueSchema" } ] }, "metadata": { "allOf": [ { "$ref": "#/components/schemas/metadataSchema" } ] }, "output": { "allOf": [ { "$ref": "#/components/schemas/jsonValueSchema" } ] }, "state": { "type": "string", "enum": [ "input-streaming", "input-available", "approval-requested", "approval-responded", "output-available", "output-error", "output-denied" ] }, "toolCallId": { "type": "string", "minLength": 1 }, "toolName": { "type": "string", "minLength": 1 }, "type": { "type": "string", "enum": [ "tool" ] } }, "required": [ "state", "toolCallId", "toolName", "type" ] }, { "type": "object", "properties": { "sourceId": { "type": "string", "minLength": 1, "maxLength": 240 }, "title": { "type": "string", "minLength": 1, "maxLength": 240 }, "type": { "type": "string", "enum": [ "source-url" ] }, "url": { "type": "string", "format": "uri" } }, "required": [ "type", "url" ] }, { "type": "object", "properties": { "attachmentId": { "type": "string", "format": "uuid" }, "mediaType": { "type": "string", "minLength": 1 }, "name": { "type": "string", "minLength": 1, "maxLength": 240 }, "type": { "type": "string", "enum": [ "file" ] } }, "required": [ "attachmentId", "mediaType", "name", "type" ] }, { "type": "object", "properties": { "href": { "type": "string", "minLength": 1, "maxLength": 1024 }, "id": { "type": "string", "minLength": 1, "maxLength": 240 }, "kind": { "$ref": "#/components/schemas/chatMentionKindSchema" }, "label": { "type": "string", "minLength": 1, "maxLength": 240 }, "metadata": { "allOf": [ { "$ref": "#/components/schemas/metadataSchema" } ] }, "type": { "type": "string", "enum": [ "mention" ] } }, "required": [ "id", "kind", "label", "type" ] }, { "type": "object", "properties": { "command": { "type": "string", "minLength": 1, "maxLength": 80 }, "end": { "type": "integer", "exclusiveMinimum": 0 }, "error": { "type": "string", "minLength": 1, "maxLength": 1000 }, "label": { "type": "string", "minLength": 1, "maxLength": 240 }, "runId": { "type": "string", "minLength": 1, "maxLength": 240 }, "start": { "type": "integer", "minimum": 0 }, "status": { "type": "string", "enum": [ "selected", "executed", "rejected" ] }, "type": { "type": "string", "enum": [ "command" ] }, "workflowId": { "type": "string", "format": "uuid" } }, "required": [ "command", "label", "status", "type" ] } ] }, "chatMessageRoleSchema": { "type": "string", "enum": [ "system", "user", "assistant", "tool" ] }, "chatMessageSchema": { "type": "object", "properties": { "clientMessageId": { "type": [ "string", "null" ], "minLength": 1, "maxLength": 240, "default": null }, "createdAt": { "$ref": "#/components/schemas/dateTimeSchema" }, "createdBy": { "type": [ "string", "null" ], "format": "uuid" }, "id": { "type": "string", "format": "uuid" }, "metadata": { "$ref": "#/components/schemas/metadataSchema" }, "organizationId": { "type": [ "string", "null" ], "format": "uuid" }, "parts": { "type": "array", "items": { "$ref": "#/components/schemas/chatMessagePartSchema" } }, "role": { "$ref": "#/components/schemas/chatMessageRoleSchema" }, "status": { "type": "string", "enum": [ "complete", "partial", "failed" ] }, "threadId": { "type": "string", "format": "uuid" }, "viewerFeedback": { "anyOf": [ { "$ref": "#/components/schemas/chatMessageFeedbackSchema" }, { "type": "null" } ] } }, "required": [ "clientMessageId", "createdAt", "createdBy", "id", "metadata", "organizationId", "parts", "role", "status", "threadId" ] }, "contextualAiActionChatPartSchema": { "type": "object", "properties": { "actionId": { "$ref": "#/components/schemas/contextualAiActionIdSchema" }, "actionVersion": { "$ref": "#/components/schemas/contextualAiActionVersionSchema" }, "approvalConfirmed": { "type": "boolean" }, "contextFingerprint": { "$ref": "#/components/schemas/contextualAiActionFingerprintSchema" }, "contextKinds": { "type": "array", "items": { "$ref": "#/components/schemas/contextualAiActionContextKindSchema" }, "minItems": 1, "maxItems": 8 }, "intakeAnswers": { "type": "array", "items": { "$ref": "#/components/schemas/contextualAiActionIntakeAnswerSchema" }, "maxItems": 12 }, "route": { "type": "string", "minLength": 1, "maxLength": 2048, "pattern": "^/" }, "safeReferences": { "type": "array", "items": { "$ref": "#/components/schemas/contextualAiActionSafeReferenceSchema" }, "maxItems": 20 }, "type": { "type": "string", "enum": [ "action" ] } }, "required": [ "actionId", "actionVersion", "approvalConfirmed", "contextFingerprint", "contextKinds", "route", "safeReferences", "type" ] }, "contextualAiActionContextKindSchema": { "type": "string", "enum": [ "page", "organization", "entity", "selection", "form", "thread", "collection", "workflow", "filter", "schedule", "template" ] }, "contextualAiActionFingerprintSchema": { "type": "string", "minLength": 8, "maxLength": 160, "pattern": "^sha256:[A-Za-z0-9_-]+$" }, "contextualAiActionIdSchema": { "type": "string", "minLength": 1, "maxLength": 120, "pattern": "^[a-z0-9]+(?:[.-][a-z0-9]+)*$" }, "contextualAiActionIntakeAnswerSchema": { "type": "object", "properties": { "fieldId": { "$ref": "#/components/schemas/contextualAiActionIdSchema" }, "value": { "$ref": "#/components/schemas/contextualAiActionIntakeValueSchema" } }, "required": [ "fieldId", "value" ], "additionalProperties": false }, "contextualAiActionIntakeValueSchema": { "anyOf": [ { "type": "string", "maxLength": 10000 }, { "type": "array", "items": { "type": "string", "maxLength": 1000 }, "maxItems": 12 } ] }, "contextualAiActionSafeReferenceSchema": { "type": "object", "properties": { "id": { "type": "string", "minLength": 1, "maxLength": 160 }, "kind": { "$ref": "#/components/schemas/contextualAiActionIdSchema" }, "label": { "type": "string", "minLength": 1, "maxLength": 160 }, "organizationId": { "type": "string", "format": "uuid" }, "path": { "type": "string", "minLength": 1, "maxLength": 1024, "pattern": "^/" } }, "required": [ "id", "kind", "label" ], "additionalProperties": false }, "contextualAiActionVersionSchema": { "type": "string", "minLength": 1, "maxLength": 40, "pattern": "^[a-z0-9]+(?:[.-][a-z0-9]+)*$" }, "dateTimeSchema": { "type": "string" }, "inboxReplyAttachmentSchema": { "type": "object", "properties": { "attachmentId": { "type": "string", "format": "uuid" }, "mediaType": { "type": "string", "minLength": 1, "maxLength": 240 }, "name": { "type": "string", "minLength": 1, "maxLength": 240 } }, "required": [ "attachmentId", "mediaType", "name" ] }, "inboxReplyRequestSchema": { "type": "object", "properties": { "files": { "type": "array", "items": { "$ref": "#/components/schemas/inboxReplyAttachmentSchema" }, "maxItems": 10 }, "text": { "type": "string", "maxLength": 4000, "default": "" } }, "required": [ "files", "text" ] }, "inboxReplyResponseSchema": { "type": "object", "properties": { "delivery": { "$ref": "#/components/schemas/channelMessageDeliverySchema" }, "message": { "$ref": "#/components/schemas/chatMessageSchema" }, "sent": { "type": "boolean" } }, "required": [ "delivery", "message", "sent" ] }, "jsonValueSchema": { "anyOf": [ { "type": "string" }, { "type": "number" }, { "type": "boolean" }, { "type": "null" }, { "type": "array", "items": { "$ref": "#/components/schemas/jsonValueSchema" } }, { "type": "object", "additionalProperties": { "$ref": "#/components/schemas/jsonValueSchema" }, "propertyNames": { "type": "string" } } ] }, "metadataSchema": { "type": "object", "additionalProperties": { "$ref": "#/components/schemas/jsonValueSchema" }, "propertyNames": { "type": "string" } } } ``` # Generate inbox reply suggestions. (/docs/openapi/generateInboxSuggestions) ## POST /v1/organizations/{organizationId}/inbox/threads/{threadId}/suggestions ```json { "operationId": "generateInboxSuggestions", "summary": "Generate inbox reply suggestions.", "description": "", "tags": [ "Inbox" ], "parameters": [ { "in": "query", "name": "locale", "required": true, "schema": { "type": [ "string", "null" ], "minLength": 2, "maxLength": 12, "default": null }, "example": "example" }, { "in": "path", "name": "organizationId", "required": true, "schema": { "type": "string", "format": "uuid" }, "example": "123e4567-e89b-12d3-a456-426614174000" }, { "in": "path", "name": "threadId", "required": true, "schema": { "type": "string", "format": "uuid" }, "example": "123e4567-e89b-12d3-a456-426614174000" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/inboxSuggestionsBodySchema" } } }, "description": "Previous suggestions to avoid when generating replacements." }, "responses": { "200": { "description": "Returns generated customer reply suggestions.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/inboxSuggestionsResponseSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "inboxSuggestionsBodySchema": { "type": "object", "properties": { "previousSuggestions": { "type": "array", "items": { "type": "string", "minLength": 1, "maxLength": 160 }, "maxItems": 20 } }, "required": [ "previousSuggestions" ] }, "inboxSuggestionsResponseSchema": { "type": "object", "properties": { "suggestions": { "type": "array", "items": { "type": "string", "minLength": 1, "maxLength": 160 } } }, "required": [ "suggestions" ] } } ``` # List stock levels per variant. (/docs/openapi/listInventoryLevels) Return on hand, reserved, available, backordered, and incoming quantities for every stock-tracked variant, for webshop stock sync. Subscribe to the `inventory.level-changed` webhook to hear about changes. ## GET /v1/organizations/{organizationId}/inventory/levels ```json { "operationId": "listInventoryLevels", "summary": "List stock levels per variant.", "description": "Return on hand, reserved, available, backordered, and incoming quantities for every stock-tracked variant, for webshop stock sync. Subscribe to the `inventory.level-changed` webhook to hear about changes.", "tags": [ "Inventory" ], "parameters": [ { "in": "query", "name": "productId", "required": false, "schema": { "allOf": [ { "$ref": "#/components/schemas/productIdSchema" } ] }, "example": "123" }, { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "responses": { "200": { "description": "Returns stock levels per variant.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/inventoryLevelsResponseSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "inventoryLevelsResponseSchema": { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/components/schemas/inventoryVariantLevelSchema" } } }, "required": [ "data" ] }, "inventoryVariantLevelSchema": { "type": "object", "properties": { "allocated": { "type": "number" }, "available": { "type": "number" }, "backordered": { "type": "number" }, "incoming": { "type": "number" }, "isDefault": { "type": "boolean" }, "onHand": { "type": "number" }, "position": { "type": "integer" }, "productId": { "$ref": "#/components/schemas/productIdSchema" }, "reorderPoint": { "type": [ "number", "null" ] }, "sku": { "type": [ "string", "null" ] }, "stockStatus": { "$ref": "#/components/schemas/stockStatusSchema" }, "variantId": { "type": "string", "format": "uuid" }, "variantTitle": { "type": "string" } }, "required": [ "allocated", "available", "backordered", "incoming", "isDefault", "onHand", "position", "productId", "reorderPoint", "sku", "stockStatus", "variantId", "variantTitle" ] }, "organizationIdSchema": { "type": "string", "format": "uuid" }, "productIdSchema": { "type": "string", "format": "uuid" }, "stockStatusSchema": { "type": "string", "enum": [ "not_tracked", "discontinued", "out_of_stock", "backordered", "low_stock", "in_stock" ] } } ``` # List invoices. (/docs/openapi/listInvoices) Return invoices for an organization the authenticated user can access. ## GET /v1/organizations/{organizationId}/invoices ```json { "operationId": "listInvoices", "summary": "List invoices.", "description": "Return invoices for an organization the authenticated user can access.", "tags": [ "Invoices" ], "parameters": [ { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "responses": { "200": { "description": "Returns paginated invoices.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/invoicesListResponseSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "invoicesListResponseSchema": { "type": "object" }, "organizationIdSchema": { "type": "string", "format": "uuid" } } ``` # Create an invoice. (/docs/openapi/createInvoice) Create an invoice for the specified organization. ## POST /v1/organizations/{organizationId}/invoices ```json { "operationId": "createInvoice", "summary": "Create an invoice.", "description": "Create an invoice for the specified organization.", "tags": [ "Invoices" ], "parameters": [ { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/invoiceInputSchema" } } }, "description": "The invoice payload." }, "responses": { "201": { "description": "Returns the created invoice.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/invoiceSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "discountKindSchema": { "type": "string", "enum": [ "none", "percentage", "fixed" ] }, "documentLineStockModeSchema": { "type": "string", "enum": [ "auto", "task", "none" ] }, "documentVatRegimeSchema": { "type": "string", "enum": [ "standard", "reverse_charge", "exempt", "kor", "margin_scheme" ] }, "invoiceCurrencyCodeSchema": { "type": "string", "pattern": "^[A-Z]{3}$" }, "invoiceDisplayNumberSchema": { "type": "string", "minLength": 1, "maxLength": 40 }, "invoiceIdSchema": { "type": "string", "format": "uuid" }, "invoiceInputSchema": { "type": "object", "properties": { "customerId": { "type": "string", "format": "uuid" }, "customerContactId": { "type": [ "string", "null" ], "format": "uuid" }, "customerReference": { "type": [ "string", "null" ], "maxLength": 240 }, "title": { "type": "string", "minLength": 1, "maxLength": 240 }, "intro": { "type": [ "string", "null" ], "maxLength": 20000 }, "terms": { "type": [ "string", "null" ], "maxLength": 20000 }, "issueDate": { "allOf": [ { "$ref": "#/components/schemas/isoDateOrNull" } ] }, "dueDate": { "allOf": [ { "$ref": "#/components/schemas/isoDateOrNull" } ] }, "currency": { "allOf": [ { "$ref": "#/components/schemas/invoiceCurrencyCodeSchema" } ] }, "vatInclusive": { "type": "boolean" }, "onlinePaymentEnabled": { "type": "boolean" }, "discountKind": { "allOf": [ { "$ref": "#/components/schemas/discountKindSchema" } ] }, "discountValue": { "type": "number", "minimum": 0 }, "discountDescription": { "type": [ "string", "null" ], "maxLength": 500 }, "promotionId": { "type": [ "string", "null" ], "format": "uuid" }, "publicMessage": { "type": [ "string", "null" ], "maxLength": 20000 }, "footerText": { "type": [ "string", "null" ], "maxLength": 20000 }, "dueDays": { "type": [ "integer", "null" ], "minimum": 0, "maximum": 365 }, "invoiceTemplateId": { "type": [ "string", "null" ], "format": "uuid" }, "sourceQuoteId": { "type": [ "string", "null" ], "format": "uuid" }, "deliveryDate": { "allOf": [ { "$ref": "#/components/schemas/isoDateOrNull" } ] }, "kind": { "allOf": [ { "$ref": "#/components/schemas/invoiceKindSchema" } ] }, "originalInvoiceId": { "type": [ "string", "null" ], "format": "uuid" }, "vatRegime": { "allOf": [ { "$ref": "#/components/schemas/documentVatRegimeSchema" } ] }, "termsPdfFileId": { "type": [ "string", "null" ], "format": "uuid" }, "termsPdfVersionId": { "type": [ "string", "null" ], "format": "uuid" }, "termsRichText": { "type": [ "string", "null" ], "maxLength": 100000 }, "termsUrl": { "allOf": [ { "$ref": "#/components/schemas/optionalUrl" } ] }, "lines": { "type": "array", "items": { "$ref": "#/components/schemas/invoiceLineInputSchema" }, "maxItems": 500 }, "linkedTaskIds": { "type": "array", "items": { "type": "string", "format": "uuid" }, "maxItems": 200 } }, "required": [ "customerId", "title" ] }, "invoiceKindSchema": { "type": "string", "enum": [ "invoice", "credit_note", "deposit" ] }, "invoiceLineIdSchema": { "type": "string", "format": "uuid" }, "invoiceLineInputSchema": { "type": "object", "properties": { "description": { "type": [ "string", "null" ], "maxLength": 20000 }, "discountKind": { "allOf": [ { "$ref": "#/components/schemas/discountKindSchema" } ] }, "discountValue": { "type": "number", "minimum": 0 }, "expenseId": { "type": "string", "format": "uuid" }, "id": { "allOf": [ { "$ref": "#/components/schemas/invoiceLineIdSchema" } ] }, "kind": { "allOf": [ { "$ref": "#/components/schemas/invoiceLineKindSchema" } ] }, "lineType": { "anyOf": [ { "$ref": "#/components/schemas/invoiceLineTypeSchema" }, { "type": "null" } ] }, "position": { "type": "number" }, "pricingMode": { "allOf": [ { "$ref": "#/components/schemas/invoiceLinePricingModeSchema" } ] }, "productId": { "type": [ "string", "null" ], "format": "uuid" }, "promotionId": { "type": [ "string", "null" ], "format": "uuid" }, "quantity": { "type": "number", "minimum": 0 }, "sourceTaskMaterialId": { "type": "string", "format": "uuid" }, "sourceTimeEntryIds": { "type": "array", "items": { "type": "string", "format": "uuid" }, "maxItems": 2000 }, "stockMode": { "allOf": [ { "$ref": "#/components/schemas/documentLineStockModeSchema" } ] }, "taskId": { "type": [ "string", "null" ], "format": "uuid" }, "title": { "type": "string", "minLength": 1, "maxLength": 500 }, "unit": { "type": [ "string", "null" ], "maxLength": 40 }, "unitPrice": { "type": "number" }, "variantId": { "type": [ "string", "null" ], "format": "uuid" }, "vatRate": { "type": "number", "minimum": 0, "maximum": 100 } }, "required": [ "title" ] }, "invoiceLineKindSchema": { "type": "string", "enum": [ "line", "section_header" ] }, "invoiceLinePricingModeSchema": { "type": "string", "enum": [ "unit", "hourly", "fixed" ] }, "invoiceLineTypeSchema": { "type": "string", "enum": [ "product", "labor", "custom" ] }, "invoiceNumberSchema": { "type": "integer", "exclusiveMinimum": 0, "maximum": 9007199254740991 }, "invoiceSchema": { "type": "object", "properties": { "id": { "$ref": "#/components/schemas/invoiceIdSchema" }, "organizationId": { "type": "string", "format": "uuid" }, "customerId": { "type": "string", "format": "uuid" }, "customerContactId": { "type": [ "string", "null" ], "format": "uuid" }, "customerReference": { "type": [ "string", "null" ] }, "number": { "anyOf": [ { "$ref": "#/components/schemas/invoiceNumberSchema" }, { "type": "null" } ] }, "numberYear": { "type": [ "integer", "null" ], "exclusiveMinimum": 0 }, "displayNumber": { "anyOf": [ { "$ref": "#/components/schemas/invoiceDisplayNumberSchema" }, { "type": "null" } ] }, "status": { "$ref": "#/components/schemas/invoiceStatusSchema" }, "dueDays": { "type": [ "integer", "null" ], "minimum": 0, "maximum": 365 }, "invoiceTemplateId": { "type": [ "string", "null" ], "format": "uuid" }, "title": { "type": "string" }, "intro": { "type": [ "string", "null" ] }, "terms": { "type": [ "string", "null" ] }, "currency": { "$ref": "#/components/schemas/invoiceCurrencyCodeSchema" }, "vatInclusive": { "type": "boolean" }, "discountKind": { "$ref": "#/components/schemas/discountKindSchema" }, "discountValue": { "type": "number" }, "discountDescription": { "type": [ "string", "null" ], "maxLength": 500 }, "promotionId": { "type": [ "string", "null" ], "format": "uuid" }, "subtotalExcl": { "type": "number" }, "vatTotal": { "type": "number" }, "totalIncl": { "type": "number" }, "issueDate": { "type": [ "string", "null" ] }, "dueDate": { "type": [ "string", "null" ] }, "publicMessage": { "type": [ "string", "null" ] }, "footerText": { "type": [ "string", "null" ] }, "sourceQuoteId": { "type": [ "string", "null" ], "format": "uuid" }, "deliveryDate": { "type": [ "string", "null" ] }, "kind": { "$ref": "#/components/schemas/invoiceKindSchema" }, "originalInvoiceId": { "type": [ "string", "null" ], "format": "uuid" }, "vatRegime": { "$ref": "#/components/schemas/documentVatRegimeSchema" }, "sellerName": { "type": [ "string", "null" ] }, "sellerAddressLine1": { "type": [ "string", "null" ] }, "sellerAddressLine2": { "type": [ "string", "null" ] }, "sellerAddressPostalCode": { "type": [ "string", "null" ] }, "sellerAddressCity": { "type": [ "string", "null" ] }, "sellerAddressCountry": { "type": [ "string", "null" ] }, "sellerEmail": { "type": [ "string", "null" ] }, "sellerPhone": { "type": [ "string", "null" ] }, "sellerKvk": { "type": [ "string", "null" ] }, "sellerBtw": { "type": [ "string", "null" ] }, "sellerIban": { "type": [ "string", "null" ] }, "buyerName": { "type": [ "string", "null" ] }, "buyerAddressLine1": { "type": [ "string", "null" ] }, "buyerAddressLine2": { "type": [ "string", "null" ] }, "buyerAddressPostalCode": { "type": [ "string", "null" ] }, "buyerAddressCity": { "type": [ "string", "null" ] }, "buyerAddressCountry": { "type": [ "string", "null" ] }, "buyerEmail": { "type": [ "string", "null" ] }, "buyerKvk": { "type": [ "string", "null" ] }, "buyerBtw": { "type": [ "string", "null" ] }, "termsPdfFileId": { "type": [ "string", "null" ], "format": "uuid" }, "termsPdfVersionId": { "type": [ "string", "null" ], "format": "uuid" }, "termsRichText": { "type": [ "string", "null" ], "maxLength": 100000 }, "termsUrl": { "type": [ "string", "null" ] }, "sentAt": { "type": [ "string", "null" ], "format": "date-time" }, "paidAt": { "type": [ "string", "null" ], "format": "date-time" }, "canceledAt": { "type": [ "string", "null" ], "format": "date-time" }, "archivedAt": { "type": [ "string", "null" ], "format": "date-time" }, "createdBy": { "type": [ "string", "null" ], "format": "uuid" }, "updatedBy": { "type": [ "string", "null" ], "format": "uuid" }, "createdAt": { "type": "string", "format": "date-time" }, "updatedAt": { "type": "string", "format": "date-time" }, "amountPaidCents": { "type": "integer", "minimum": 0 }, "amountDueCents": { "type": "integer", "minimum": 0 }, "uncollectibleAt": { "type": [ "string", "null" ], "format": "date-time" }, "uncollectibleReason": { "type": [ "string", "null" ] }, "onlinePaymentEnabled": { "type": "boolean", "default": true } }, "required": [ "id", "organizationId", "customerId", "customerContactId", "customerReference", "number", "numberYear", "displayNumber", "status", "dueDays", "invoiceTemplateId", "title", "intro", "terms", "currency", "vatInclusive", "discountKind", "discountValue", "discountDescription", "promotionId", "subtotalExcl", "vatTotal", "totalIncl", "issueDate", "dueDate", "publicMessage", "footerText", "sourceQuoteId", "deliveryDate", "kind", "originalInvoiceId", "vatRegime", "sellerName", "sellerAddressLine1", "sellerAddressLine2", "sellerAddressPostalCode", "sellerAddressCity", "sellerAddressCountry", "sellerEmail", "sellerPhone", "sellerKvk", "sellerBtw", "sellerIban", "buyerName", "buyerAddressLine1", "buyerAddressLine2", "buyerAddressPostalCode", "buyerAddressCity", "buyerAddressCountry", "buyerEmail", "buyerKvk", "buyerBtw", "termsPdfFileId", "termsPdfVersionId", "termsRichText", "termsUrl", "sentAt", "paidAt", "canceledAt", "archivedAt", "createdBy", "updatedBy", "createdAt", "updatedAt", "amountPaidCents", "amountDueCents", "uncollectibleAt", "uncollectibleReason", "onlinePaymentEnabled" ] }, "invoiceStatusSchema": { "type": "string", "enum": [ "draft", "sent", "paid", "overdue", "canceled", "uncollectible" ] }, "isoDateOrNull": { "type": [ "string", "null" ], "pattern": "^\\d{4}-\\d{2}-\\d{2}$" }, "optionalUrl": { "type": [ "string", "null" ], "maxLength": 2000 }, "organizationIdSchema": { "type": "string", "format": "uuid" } } ``` # Get an invoice. (/docs/openapi/getInvoice) Return an invoice detail record for the specified organization. ## GET /v1/organizations/{organizationId}/invoices/{invoiceId} ```json { "operationId": "getInvoice", "summary": "Get an invoice.", "description": "Return an invoice detail record for the specified organization.", "tags": [ "Invoices" ], "parameters": [ { "in": "path", "name": "invoiceId", "required": true, "schema": { "$ref": "#/components/schemas/invoiceIdSchema" }, "example": "123" }, { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "responses": { "200": { "description": "Returns the requested invoice.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/invoiceDetailSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "discountKindSchema": { "type": "string", "enum": [ "none", "percentage", "fixed" ] }, "documentLineStockModeSchema": { "type": "string", "enum": [ "auto", "task", "none" ] }, "documentVatRegimeSchema": { "type": "string", "enum": [ "standard", "reverse_charge", "exempt", "kor", "margin_scheme" ] }, "invoiceChargeKindSchema": { "type": "string", "enum": [ "collection_costs", "statutory_interest", "late_fee" ] }, "invoiceChargeSchema": { "type": "object", "properties": { "amountCents": { "type": "integer", "exclusiveMinimum": 0 }, "createdAt": { "type": "string", "format": "date-time" }, "createdBy": { "type": [ "string", "null" ], "format": "uuid" }, "createdByRunStepId": { "type": [ "string", "null" ], "format": "uuid" }, "currency": { "$ref": "#/components/schemas/invoiceCurrencyCodeSchema" }, "id": { "type": "string", "format": "uuid" }, "invoiceId": { "$ref": "#/components/schemas/invoiceIdSchema" }, "kind": { "$ref": "#/components/schemas/invoiceChargeKindSchema" }, "organizationId": { "type": "string", "format": "uuid" }, "reason": { "type": [ "string", "null" ] }, "updatedAt": { "type": "string", "format": "date-time" } }, "required": [ "amountCents", "createdAt", "createdBy", "createdByRunStepId", "currency", "id", "invoiceId", "kind", "organizationId", "reason", "updatedAt" ] }, "invoiceCurrencyCodeSchema": { "type": "string", "pattern": "^[A-Z]{3}$" }, "invoiceDetailSchema": { "type": "object", "properties": { "charges": { "type": "array", "items": { "$ref": "#/components/schemas/invoiceChargeSchema" } }, "invoice": { "$ref": "#/components/schemas/invoiceSchema" }, "lines": { "type": "array", "items": { "$ref": "#/components/schemas/invoiceLineSchema" } }, "linkedTaskIds": { "type": "array", "items": { "type": "string", "format": "uuid" } }, "originalDisplayNumber": { "type": [ "string", "null" ] }, "payment": { "anyOf": [ { "$ref": "#/components/schemas/invoicePaymentSchema" }, { "type": "null" } ] }, "payments": { "type": "array", "items": { "$ref": "#/components/schemas/invoicePaymentSchema" } }, "statusHistory": { "type": "array", "items": { "$ref": "#/components/schemas/invoiceStatusHistoryEntrySchema" } }, "stockPreview": { "type": "array", "items": { "$ref": "#/components/schemas/invoiceStockPreviewLineSchema" } } }, "required": [ "charges", "invoice", "lines", "linkedTaskIds", "originalDisplayNumber", "payment", "payments", "statusHistory", "stockPreview" ] }, "invoiceDisplayNumberSchema": { "type": "string", "minLength": 1, "maxLength": 40 }, "invoiceIdSchema": { "type": "string", "format": "uuid" }, "invoiceKindSchema": { "type": "string", "enum": [ "invoice", "credit_note", "deposit" ] }, "invoiceLineIdSchema": { "type": "string", "format": "uuid" }, "invoiceLineKindSchema": { "type": "string", "enum": [ "line", "section_header" ] }, "invoiceLinePricingModeSchema": { "type": "string", "enum": [ "unit", "hourly", "fixed" ] }, "invoiceLineSchema": { "type": "object", "properties": { "createdAt": { "type": "string", "format": "date-time" }, "description": { "type": [ "string", "null" ] }, "discountKind": { "$ref": "#/components/schemas/discountKindSchema" }, "discountValue": { "type": "number" }, "id": { "$ref": "#/components/schemas/invoiceLineIdSchema" }, "invoiceId": { "$ref": "#/components/schemas/invoiceIdSchema" }, "kind": { "$ref": "#/components/schemas/invoiceLineKindSchema" }, "lineType": { "anyOf": [ { "$ref": "#/components/schemas/invoiceLineTypeSchema" }, { "type": "null" } ] }, "organizationId": { "type": "string", "format": "uuid" }, "position": { "type": "number" }, "pricingMode": { "$ref": "#/components/schemas/invoiceLinePricingModeSchema" }, "productId": { "type": [ "string", "null" ], "format": "uuid" }, "promotionId": { "type": [ "string", "null" ], "format": "uuid" }, "quantity": { "type": "number" }, "stockMode": { "$ref": "#/components/schemas/documentLineStockModeSchema" }, "taskId": { "type": [ "string", "null" ], "format": "uuid" }, "title": { "type": "string" }, "unit": { "type": [ "string", "null" ] }, "unitPrice": { "type": "number" }, "updatedAt": { "type": "string", "format": "date-time" }, "variantId": { "type": [ "string", "null" ], "format": "uuid" }, "vatRate": { "type": "number" } }, "required": [ "createdAt", "description", "discountKind", "discountValue", "id", "invoiceId", "kind", "lineType", "organizationId", "position", "pricingMode", "productId", "promotionId", "quantity", "stockMode", "taskId", "title", "unit", "unitPrice", "updatedAt", "variantId", "vatRate" ] }, "invoiceLineTypeSchema": { "type": "string", "enum": [ "product", "labor", "custom" ] }, "invoiceNumberSchema": { "type": "integer", "exclusiveMinimum": 0, "maximum": 9007199254740991 }, "invoicePaymentMethodSchema": { "type": "string", "enum": [ "stripe", "bank_transfer", "cash", "other" ] }, "invoicePaymentSchema": { "type": "object", "properties": { "amountCents": { "type": "integer", "minimum": 0 }, "applicationFeeBps": { "type": "integer", "minimum": 0, "maximum": 10000 }, "applicationFeeCents": { "type": "integer", "minimum": 0 }, "applicationFeeMode": { "type": "string", "enum": [ "percentage", "fixed" ] }, "applicationFeeNetCents": { "type": "integer", "minimum": 0 }, "applicationFeeRefundedCents": { "type": "integer", "minimum": 0 }, "applicationFeeVatCents": { "type": "integer", "minimum": 0 }, "applicationFeeVatTreatment": { "type": [ "string", "null" ], "enum": [ "domestic", "reverse_charge", "outside_scope" ] }, "canceledAt": { "type": [ "string", "null" ], "format": "date-time" }, "createdAt": { "type": "string", "format": "date-time" }, "currency": { "$ref": "#/components/schemas/invoiceCurrencyCodeSchema" }, "disputeLostAt": { "type": [ "string", "null" ], "format": "date-time" }, "disputeLostCents": { "type": "integer", "minimum": 0 }, "disputedAt": { "type": [ "string", "null" ], "format": "date-time" }, "failureCode": { "type": [ "string", "null" ] }, "failureMessage": { "type": [ "string", "null" ] }, "id": { "type": "string", "format": "uuid" }, "invoiceId": { "$ref": "#/components/schemas/invoiceIdSchema" }, "method": { "$ref": "#/components/schemas/invoicePaymentMethodSchema" }, "note": { "type": [ "string", "null" ] }, "organizationId": { "type": "string", "format": "uuid" }, "paidAt": { "type": [ "string", "null" ], "format": "date-time" }, "receivedOn": { "type": [ "string", "null" ], "format": "date" }, "recordedBy": { "type": [ "string", "null" ], "format": "uuid" }, "reference": { "type": [ "string", "null" ] }, "refundedAt": { "type": [ "string", "null" ], "format": "date-time" }, "refundedCents": { "type": "integer", "minimum": 0 }, "status": { "$ref": "#/components/schemas/invoicePaymentStatusSchema" }, "stripeAccountId": { "type": [ "string", "null" ], "minLength": 1, "maxLength": 255 }, "stripeChargeId": { "type": [ "string", "null" ], "minLength": 1, "maxLength": 255 }, "stripePaymentIntentId": { "type": [ "string", "null" ], "minLength": 1, "maxLength": 255 }, "updatedAt": { "type": "string", "format": "date-time" } }, "required": [ "amountCents", "applicationFeeBps", "applicationFeeCents", "applicationFeeMode", "applicationFeeNetCents", "applicationFeeRefundedCents", "applicationFeeVatCents", "applicationFeeVatTreatment", "canceledAt", "createdAt", "currency", "disputeLostAt", "disputeLostCents", "disputedAt", "failureCode", "failureMessage", "id", "invoiceId", "method", "note", "organizationId", "paidAt", "receivedOn", "recordedBy", "reference", "refundedAt", "refundedCents", "status", "stripeAccountId", "stripeChargeId", "stripePaymentIntentId", "updatedAt" ] }, "invoicePaymentStatusSchema": { "type": "string", "enum": [ "requires_payment_method", "requires_action", "processing", "succeeded", "canceled", "failed", "refunded", "disputed" ] }, "invoiceSchema": { "type": "object", "properties": { "id": { "$ref": "#/components/schemas/invoiceIdSchema" }, "organizationId": { "type": "string", "format": "uuid" }, "customerId": { "type": "string", "format": "uuid" }, "customerContactId": { "type": [ "string", "null" ], "format": "uuid" }, "customerReference": { "type": [ "string", "null" ] }, "number": { "anyOf": [ { "$ref": "#/components/schemas/invoiceNumberSchema" }, { "type": "null" } ] }, "numberYear": { "type": [ "integer", "null" ], "exclusiveMinimum": 0 }, "displayNumber": { "anyOf": [ { "$ref": "#/components/schemas/invoiceDisplayNumberSchema" }, { "type": "null" } ] }, "status": { "$ref": "#/components/schemas/invoiceStatusSchema" }, "dueDays": { "type": [ "integer", "null" ], "minimum": 0, "maximum": 365 }, "invoiceTemplateId": { "type": [ "string", "null" ], "format": "uuid" }, "title": { "type": "string" }, "intro": { "type": [ "string", "null" ] }, "terms": { "type": [ "string", "null" ] }, "currency": { "$ref": "#/components/schemas/invoiceCurrencyCodeSchema" }, "vatInclusive": { "type": "boolean" }, "discountKind": { "$ref": "#/components/schemas/discountKindSchema" }, "discountValue": { "type": "number" }, "discountDescription": { "type": [ "string", "null" ], "maxLength": 500 }, "promotionId": { "type": [ "string", "null" ], "format": "uuid" }, "subtotalExcl": { "type": "number" }, "vatTotal": { "type": "number" }, "totalIncl": { "type": "number" }, "issueDate": { "type": [ "string", "null" ] }, "dueDate": { "type": [ "string", "null" ] }, "publicMessage": { "type": [ "string", "null" ] }, "footerText": { "type": [ "string", "null" ] }, "sourceQuoteId": { "type": [ "string", "null" ], "format": "uuid" }, "deliveryDate": { "type": [ "string", "null" ] }, "kind": { "$ref": "#/components/schemas/invoiceKindSchema" }, "originalInvoiceId": { "type": [ "string", "null" ], "format": "uuid" }, "vatRegime": { "$ref": "#/components/schemas/documentVatRegimeSchema" }, "sellerName": { "type": [ "string", "null" ] }, "sellerAddressLine1": { "type": [ "string", "null" ] }, "sellerAddressLine2": { "type": [ "string", "null" ] }, "sellerAddressPostalCode": { "type": [ "string", "null" ] }, "sellerAddressCity": { "type": [ "string", "null" ] }, "sellerAddressCountry": { "type": [ "string", "null" ] }, "sellerEmail": { "type": [ "string", "null" ] }, "sellerPhone": { "type": [ "string", "null" ] }, "sellerKvk": { "type": [ "string", "null" ] }, "sellerBtw": { "type": [ "string", "null" ] }, "sellerIban": { "type": [ "string", "null" ] }, "buyerName": { "type": [ "string", "null" ] }, "buyerAddressLine1": { "type": [ "string", "null" ] }, "buyerAddressLine2": { "type": [ "string", "null" ] }, "buyerAddressPostalCode": { "type": [ "string", "null" ] }, "buyerAddressCity": { "type": [ "string", "null" ] }, "buyerAddressCountry": { "type": [ "string", "null" ] }, "buyerEmail": { "type": [ "string", "null" ] }, "buyerKvk": { "type": [ "string", "null" ] }, "buyerBtw": { "type": [ "string", "null" ] }, "termsPdfFileId": { "type": [ "string", "null" ], "format": "uuid" }, "termsPdfVersionId": { "type": [ "string", "null" ], "format": "uuid" }, "termsRichText": { "type": [ "string", "null" ], "maxLength": 100000 }, "termsUrl": { "type": [ "string", "null" ] }, "sentAt": { "type": [ "string", "null" ], "format": "date-time" }, "paidAt": { "type": [ "string", "null" ], "format": "date-time" }, "canceledAt": { "type": [ "string", "null" ], "format": "date-time" }, "archivedAt": { "type": [ "string", "null" ], "format": "date-time" }, "createdBy": { "type": [ "string", "null" ], "format": "uuid" }, "updatedBy": { "type": [ "string", "null" ], "format": "uuid" }, "createdAt": { "type": "string", "format": "date-time" }, "updatedAt": { "type": "string", "format": "date-time" }, "amountPaidCents": { "type": "integer", "minimum": 0 }, "amountDueCents": { "type": "integer", "minimum": 0 }, "uncollectibleAt": { "type": [ "string", "null" ], "format": "date-time" }, "uncollectibleReason": { "type": [ "string", "null" ] }, "onlinePaymentEnabled": { "type": "boolean", "default": true } }, "required": [ "id", "organizationId", "customerId", "customerContactId", "customerReference", "number", "numberYear", "displayNumber", "status", "dueDays", "invoiceTemplateId", "title", "intro", "terms", "currency", "vatInclusive", "discountKind", "discountValue", "discountDescription", "promotionId", "subtotalExcl", "vatTotal", "totalIncl", "issueDate", "dueDate", "publicMessage", "footerText", "sourceQuoteId", "deliveryDate", "kind", "originalInvoiceId", "vatRegime", "sellerName", "sellerAddressLine1", "sellerAddressLine2", "sellerAddressPostalCode", "sellerAddressCity", "sellerAddressCountry", "sellerEmail", "sellerPhone", "sellerKvk", "sellerBtw", "sellerIban", "buyerName", "buyerAddressLine1", "buyerAddressLine2", "buyerAddressPostalCode", "buyerAddressCity", "buyerAddressCountry", "buyerEmail", "buyerKvk", "buyerBtw", "termsPdfFileId", "termsPdfVersionId", "termsRichText", "termsUrl", "sentAt", "paidAt", "canceledAt", "archivedAt", "createdBy", "updatedBy", "createdAt", "updatedAt", "amountPaidCents", "amountDueCents", "uncollectibleAt", "uncollectibleReason", "onlinePaymentEnabled" ] }, "invoiceStatusHistoryEntrySchema": { "type": "object", "properties": { "actorUserId": { "type": [ "string", "null" ], "format": "uuid" }, "createdAt": { "type": "string", "format": "date-time" }, "fromStatus": { "anyOf": [ { "$ref": "#/components/schemas/invoiceStatusSchema" }, { "type": "null" } ] }, "id": { "type": "string", "format": "uuid" }, "invoiceId": { "$ref": "#/components/schemas/invoiceIdSchema" }, "organizationId": { "type": "string", "format": "uuid" }, "reason": { "type": [ "string", "null" ] }, "toStatus": { "$ref": "#/components/schemas/invoiceStatusSchema" } }, "required": [ "actorUserId", "createdAt", "fromStatus", "id", "invoiceId", "organizationId", "reason", "toStatus" ] }, "invoiceStatusSchema": { "type": "string", "enum": [ "draft", "sent", "paid", "overdue", "canceled", "uncollectible" ] }, "invoiceStockPreviewLineSchema": { "type": "object", "properties": { "availableQuantity": { "type": "number", "minimum": 0 }, "lineId": { "type": "string", "format": "uuid" }, "productId": { "type": "string", "format": "uuid" }, "requestedQuantity": { "type": "number", "minimum": 0 }, "shortQuantity": { "type": "number", "exclusiveMinimum": 0 }, "title": { "type": "string" } }, "required": [ "availableQuantity", "lineId", "productId", "requestedQuantity", "shortQuantity", "title" ] }, "organizationIdSchema": { "type": "string", "format": "uuid" } } ``` # Update an invoice. (/docs/openapi/updateInvoice) Update an invoice for the specified organization. ## PATCH /v1/organizations/{organizationId}/invoices/{invoiceId} ```json { "operationId": "updateInvoice", "summary": "Update an invoice.", "description": "Update an invoice for the specified organization.", "tags": [ "Invoices" ], "parameters": [ { "in": "path", "name": "invoiceId", "required": true, "schema": { "$ref": "#/components/schemas/invoiceIdSchema" }, "example": "123" }, { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/invoicePatchSchema" } } }, "description": "The invoice patch payload." }, "responses": { "200": { "description": "Returns the updated invoice.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/invoiceSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "discountKindSchema": { "type": "string", "enum": [ "none", "percentage", "fixed" ] }, "documentLineStockModeSchema": { "type": "string", "enum": [ "auto", "task", "none" ] }, "documentVatRegimeSchema": { "type": "string", "enum": [ "standard", "reverse_charge", "exempt", "kor", "margin_scheme" ] }, "invoiceCurrencyCodeSchema": { "type": "string", "pattern": "^[A-Z]{3}$" }, "invoiceDisplayNumberSchema": { "type": "string", "minLength": 1, "maxLength": 40 }, "invoiceIdSchema": { "type": "string", "format": "uuid" }, "invoiceKindSchema": { "type": "string", "enum": [ "invoice", "credit_note", "deposit" ] }, "invoiceLineIdSchema": { "type": "string", "format": "uuid" }, "invoiceLineInputSchema": { "type": "object", "properties": { "description": { "type": [ "string", "null" ], "maxLength": 20000 }, "discountKind": { "allOf": [ { "$ref": "#/components/schemas/discountKindSchema" } ] }, "discountValue": { "type": "number", "minimum": 0 }, "expenseId": { "type": "string", "format": "uuid" }, "id": { "allOf": [ { "$ref": "#/components/schemas/invoiceLineIdSchema" } ] }, "kind": { "allOf": [ { "$ref": "#/components/schemas/invoiceLineKindSchema" } ] }, "lineType": { "anyOf": [ { "$ref": "#/components/schemas/invoiceLineTypeSchema" }, { "type": "null" } ] }, "position": { "type": "number" }, "pricingMode": { "allOf": [ { "$ref": "#/components/schemas/invoiceLinePricingModeSchema" } ] }, "productId": { "type": [ "string", "null" ], "format": "uuid" }, "promotionId": { "type": [ "string", "null" ], "format": "uuid" }, "quantity": { "type": "number", "minimum": 0 }, "sourceTaskMaterialId": { "type": "string", "format": "uuid" }, "sourceTimeEntryIds": { "type": "array", "items": { "type": "string", "format": "uuid" }, "maxItems": 2000 }, "stockMode": { "allOf": [ { "$ref": "#/components/schemas/documentLineStockModeSchema" } ] }, "taskId": { "type": [ "string", "null" ], "format": "uuid" }, "title": { "type": "string", "minLength": 1, "maxLength": 500 }, "unit": { "type": [ "string", "null" ], "maxLength": 40 }, "unitPrice": { "type": "number" }, "variantId": { "type": [ "string", "null" ], "format": "uuid" }, "vatRate": { "type": "number", "minimum": 0, "maximum": 100 } }, "required": [ "title" ] }, "invoiceLineKindSchema": { "type": "string", "enum": [ "line", "section_header" ] }, "invoiceLinePricingModeSchema": { "type": "string", "enum": [ "unit", "hourly", "fixed" ] }, "invoiceLineTypeSchema": { "type": "string", "enum": [ "product", "labor", "custom" ] }, "invoiceNumberSchema": { "type": "integer", "exclusiveMinimum": 0, "maximum": 9007199254740991 }, "invoicePatchSchema": { "type": "object", "properties": { "customerId": { "type": "string", "format": "uuid" }, "customerContactId": { "type": [ "string", "null" ], "format": "uuid" }, "customerReference": { "type": [ "string", "null" ], "maxLength": 240 }, "title": { "type": "string", "minLength": 1, "maxLength": 240 }, "intro": { "type": [ "string", "null" ], "maxLength": 20000 }, "terms": { "type": [ "string", "null" ], "maxLength": 20000 }, "issueDate": { "allOf": [ { "$ref": "#/components/schemas/isoDateOrNull" } ] }, "dueDate": { "allOf": [ { "$ref": "#/components/schemas/isoDateOrNull" } ] }, "currency": { "allOf": [ { "$ref": "#/components/schemas/invoiceCurrencyCodeSchema" } ] }, "vatInclusive": { "type": "boolean" }, "onlinePaymentEnabled": { "type": "boolean" }, "discountKind": { "allOf": [ { "$ref": "#/components/schemas/discountKindSchema" } ] }, "discountValue": { "type": "number", "minimum": 0 }, "discountDescription": { "type": [ "string", "null" ], "maxLength": 500 }, "promotionId": { "type": [ "string", "null" ], "format": "uuid" }, "publicMessage": { "type": [ "string", "null" ], "maxLength": 20000 }, "footerText": { "type": [ "string", "null" ], "maxLength": 20000 }, "dueDays": { "type": [ "integer", "null" ], "minimum": 0, "maximum": 365 }, "invoiceTemplateId": { "type": [ "string", "null" ], "format": "uuid" }, "sourceQuoteId": { "type": [ "string", "null" ], "format": "uuid" }, "deliveryDate": { "allOf": [ { "$ref": "#/components/schemas/isoDateOrNull" } ] }, "kind": { "allOf": [ { "$ref": "#/components/schemas/invoiceKindSchema" } ] }, "originalInvoiceId": { "type": [ "string", "null" ], "format": "uuid" }, "vatRegime": { "allOf": [ { "$ref": "#/components/schemas/documentVatRegimeSchema" } ] }, "termsPdfFileId": { "type": [ "string", "null" ], "format": "uuid" }, "termsPdfVersionId": { "type": [ "string", "null" ], "format": "uuid" }, "termsRichText": { "type": [ "string", "null" ], "maxLength": 100000 }, "termsUrl": { "allOf": [ { "$ref": "#/components/schemas/optionalUrl" } ] }, "lines": { "type": "array", "items": { "$ref": "#/components/schemas/invoiceLineInputSchema" }, "maxItems": 500 }, "linkedTaskIds": { "type": "array", "items": { "type": "string", "format": "uuid" }, "maxItems": 200 } } }, "invoiceSchema": { "type": "object", "properties": { "id": { "$ref": "#/components/schemas/invoiceIdSchema" }, "organizationId": { "type": "string", "format": "uuid" }, "customerId": { "type": "string", "format": "uuid" }, "customerContactId": { "type": [ "string", "null" ], "format": "uuid" }, "customerReference": { "type": [ "string", "null" ] }, "number": { "anyOf": [ { "$ref": "#/components/schemas/invoiceNumberSchema" }, { "type": "null" } ] }, "numberYear": { "type": [ "integer", "null" ], "exclusiveMinimum": 0 }, "displayNumber": { "anyOf": [ { "$ref": "#/components/schemas/invoiceDisplayNumberSchema" }, { "type": "null" } ] }, "status": { "$ref": "#/components/schemas/invoiceStatusSchema" }, "dueDays": { "type": [ "integer", "null" ], "minimum": 0, "maximum": 365 }, "invoiceTemplateId": { "type": [ "string", "null" ], "format": "uuid" }, "title": { "type": "string" }, "intro": { "type": [ "string", "null" ] }, "terms": { "type": [ "string", "null" ] }, "currency": { "$ref": "#/components/schemas/invoiceCurrencyCodeSchema" }, "vatInclusive": { "type": "boolean" }, "discountKind": { "$ref": "#/components/schemas/discountKindSchema" }, "discountValue": { "type": "number" }, "discountDescription": { "type": [ "string", "null" ], "maxLength": 500 }, "promotionId": { "type": [ "string", "null" ], "format": "uuid" }, "subtotalExcl": { "type": "number" }, "vatTotal": { "type": "number" }, "totalIncl": { "type": "number" }, "issueDate": { "type": [ "string", "null" ] }, "dueDate": { "type": [ "string", "null" ] }, "publicMessage": { "type": [ "string", "null" ] }, "footerText": { "type": [ "string", "null" ] }, "sourceQuoteId": { "type": [ "string", "null" ], "format": "uuid" }, "deliveryDate": { "type": [ "string", "null" ] }, "kind": { "$ref": "#/components/schemas/invoiceKindSchema" }, "originalInvoiceId": { "type": [ "string", "null" ], "format": "uuid" }, "vatRegime": { "$ref": "#/components/schemas/documentVatRegimeSchema" }, "sellerName": { "type": [ "string", "null" ] }, "sellerAddressLine1": { "type": [ "string", "null" ] }, "sellerAddressLine2": { "type": [ "string", "null" ] }, "sellerAddressPostalCode": { "type": [ "string", "null" ] }, "sellerAddressCity": { "type": [ "string", "null" ] }, "sellerAddressCountry": { "type": [ "string", "null" ] }, "sellerEmail": { "type": [ "string", "null" ] }, "sellerPhone": { "type": [ "string", "null" ] }, "sellerKvk": { "type": [ "string", "null" ] }, "sellerBtw": { "type": [ "string", "null" ] }, "sellerIban": { "type": [ "string", "null" ] }, "buyerName": { "type": [ "string", "null" ] }, "buyerAddressLine1": { "type": [ "string", "null" ] }, "buyerAddressLine2": { "type": [ "string", "null" ] }, "buyerAddressPostalCode": { "type": [ "string", "null" ] }, "buyerAddressCity": { "type": [ "string", "null" ] }, "buyerAddressCountry": { "type": [ "string", "null" ] }, "buyerEmail": { "type": [ "string", "null" ] }, "buyerKvk": { "type": [ "string", "null" ] }, "buyerBtw": { "type": [ "string", "null" ] }, "termsPdfFileId": { "type": [ "string", "null" ], "format": "uuid" }, "termsPdfVersionId": { "type": [ "string", "null" ], "format": "uuid" }, "termsRichText": { "type": [ "string", "null" ], "maxLength": 100000 }, "termsUrl": { "type": [ "string", "null" ] }, "sentAt": { "type": [ "string", "null" ], "format": "date-time" }, "paidAt": { "type": [ "string", "null" ], "format": "date-time" }, "canceledAt": { "type": [ "string", "null" ], "format": "date-time" }, "archivedAt": { "type": [ "string", "null" ], "format": "date-time" }, "createdBy": { "type": [ "string", "null" ], "format": "uuid" }, "updatedBy": { "type": [ "string", "null" ], "format": "uuid" }, "createdAt": { "type": "string", "format": "date-time" }, "updatedAt": { "type": "string", "format": "date-time" }, "amountPaidCents": { "type": "integer", "minimum": 0 }, "amountDueCents": { "type": "integer", "minimum": 0 }, "uncollectibleAt": { "type": [ "string", "null" ], "format": "date-time" }, "uncollectibleReason": { "type": [ "string", "null" ] }, "onlinePaymentEnabled": { "type": "boolean", "default": true } }, "required": [ "id", "organizationId", "customerId", "customerContactId", "customerReference", "number", "numberYear", "displayNumber", "status", "dueDays", "invoiceTemplateId", "title", "intro", "terms", "currency", "vatInclusive", "discountKind", "discountValue", "discountDescription", "promotionId", "subtotalExcl", "vatTotal", "totalIncl", "issueDate", "dueDate", "publicMessage", "footerText", "sourceQuoteId", "deliveryDate", "kind", "originalInvoiceId", "vatRegime", "sellerName", "sellerAddressLine1", "sellerAddressLine2", "sellerAddressPostalCode", "sellerAddressCity", "sellerAddressCountry", "sellerEmail", "sellerPhone", "sellerKvk", "sellerBtw", "sellerIban", "buyerName", "buyerAddressLine1", "buyerAddressLine2", "buyerAddressPostalCode", "buyerAddressCity", "buyerAddressCountry", "buyerEmail", "buyerKvk", "buyerBtw", "termsPdfFileId", "termsPdfVersionId", "termsRichText", "termsUrl", "sentAt", "paidAt", "canceledAt", "archivedAt", "createdBy", "updatedBy", "createdAt", "updatedAt", "amountPaidCents", "amountDueCents", "uncollectibleAt", "uncollectibleReason", "onlinePaymentEnabled" ] }, "invoiceStatusSchema": { "type": "string", "enum": [ "draft", "sent", "paid", "overdue", "canceled", "uncollectible" ] }, "isoDateOrNull": { "type": [ "string", "null" ], "pattern": "^\\d{4}-\\d{2}-\\d{2}$" }, "optionalUrl": { "type": [ "string", "null" ], "maxLength": 2000 }, "organizationIdSchema": { "type": "string", "format": "uuid" } } ``` # Delete an invoice. (/docs/openapi/deleteInvoice) Archive an invoice for the specified organization. ## DELETE /v1/organizations/{organizationId}/invoices/{invoiceId} ```json { "operationId": "deleteInvoice", "summary": "Delete an invoice.", "description": "Archive an invoice for the specified organization.", "tags": [ "Invoices" ], "parameters": [ { "in": "path", "name": "invoiceId", "required": true, "schema": { "$ref": "#/components/schemas/invoiceIdSchema" }, "example": "123" }, { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "responses": { "200": { "description": "Returns a success payload when the invoice is archived.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/publicApiSuccessResponseSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "invoiceIdSchema": { "type": "string", "format": "uuid" }, "organizationIdSchema": { "type": "string", "format": "uuid" }, "publicApiSuccessResponseSchema": { "type": "object", "properties": { "success": { "type": "boolean", "enum": [ true ] } }, "required": [ "success" ] } } ``` # Create an invoice from a quote. (/docs/openapi/createInvoiceFromQuote) Generate a draft invoice by copying the lines of an accepted quote. A quote is invoiced once: while an invoice made from it is open (not cancelled or archived), that invoice is returned instead of a new one. ## POST /v1/organizations/{organizationId}/invoices/from-quote ```json { "operationId": "createInvoiceFromQuote", "summary": "Create an invoice from a quote.", "description": "Generate a draft invoice by copying the lines of an accepted quote. A quote is invoiced once: while an invoice made from it is open (not cancelled or archived), that invoice is returned instead of a new one.", "tags": [ "Invoices" ], "parameters": [ { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/createInvoiceFromQuoteInputSchema" } } }, "description": "The source quote id." }, "responses": { "201": { "description": "Returns the created draft invoice, or the quote's existing invoice.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/invoiceSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "createInvoiceFromQuoteInputSchema": { "type": "object", "properties": { "depositLineTitle": { "allOf": [ { "$ref": "#/components/schemas/invoiceLineTitleTemplateSchema" } ] }, "quoteId": { "type": "string", "format": "uuid" } }, "required": [ "quoteId" ] }, "discountKindSchema": { "type": "string", "enum": [ "none", "percentage", "fixed" ] }, "documentVatRegimeSchema": { "type": "string", "enum": [ "standard", "reverse_charge", "exempt", "kor", "margin_scheme" ] }, "invoiceCurrencyCodeSchema": { "type": "string", "pattern": "^[A-Z]{3}$" }, "invoiceDisplayNumberSchema": { "type": "string", "minLength": 1, "maxLength": 40 }, "invoiceIdSchema": { "type": "string", "format": "uuid" }, "invoiceKindSchema": { "type": "string", "enum": [ "invoice", "credit_note", "deposit" ] }, "invoiceLineTitleTemplateSchema": { "type": "string", "minLength": 1, "maxLength": 240 }, "invoiceNumberSchema": { "type": "integer", "exclusiveMinimum": 0, "maximum": 9007199254740991 }, "invoiceSchema": { "type": "object", "properties": { "id": { "$ref": "#/components/schemas/invoiceIdSchema" }, "organizationId": { "type": "string", "format": "uuid" }, "customerId": { "type": "string", "format": "uuid" }, "customerContactId": { "type": [ "string", "null" ], "format": "uuid" }, "customerReference": { "type": [ "string", "null" ] }, "number": { "anyOf": [ { "$ref": "#/components/schemas/invoiceNumberSchema" }, { "type": "null" } ] }, "numberYear": { "type": [ "integer", "null" ], "exclusiveMinimum": 0 }, "displayNumber": { "anyOf": [ { "$ref": "#/components/schemas/invoiceDisplayNumberSchema" }, { "type": "null" } ] }, "status": { "$ref": "#/components/schemas/invoiceStatusSchema" }, "dueDays": { "type": [ "integer", "null" ], "minimum": 0, "maximum": 365 }, "invoiceTemplateId": { "type": [ "string", "null" ], "format": "uuid" }, "title": { "type": "string" }, "intro": { "type": [ "string", "null" ] }, "terms": { "type": [ "string", "null" ] }, "currency": { "$ref": "#/components/schemas/invoiceCurrencyCodeSchema" }, "vatInclusive": { "type": "boolean" }, "discountKind": { "$ref": "#/components/schemas/discountKindSchema" }, "discountValue": { "type": "number" }, "discountDescription": { "type": [ "string", "null" ], "maxLength": 500 }, "promotionId": { "type": [ "string", "null" ], "format": "uuid" }, "subtotalExcl": { "type": "number" }, "vatTotal": { "type": "number" }, "totalIncl": { "type": "number" }, "issueDate": { "type": [ "string", "null" ] }, "dueDate": { "type": [ "string", "null" ] }, "publicMessage": { "type": [ "string", "null" ] }, "footerText": { "type": [ "string", "null" ] }, "sourceQuoteId": { "type": [ "string", "null" ], "format": "uuid" }, "deliveryDate": { "type": [ "string", "null" ] }, "kind": { "$ref": "#/components/schemas/invoiceKindSchema" }, "originalInvoiceId": { "type": [ "string", "null" ], "format": "uuid" }, "vatRegime": { "$ref": "#/components/schemas/documentVatRegimeSchema" }, "sellerName": { "type": [ "string", "null" ] }, "sellerAddressLine1": { "type": [ "string", "null" ] }, "sellerAddressLine2": { "type": [ "string", "null" ] }, "sellerAddressPostalCode": { "type": [ "string", "null" ] }, "sellerAddressCity": { "type": [ "string", "null" ] }, "sellerAddressCountry": { "type": [ "string", "null" ] }, "sellerEmail": { "type": [ "string", "null" ] }, "sellerPhone": { "type": [ "string", "null" ] }, "sellerKvk": { "type": [ "string", "null" ] }, "sellerBtw": { "type": [ "string", "null" ] }, "sellerIban": { "type": [ "string", "null" ] }, "buyerName": { "type": [ "string", "null" ] }, "buyerAddressLine1": { "type": [ "string", "null" ] }, "buyerAddressLine2": { "type": [ "string", "null" ] }, "buyerAddressPostalCode": { "type": [ "string", "null" ] }, "buyerAddressCity": { "type": [ "string", "null" ] }, "buyerAddressCountry": { "type": [ "string", "null" ] }, "buyerEmail": { "type": [ "string", "null" ] }, "buyerKvk": { "type": [ "string", "null" ] }, "buyerBtw": { "type": [ "string", "null" ] }, "termsPdfFileId": { "type": [ "string", "null" ], "format": "uuid" }, "termsPdfVersionId": { "type": [ "string", "null" ], "format": "uuid" }, "termsRichText": { "type": [ "string", "null" ], "maxLength": 100000 }, "termsUrl": { "type": [ "string", "null" ] }, "sentAt": { "type": [ "string", "null" ], "format": "date-time" }, "paidAt": { "type": [ "string", "null" ], "format": "date-time" }, "canceledAt": { "type": [ "string", "null" ], "format": "date-time" }, "archivedAt": { "type": [ "string", "null" ], "format": "date-time" }, "createdBy": { "type": [ "string", "null" ], "format": "uuid" }, "updatedBy": { "type": [ "string", "null" ], "format": "uuid" }, "createdAt": { "type": "string", "format": "date-time" }, "updatedAt": { "type": "string", "format": "date-time" }, "amountPaidCents": { "type": "integer", "minimum": 0 }, "amountDueCents": { "type": "integer", "minimum": 0 }, "uncollectibleAt": { "type": [ "string", "null" ], "format": "date-time" }, "uncollectibleReason": { "type": [ "string", "null" ] }, "onlinePaymentEnabled": { "type": "boolean", "default": true } }, "required": [ "id", "organizationId", "customerId", "customerContactId", "customerReference", "number", "numberYear", "displayNumber", "status", "dueDays", "invoiceTemplateId", "title", "intro", "terms", "currency", "vatInclusive", "discountKind", "discountValue", "discountDescription", "promotionId", "subtotalExcl", "vatTotal", "totalIncl", "issueDate", "dueDate", "publicMessage", "footerText", "sourceQuoteId", "deliveryDate", "kind", "originalInvoiceId", "vatRegime", "sellerName", "sellerAddressLine1", "sellerAddressLine2", "sellerAddressPostalCode", "sellerAddressCity", "sellerAddressCountry", "sellerEmail", "sellerPhone", "sellerKvk", "sellerBtw", "sellerIban", "buyerName", "buyerAddressLine1", "buyerAddressLine2", "buyerAddressPostalCode", "buyerAddressCity", "buyerAddressCountry", "buyerEmail", "buyerKvk", "buyerBtw", "termsPdfFileId", "termsPdfVersionId", "termsRichText", "termsUrl", "sentAt", "paidAt", "canceledAt", "archivedAt", "createdBy", "updatedBy", "createdAt", "updatedAt", "amountPaidCents", "amountDueCents", "uncollectibleAt", "uncollectibleReason", "onlinePaymentEnabled" ] }, "invoiceStatusSchema": { "type": "string", "enum": [ "draft", "sent", "paid", "overdue", "canceled", "uncollectible" ] }, "organizationIdSchema": { "type": "string", "format": "uuid" } } ``` # Cancel an invoice. (/docs/openapi/cancelInvoice) Cancel a draft, sent, or overdue invoice. ## POST /v1/organizations/{organizationId}/invoices/{invoiceId}/cancel ```json { "operationId": "cancelInvoice", "summary": "Cancel an invoice.", "description": "Cancel a draft, sent, or overdue invoice.", "tags": [ "Invoices" ], "parameters": [ { "in": "path", "name": "invoiceId", "required": true, "schema": { "$ref": "#/components/schemas/invoiceIdSchema" }, "example": "123" }, { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/invoiceCancelInputSchema" } } }, "description": "Optional cancellation reason.", "required": true }, "responses": { "200": { "description": "Returns the cancelled invoice.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/invoiceSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "discountKindSchema": { "type": "string", "enum": [ "none", "percentage", "fixed" ] }, "documentVatRegimeSchema": { "type": "string", "enum": [ "standard", "reverse_charge", "exempt", "kor", "margin_scheme" ] }, "invoiceCancelInputSchema": { "type": "object", "properties": { "reason": { "type": [ "string", "null" ], "maxLength": 20000 }, "returnToStock": { "type": "boolean" } } }, "invoiceCurrencyCodeSchema": { "type": "string", "pattern": "^[A-Z]{3}$" }, "invoiceDisplayNumberSchema": { "type": "string", "minLength": 1, "maxLength": 40 }, "invoiceIdSchema": { "type": "string", "format": "uuid" }, "invoiceKindSchema": { "type": "string", "enum": [ "invoice", "credit_note", "deposit" ] }, "invoiceNumberSchema": { "type": "integer", "exclusiveMinimum": 0, "maximum": 9007199254740991 }, "invoiceSchema": { "type": "object", "properties": { "id": { "$ref": "#/components/schemas/invoiceIdSchema" }, "organizationId": { "type": "string", "format": "uuid" }, "customerId": { "type": "string", "format": "uuid" }, "customerContactId": { "type": [ "string", "null" ], "format": "uuid" }, "customerReference": { "type": [ "string", "null" ] }, "number": { "anyOf": [ { "$ref": "#/components/schemas/invoiceNumberSchema" }, { "type": "null" } ] }, "numberYear": { "type": [ "integer", "null" ], "exclusiveMinimum": 0 }, "displayNumber": { "anyOf": [ { "$ref": "#/components/schemas/invoiceDisplayNumberSchema" }, { "type": "null" } ] }, "status": { "$ref": "#/components/schemas/invoiceStatusSchema" }, "dueDays": { "type": [ "integer", "null" ], "minimum": 0, "maximum": 365 }, "invoiceTemplateId": { "type": [ "string", "null" ], "format": "uuid" }, "title": { "type": "string" }, "intro": { "type": [ "string", "null" ] }, "terms": { "type": [ "string", "null" ] }, "currency": { "$ref": "#/components/schemas/invoiceCurrencyCodeSchema" }, "vatInclusive": { "type": "boolean" }, "discountKind": { "$ref": "#/components/schemas/discountKindSchema" }, "discountValue": { "type": "number" }, "discountDescription": { "type": [ "string", "null" ], "maxLength": 500 }, "promotionId": { "type": [ "string", "null" ], "format": "uuid" }, "subtotalExcl": { "type": "number" }, "vatTotal": { "type": "number" }, "totalIncl": { "type": "number" }, "issueDate": { "type": [ "string", "null" ] }, "dueDate": { "type": [ "string", "null" ] }, "publicMessage": { "type": [ "string", "null" ] }, "footerText": { "type": [ "string", "null" ] }, "sourceQuoteId": { "type": [ "string", "null" ], "format": "uuid" }, "deliveryDate": { "type": [ "string", "null" ] }, "kind": { "$ref": "#/components/schemas/invoiceKindSchema" }, "originalInvoiceId": { "type": [ "string", "null" ], "format": "uuid" }, "vatRegime": { "$ref": "#/components/schemas/documentVatRegimeSchema" }, "sellerName": { "type": [ "string", "null" ] }, "sellerAddressLine1": { "type": [ "string", "null" ] }, "sellerAddressLine2": { "type": [ "string", "null" ] }, "sellerAddressPostalCode": { "type": [ "string", "null" ] }, "sellerAddressCity": { "type": [ "string", "null" ] }, "sellerAddressCountry": { "type": [ "string", "null" ] }, "sellerEmail": { "type": [ "string", "null" ] }, "sellerPhone": { "type": [ "string", "null" ] }, "sellerKvk": { "type": [ "string", "null" ] }, "sellerBtw": { "type": [ "string", "null" ] }, "sellerIban": { "type": [ "string", "null" ] }, "buyerName": { "type": [ "string", "null" ] }, "buyerAddressLine1": { "type": [ "string", "null" ] }, "buyerAddressLine2": { "type": [ "string", "null" ] }, "buyerAddressPostalCode": { "type": [ "string", "null" ] }, "buyerAddressCity": { "type": [ "string", "null" ] }, "buyerAddressCountry": { "type": [ "string", "null" ] }, "buyerEmail": { "type": [ "string", "null" ] }, "buyerKvk": { "type": [ "string", "null" ] }, "buyerBtw": { "type": [ "string", "null" ] }, "termsPdfFileId": { "type": [ "string", "null" ], "format": "uuid" }, "termsPdfVersionId": { "type": [ "string", "null" ], "format": "uuid" }, "termsRichText": { "type": [ "string", "null" ], "maxLength": 100000 }, "termsUrl": { "type": [ "string", "null" ] }, "sentAt": { "type": [ "string", "null" ], "format": "date-time" }, "paidAt": { "type": [ "string", "null" ], "format": "date-time" }, "canceledAt": { "type": [ "string", "null" ], "format": "date-time" }, "archivedAt": { "type": [ "string", "null" ], "format": "date-time" }, "createdBy": { "type": [ "string", "null" ], "format": "uuid" }, "updatedBy": { "type": [ "string", "null" ], "format": "uuid" }, "createdAt": { "type": "string", "format": "date-time" }, "updatedAt": { "type": "string", "format": "date-time" }, "amountPaidCents": { "type": "integer", "minimum": 0 }, "amountDueCents": { "type": "integer", "minimum": 0 }, "uncollectibleAt": { "type": [ "string", "null" ], "format": "date-time" }, "uncollectibleReason": { "type": [ "string", "null" ] }, "onlinePaymentEnabled": { "type": "boolean", "default": true } }, "required": [ "id", "organizationId", "customerId", "customerContactId", "customerReference", "number", "numberYear", "displayNumber", "status", "dueDays", "invoiceTemplateId", "title", "intro", "terms", "currency", "vatInclusive", "discountKind", "discountValue", "discountDescription", "promotionId", "subtotalExcl", "vatTotal", "totalIncl", "issueDate", "dueDate", "publicMessage", "footerText", "sourceQuoteId", "deliveryDate", "kind", "originalInvoiceId", "vatRegime", "sellerName", "sellerAddressLine1", "sellerAddressLine2", "sellerAddressPostalCode", "sellerAddressCity", "sellerAddressCountry", "sellerEmail", "sellerPhone", "sellerKvk", "sellerBtw", "sellerIban", "buyerName", "buyerAddressLine1", "buyerAddressLine2", "buyerAddressPostalCode", "buyerAddressCity", "buyerAddressCountry", "buyerEmail", "buyerKvk", "buyerBtw", "termsPdfFileId", "termsPdfVersionId", "termsRichText", "termsUrl", "sentAt", "paidAt", "canceledAt", "archivedAt", "createdBy", "updatedBy", "createdAt", "updatedAt", "amountPaidCents", "amountDueCents", "uncollectibleAt", "uncollectibleReason", "onlinePaymentEnabled" ] }, "invoiceStatusSchema": { "type": "string", "enum": [ "draft", "sent", "paid", "overdue", "canceled", "uncollectible" ] }, "organizationIdSchema": { "type": "string", "format": "uuid" } } ``` # Duplicate an invoice. (/docs/openapi/duplicateInvoice) Create a new draft invoice copied from an existing one. ## POST /v1/organizations/{organizationId}/invoices/{invoiceId}/duplicate ```json { "operationId": "duplicateInvoice", "summary": "Duplicate an invoice.", "description": "Create a new draft invoice copied from an existing one.", "tags": [ "Invoices" ], "parameters": [ { "in": "path", "name": "invoiceId", "required": true, "schema": { "$ref": "#/components/schemas/invoiceIdSchema" }, "example": "123" }, { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "responses": { "201": { "description": "Returns the duplicated draft invoice.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/invoiceSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "discountKindSchema": { "type": "string", "enum": [ "none", "percentage", "fixed" ] }, "documentVatRegimeSchema": { "type": "string", "enum": [ "standard", "reverse_charge", "exempt", "kor", "margin_scheme" ] }, "invoiceCurrencyCodeSchema": { "type": "string", "pattern": "^[A-Z]{3}$" }, "invoiceDisplayNumberSchema": { "type": "string", "minLength": 1, "maxLength": 40 }, "invoiceIdSchema": { "type": "string", "format": "uuid" }, "invoiceKindSchema": { "type": "string", "enum": [ "invoice", "credit_note", "deposit" ] }, "invoiceNumberSchema": { "type": "integer", "exclusiveMinimum": 0, "maximum": 9007199254740991 }, "invoiceSchema": { "type": "object", "properties": { "id": { "$ref": "#/components/schemas/invoiceIdSchema" }, "organizationId": { "type": "string", "format": "uuid" }, "customerId": { "type": "string", "format": "uuid" }, "customerContactId": { "type": [ "string", "null" ], "format": "uuid" }, "customerReference": { "type": [ "string", "null" ] }, "number": { "anyOf": [ { "$ref": "#/components/schemas/invoiceNumberSchema" }, { "type": "null" } ] }, "numberYear": { "type": [ "integer", "null" ], "exclusiveMinimum": 0 }, "displayNumber": { "anyOf": [ { "$ref": "#/components/schemas/invoiceDisplayNumberSchema" }, { "type": "null" } ] }, "status": { "$ref": "#/components/schemas/invoiceStatusSchema" }, "dueDays": { "type": [ "integer", "null" ], "minimum": 0, "maximum": 365 }, "invoiceTemplateId": { "type": [ "string", "null" ], "format": "uuid" }, "title": { "type": "string" }, "intro": { "type": [ "string", "null" ] }, "terms": { "type": [ "string", "null" ] }, "currency": { "$ref": "#/components/schemas/invoiceCurrencyCodeSchema" }, "vatInclusive": { "type": "boolean" }, "discountKind": { "$ref": "#/components/schemas/discountKindSchema" }, "discountValue": { "type": "number" }, "discountDescription": { "type": [ "string", "null" ], "maxLength": 500 }, "promotionId": { "type": [ "string", "null" ], "format": "uuid" }, "subtotalExcl": { "type": "number" }, "vatTotal": { "type": "number" }, "totalIncl": { "type": "number" }, "issueDate": { "type": [ "string", "null" ] }, "dueDate": { "type": [ "string", "null" ] }, "publicMessage": { "type": [ "string", "null" ] }, "footerText": { "type": [ "string", "null" ] }, "sourceQuoteId": { "type": [ "string", "null" ], "format": "uuid" }, "deliveryDate": { "type": [ "string", "null" ] }, "kind": { "$ref": "#/components/schemas/invoiceKindSchema" }, "originalInvoiceId": { "type": [ "string", "null" ], "format": "uuid" }, "vatRegime": { "$ref": "#/components/schemas/documentVatRegimeSchema" }, "sellerName": { "type": [ "string", "null" ] }, "sellerAddressLine1": { "type": [ "string", "null" ] }, "sellerAddressLine2": { "type": [ "string", "null" ] }, "sellerAddressPostalCode": { "type": [ "string", "null" ] }, "sellerAddressCity": { "type": [ "string", "null" ] }, "sellerAddressCountry": { "type": [ "string", "null" ] }, "sellerEmail": { "type": [ "string", "null" ] }, "sellerPhone": { "type": [ "string", "null" ] }, "sellerKvk": { "type": [ "string", "null" ] }, "sellerBtw": { "type": [ "string", "null" ] }, "sellerIban": { "type": [ "string", "null" ] }, "buyerName": { "type": [ "string", "null" ] }, "buyerAddressLine1": { "type": [ "string", "null" ] }, "buyerAddressLine2": { "type": [ "string", "null" ] }, "buyerAddressPostalCode": { "type": [ "string", "null" ] }, "buyerAddressCity": { "type": [ "string", "null" ] }, "buyerAddressCountry": { "type": [ "string", "null" ] }, "buyerEmail": { "type": [ "string", "null" ] }, "buyerKvk": { "type": [ "string", "null" ] }, "buyerBtw": { "type": [ "string", "null" ] }, "termsPdfFileId": { "type": [ "string", "null" ], "format": "uuid" }, "termsPdfVersionId": { "type": [ "string", "null" ], "format": "uuid" }, "termsRichText": { "type": [ "string", "null" ], "maxLength": 100000 }, "termsUrl": { "type": [ "string", "null" ] }, "sentAt": { "type": [ "string", "null" ], "format": "date-time" }, "paidAt": { "type": [ "string", "null" ], "format": "date-time" }, "canceledAt": { "type": [ "string", "null" ], "format": "date-time" }, "archivedAt": { "type": [ "string", "null" ], "format": "date-time" }, "createdBy": { "type": [ "string", "null" ], "format": "uuid" }, "updatedBy": { "type": [ "string", "null" ], "format": "uuid" }, "createdAt": { "type": "string", "format": "date-time" }, "updatedAt": { "type": "string", "format": "date-time" }, "amountPaidCents": { "type": "integer", "minimum": 0 }, "amountDueCents": { "type": "integer", "minimum": 0 }, "uncollectibleAt": { "type": [ "string", "null" ], "format": "date-time" }, "uncollectibleReason": { "type": [ "string", "null" ] }, "onlinePaymentEnabled": { "type": "boolean", "default": true } }, "required": [ "id", "organizationId", "customerId", "customerContactId", "customerReference", "number", "numberYear", "displayNumber", "status", "dueDays", "invoiceTemplateId", "title", "intro", "terms", "currency", "vatInclusive", "discountKind", "discountValue", "discountDescription", "promotionId", "subtotalExcl", "vatTotal", "totalIncl", "issueDate", "dueDate", "publicMessage", "footerText", "sourceQuoteId", "deliveryDate", "kind", "originalInvoiceId", "vatRegime", "sellerName", "sellerAddressLine1", "sellerAddressLine2", "sellerAddressPostalCode", "sellerAddressCity", "sellerAddressCountry", "sellerEmail", "sellerPhone", "sellerKvk", "sellerBtw", "sellerIban", "buyerName", "buyerAddressLine1", "buyerAddressLine2", "buyerAddressPostalCode", "buyerAddressCity", "buyerAddressCountry", "buyerEmail", "buyerKvk", "buyerBtw", "termsPdfFileId", "termsPdfVersionId", "termsRichText", "termsUrl", "sentAt", "paidAt", "canceledAt", "archivedAt", "createdBy", "updatedBy", "createdAt", "updatedAt", "amountPaidCents", "amountDueCents", "uncollectibleAt", "uncollectibleReason", "onlinePaymentEnabled" ] }, "invoiceStatusSchema": { "type": "string", "enum": [ "draft", "sent", "paid", "overdue", "canceled", "uncollectible" ] }, "organizationIdSchema": { "type": "string", "format": "uuid" } } ``` # Read a invoice lifecycle. (/docs/openapi/getInvoiceLifecycle) The invoice's lifecycle: the pinned workflow version, the current stage, the stage history, and what the lifecycle waits on now. `lifecycle` is null when no lifecycle run started for the invoice. ## GET /v1/organizations/{organizationId}/invoices/{invoiceId}/lifecycle ```json { "operationId": "getInvoiceLifecycle", "summary": "Read a invoice lifecycle.", "description": "The invoice's lifecycle: the pinned workflow version, the current stage, the stage history, and what the lifecycle waits on now. `lifecycle` is null when no lifecycle run started for the invoice.", "tags": [ "Invoices" ], "parameters": [ { "in": "path", "name": "invoiceId", "required": true, "schema": { "$ref": "#/components/schemas/invoiceIdSchema" }, "example": "123" }, { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "responses": { "200": { "description": "Returns the invoice's lifecycle.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/recordLifecycleSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "dateTimeSchema": { "type": "string" }, "invoiceIdSchema": { "type": "string", "format": "uuid" }, "organizationIdSchema": { "type": "string", "format": "uuid" }, "recordLifecycleSchema": { "type": "object", "properties": { "definition": { "type": [ "object", "null" ], "properties": { "id": { "type": "string", "format": "uuid" }, "key": { "type": "string" }, "name": { "type": "string" }, "source": { "type": "string", "enum": [ "organization", "record", "system", "template" ] }, "version": { "type": "integer", "exclusiveMinimum": 0 } }, "required": [ "id", "key", "name", "source", "version" ] }, "events": { "type": "array", "items": { "$ref": "#/components/schemas/workflowLifecycleEventSchema" } }, "lifecycle": { "anyOf": [ { "$ref": "#/components/schemas/workflowLifecycleSchema" }, { "type": "null" } ] }, "waiting": { "type": [ "object", "null" ], "properties": { "at": { "anyOf": [ { "$ref": "#/components/schemas/dateTimeSchema" }, { "type": "null" } ] }, "events": { "type": "array", "items": { "type": "string" } }, "kind": { "type": "string", "enum": [ "approval", "delay", "event", "manual" ] }, "nextStageKey": { "type": [ "string", "null" ] }, "nextStageLabel": { "type": [ "string", "null" ] } }, "required": [ "at", "events", "kind", "nextStageKey", "nextStageLabel" ] } }, "required": [ "definition", "events", "lifecycle", "waiting" ] }, "workflowLifecycleEventSchema": { "type": "object", "properties": { "actorCustomerContactId": { "type": [ "string", "null" ], "format": "uuid" }, "actorUserId": { "type": [ "string", "null" ], "format": "uuid" }, "clearedAt": { "anyOf": [ { "$ref": "#/components/schemas/dateTimeSchema" }, { "type": "null" } ] }, "createdAt": { "$ref": "#/components/schemas/dateTimeSchema" }, "display": { "type": "string", "enum": [ "main", "sub", "flag", "hidden" ] }, "id": { "type": "string", "format": "uuid" }, "label": { "type": "string", "minLength": 1 }, "lifecycleId": { "type": "string", "format": "uuid" }, "organizationId": { "type": "string", "format": "uuid" }, "outcome": { "type": "string", "enum": [ "progress", "success", "failure", "neutral" ] }, "reachedAt": { "$ref": "#/components/schemas/dateTimeSchema" }, "runStepId": { "type": [ "string", "null" ], "format": "uuid" }, "sourceRunId": { "type": [ "string", "null" ], "format": "uuid" }, "stageKey": { "type": "string", "minLength": 1 } }, "required": [ "actorCustomerContactId", "actorUserId", "clearedAt", "createdAt", "display", "id", "label", "lifecycleId", "organizationId", "outcome", "reachedAt", "runStepId", "sourceRunId", "stageKey" ] }, "workflowLifecycleSchema": { "type": "object", "properties": { "createdAt": { "$ref": "#/components/schemas/dateTimeSchema" }, "currentStageKey": { "type": [ "string", "null" ] }, "definitionId": { "type": "string", "format": "uuid" }, "definitionVersion": { "type": "integer", "exclusiveMinimum": 0 }, "endedAt": { "anyOf": [ { "$ref": "#/components/schemas/dateTimeSchema" }, { "type": "null" } ] }, "id": { "type": "string", "format": "uuid" }, "organizationId": { "type": "string", "format": "uuid" }, "pauseReason": { "type": [ "string", "null" ] }, "pausedAt": { "anyOf": [ { "$ref": "#/components/schemas/dateTimeSchema" }, { "type": "null" } ] }, "pausedBy": { "type": [ "string", "null" ], "format": "uuid" }, "pausedUntil": { "anyOf": [ { "$ref": "#/components/schemas/dateTimeSchema" }, { "type": "null" } ] }, "runId": { "type": [ "string", "null" ], "format": "uuid" }, "startedAt": { "$ref": "#/components/schemas/dateTimeSchema" }, "subjectId": { "type": "string", "format": "uuid" }, "subjectType": { "$ref": "#/components/schemas/workflowSubjectTypeSchema" }, "updatedAt": { "$ref": "#/components/schemas/dateTimeSchema" } }, "required": [ "createdAt", "currentStageKey", "definitionId", "definitionVersion", "endedAt", "id", "organizationId", "pauseReason", "pausedAt", "pausedBy", "pausedUntil", "runId", "startedAt", "subjectId", "subjectType", "updatedAt" ] }, "workflowSubjectTypeSchema": { "type": "string", "enum": [ "invoice", "order", "purchase_order", "quote", "shipment", "task" ] } } ``` # Mark an invoice paid. (/docs/openapi/markInvoicePaid) Transition a sent or overdue invoice to paid. ## POST /v1/organizations/{organizationId}/invoices/{invoiceId}/mark-paid ```json { "operationId": "markInvoicePaid", "summary": "Mark an invoice paid.", "description": "Transition a sent or overdue invoice to paid.", "tags": [ "Invoices" ], "parameters": [ { "in": "path", "name": "invoiceId", "required": true, "schema": { "$ref": "#/components/schemas/invoiceIdSchema" }, "example": "123" }, { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/invoiceMarkPaidInputSchema" } } }, "description": "Optional payment date and note.", "required": true }, "responses": { "200": { "description": "Returns the paid invoice.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/invoiceSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "discountKindSchema": { "type": "string", "enum": [ "none", "percentage", "fixed" ] }, "documentVatRegimeSchema": { "type": "string", "enum": [ "standard", "reverse_charge", "exempt", "kor", "margin_scheme" ] }, "invoiceCurrencyCodeSchema": { "type": "string", "pattern": "^[A-Z]{3}$" }, "invoiceDisplayNumberSchema": { "type": "string", "minLength": 1, "maxLength": 40 }, "invoiceIdSchema": { "type": "string", "format": "uuid" }, "invoiceKindSchema": { "type": "string", "enum": [ "invoice", "credit_note", "deposit" ] }, "invoiceMarkPaidInputSchema": { "type": "object", "properties": { "paidAt": { "allOf": [ { "$ref": "#/components/schemas/isoDateOrNull" } ] }, "reason": { "type": [ "string", "null" ], "maxLength": 2000 } } }, "invoiceNumberSchema": { "type": "integer", "exclusiveMinimum": 0, "maximum": 9007199254740991 }, "invoiceSchema": { "type": "object", "properties": { "id": { "$ref": "#/components/schemas/invoiceIdSchema" }, "organizationId": { "type": "string", "format": "uuid" }, "customerId": { "type": "string", "format": "uuid" }, "customerContactId": { "type": [ "string", "null" ], "format": "uuid" }, "customerReference": { "type": [ "string", "null" ] }, "number": { "anyOf": [ { "$ref": "#/components/schemas/invoiceNumberSchema" }, { "type": "null" } ] }, "numberYear": { "type": [ "integer", "null" ], "exclusiveMinimum": 0 }, "displayNumber": { "anyOf": [ { "$ref": "#/components/schemas/invoiceDisplayNumberSchema" }, { "type": "null" } ] }, "status": { "$ref": "#/components/schemas/invoiceStatusSchema" }, "dueDays": { "type": [ "integer", "null" ], "minimum": 0, "maximum": 365 }, "invoiceTemplateId": { "type": [ "string", "null" ], "format": "uuid" }, "title": { "type": "string" }, "intro": { "type": [ "string", "null" ] }, "terms": { "type": [ "string", "null" ] }, "currency": { "$ref": "#/components/schemas/invoiceCurrencyCodeSchema" }, "vatInclusive": { "type": "boolean" }, "discountKind": { "$ref": "#/components/schemas/discountKindSchema" }, "discountValue": { "type": "number" }, "discountDescription": { "type": [ "string", "null" ], "maxLength": 500 }, "promotionId": { "type": [ "string", "null" ], "format": "uuid" }, "subtotalExcl": { "type": "number" }, "vatTotal": { "type": "number" }, "totalIncl": { "type": "number" }, "issueDate": { "type": [ "string", "null" ] }, "dueDate": { "type": [ "string", "null" ] }, "publicMessage": { "type": [ "string", "null" ] }, "footerText": { "type": [ "string", "null" ] }, "sourceQuoteId": { "type": [ "string", "null" ], "format": "uuid" }, "deliveryDate": { "type": [ "string", "null" ] }, "kind": { "$ref": "#/components/schemas/invoiceKindSchema" }, "originalInvoiceId": { "type": [ "string", "null" ], "format": "uuid" }, "vatRegime": { "$ref": "#/components/schemas/documentVatRegimeSchema" }, "sellerName": { "type": [ "string", "null" ] }, "sellerAddressLine1": { "type": [ "string", "null" ] }, "sellerAddressLine2": { "type": [ "string", "null" ] }, "sellerAddressPostalCode": { "type": [ "string", "null" ] }, "sellerAddressCity": { "type": [ "string", "null" ] }, "sellerAddressCountry": { "type": [ "string", "null" ] }, "sellerEmail": { "type": [ "string", "null" ] }, "sellerPhone": { "type": [ "string", "null" ] }, "sellerKvk": { "type": [ "string", "null" ] }, "sellerBtw": { "type": [ "string", "null" ] }, "sellerIban": { "type": [ "string", "null" ] }, "buyerName": { "type": [ "string", "null" ] }, "buyerAddressLine1": { "type": [ "string", "null" ] }, "buyerAddressLine2": { "type": [ "string", "null" ] }, "buyerAddressPostalCode": { "type": [ "string", "null" ] }, "buyerAddressCity": { "type": [ "string", "null" ] }, "buyerAddressCountry": { "type": [ "string", "null" ] }, "buyerEmail": { "type": [ "string", "null" ] }, "buyerKvk": { "type": [ "string", "null" ] }, "buyerBtw": { "type": [ "string", "null" ] }, "termsPdfFileId": { "type": [ "string", "null" ], "format": "uuid" }, "termsPdfVersionId": { "type": [ "string", "null" ], "format": "uuid" }, "termsRichText": { "type": [ "string", "null" ], "maxLength": 100000 }, "termsUrl": { "type": [ "string", "null" ] }, "sentAt": { "type": [ "string", "null" ], "format": "date-time" }, "paidAt": { "type": [ "string", "null" ], "format": "date-time" }, "canceledAt": { "type": [ "string", "null" ], "format": "date-time" }, "archivedAt": { "type": [ "string", "null" ], "format": "date-time" }, "createdBy": { "type": [ "string", "null" ], "format": "uuid" }, "updatedBy": { "type": [ "string", "null" ], "format": "uuid" }, "createdAt": { "type": "string", "format": "date-time" }, "updatedAt": { "type": "string", "format": "date-time" }, "amountPaidCents": { "type": "integer", "minimum": 0 }, "amountDueCents": { "type": "integer", "minimum": 0 }, "uncollectibleAt": { "type": [ "string", "null" ], "format": "date-time" }, "uncollectibleReason": { "type": [ "string", "null" ] }, "onlinePaymentEnabled": { "type": "boolean", "default": true } }, "required": [ "id", "organizationId", "customerId", "customerContactId", "customerReference", "number", "numberYear", "displayNumber", "status", "dueDays", "invoiceTemplateId", "title", "intro", "terms", "currency", "vatInclusive", "discountKind", "discountValue", "discountDescription", "promotionId", "subtotalExcl", "vatTotal", "totalIncl", "issueDate", "dueDate", "publicMessage", "footerText", "sourceQuoteId", "deliveryDate", "kind", "originalInvoiceId", "vatRegime", "sellerName", "sellerAddressLine1", "sellerAddressLine2", "sellerAddressPostalCode", "sellerAddressCity", "sellerAddressCountry", "sellerEmail", "sellerPhone", "sellerKvk", "sellerBtw", "sellerIban", "buyerName", "buyerAddressLine1", "buyerAddressLine2", "buyerAddressPostalCode", "buyerAddressCity", "buyerAddressCountry", "buyerEmail", "buyerKvk", "buyerBtw", "termsPdfFileId", "termsPdfVersionId", "termsRichText", "termsUrl", "sentAt", "paidAt", "canceledAt", "archivedAt", "createdBy", "updatedBy", "createdAt", "updatedAt", "amountPaidCents", "amountDueCents", "uncollectibleAt", "uncollectibleReason", "onlinePaymentEnabled" ] }, "invoiceStatusSchema": { "type": "string", "enum": [ "draft", "sent", "paid", "overdue", "canceled", "uncollectible" ] }, "isoDateOrNull": { "type": [ "string", "null" ], "pattern": "^\\d{4}-\\d{2}-\\d{2}$" }, "organizationIdSchema": { "type": "string", "format": "uuid" } } ``` # List invoice notes. (/docs/openapi/listInvoiceNotes) Return internal notes for an invoice. ## GET /v1/organizations/{organizationId}/invoices/{invoiceId}/notes ```json { "operationId": "listInvoiceNotes", "summary": "List invoice notes.", "description": "Return internal notes for an invoice.", "tags": [ "Invoices" ], "parameters": [ { "in": "path", "name": "invoiceId", "required": true, "schema": { "$ref": "#/components/schemas/invoiceIdSchema" }, "example": "123" }, { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "responses": { "200": { "description": "Returns invoice notes.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/invoiceNotesResponseSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "collaborationBlockSchema": { "type": "object", "discriminator": { "propertyName": "type" }, "oneOf": [ { "type": "object", "properties": { "content": { "$ref": "#/components/schemas/collaborationInlineContentSchema" }, "type": { "type": "string", "enum": [ "paragraph" ] } }, "required": [ "content", "type" ], "additionalProperties": false }, { "type": "object", "properties": { "items": { "type": "array", "items": { "$ref": "#/components/schemas/collaborationInlineContentSchema" }, "minItems": 1, "maxItems": 500 }, "type": { "type": "string", "enum": [ "bullet-list" ] } }, "required": [ "items", "type" ], "additionalProperties": false }, { "type": "object", "properties": { "items": { "type": "array", "items": { "$ref": "#/components/schemas/collaborationInlineContentSchema" }, "minItems": 1, "maxItems": 500 }, "type": { "type": "string", "enum": [ "numbered-list" ] } }, "required": [ "items", "type" ], "additionalProperties": false } ] }, "collaborationCommentAuthorSchema": { "type": "object", "properties": { "avatarUrl": { "type": [ "string", "null" ], "format": "uri" }, "displayName": { "type": "string", "minLength": 1 }, "userId": { "type": "string", "format": "uuid" } }, "required": [ "avatarUrl", "displayName", "userId" ] }, "collaborationDocumentSchema": { "type": "object", "properties": { "blocks": { "type": "array", "items": { "$ref": "#/components/schemas/collaborationBlockSchema" }, "minItems": 1, "maxItems": 1000 }, "version": { "type": "integer", "enum": [ 1 ] } }, "required": [ "blocks", "version" ], "additionalProperties": false }, "collaborationInlineContentSchema": { "type": "array", "items": { "$ref": "#/components/schemas/collaborationInlineSchema" }, "minItems": 1, "maxItems": 1000 }, "collaborationInlineSchema": { "type": "object", "discriminator": { "propertyName": "type" }, "oneOf": [ { "type": "object", "properties": { "text": { "type": "string", "minLength": 1, "maxLength": 20000 }, "type": { "type": "string", "enum": [ "text" ] } }, "required": [ "text", "type" ], "additionalProperties": false }, { "type": "object", "properties": { "reference": { "$ref": "#/components/schemas/collaborationReferenceSchema" }, "type": { "type": "string", "enum": [ "reference" ] } }, "required": [ "reference", "type" ], "additionalProperties": false } ] }, "collaborationReferenceSchema": { "type": "object", "discriminator": { "propertyName": "kind" }, "oneOf": [ { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "kind": { "type": "string", "enum": [ "user" ] }, "label": { "type": "string", "minLength": 1, "maxLength": 500 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "path": { "type": "string", "pattern": "^/", "maxLength": 2000 } }, "required": [ "id", "kind", "label", "organizationId", "path" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "kind": { "type": "string", "enum": [ "team" ] }, "label": { "type": "string", "minLength": 1, "maxLength": 500 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "path": { "type": "string", "pattern": "^/", "maxLength": 2000 } }, "required": [ "id", "kind", "label", "organizationId", "path" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "kind": { "type": "string", "enum": [ "task" ] }, "label": { "type": "string", "minLength": 1, "maxLength": 500 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "path": { "type": "string", "pattern": "^/", "maxLength": 2000 } }, "required": [ "id", "kind", "label", "organizationId", "path" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "kind": { "type": "string", "enum": [ "customer" ] }, "label": { "type": "string", "minLength": 1, "maxLength": 500 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "path": { "type": "string", "pattern": "^/", "maxLength": 2000 } }, "required": [ "id", "kind", "label", "organizationId", "path" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "$ref": "#/components/schemas/quoteIdSchema" }, "kind": { "type": "string", "enum": [ "quote" ] }, "label": { "type": "string", "minLength": 1, "maxLength": 500 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "path": { "type": "string", "pattern": "^/", "maxLength": 2000 } }, "required": [ "id", "kind", "label", "organizationId", "path" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "$ref": "#/components/schemas/invoiceIdSchema" }, "kind": { "type": "string", "enum": [ "invoice" ] }, "label": { "type": "string", "minLength": 1, "maxLength": 500 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "path": { "type": "string", "pattern": "^/", "maxLength": 2000 } }, "required": [ "id", "kind", "label", "organizationId", "path" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "$ref": "#/components/schemas/productIdSchema" }, "kind": { "type": "string", "enum": [ "product" ] }, "label": { "type": "string", "minLength": 1, "maxLength": 500 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "path": { "type": "string", "pattern": "^/", "maxLength": 2000 } }, "required": [ "id", "kind", "label", "organizationId", "path" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "kind": { "type": "string", "enum": [ "customer-asset" ] }, "label": { "type": "string", "minLength": 1, "maxLength": 500 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "path": { "type": "string", "pattern": "^/", "maxLength": 2000 } }, "required": [ "id", "kind", "label", "organizationId", "path" ], "additionalProperties": false } ] }, "invoiceIdSchema": { "type": "string", "format": "uuid" }, "invoiceNoteSchema": { "type": "object", "properties": { "author": { "anyOf": [ { "$ref": "#/components/schemas/collaborationCommentAuthorSchema" }, { "type": "null" } ] }, "authorId": { "type": [ "string", "null" ], "format": "uuid" }, "contentDoc": { "$ref": "#/components/schemas/collaborationDocumentSchema" }, "contentText": { "type": "string", "minLength": 1, "maxLength": 20000 }, "createdAt": { "type": "string", "format": "date-time" }, "id": { "type": "string", "format": "uuid" }, "invoiceId": { "$ref": "#/components/schemas/invoiceIdSchema" }, "organizationId": { "type": "string", "format": "uuid" }, "updatedAt": { "type": "string", "format": "date-time" } }, "required": [ "author", "authorId", "contentDoc", "contentText", "createdAt", "id", "invoiceId", "organizationId", "updatedAt" ] }, "invoiceNotesResponseSchema": { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/components/schemas/invoiceNoteSchema" } }, "pagination": { "$ref": "#/components/schemas/listPaginationSchema" } }, "required": [ "data", "pagination" ] }, "listPaginationSchema": { "type": "object", "properties": { "hasMore": { "type": "boolean" }, "limit": { "type": "integer", "minimum": 1, "maximum": 100 }, "offset": { "type": "integer", "minimum": 0 }, "total": { "type": "integer", "minimum": 0 }, "totalIsEstimate": { "type": "boolean" } }, "required": [ "hasMore", "limit", "offset", "total" ] }, "organizationIdSchema": { "type": "string", "format": "uuid" }, "productIdSchema": { "type": "string", "format": "uuid" }, "quoteIdSchema": { "type": "string", "format": "uuid" } } ``` # Add an invoice note. (/docs/openapi/addInvoiceNote) Add an internal note to an invoice. ## POST /v1/organizations/{organizationId}/invoices/{invoiceId}/notes ```json { "operationId": "addInvoiceNote", "summary": "Add an invoice note.", "description": "Add an internal note to an invoice.", "tags": [ "Invoices" ], "parameters": [ { "in": "path", "name": "invoiceId", "required": true, "schema": { "$ref": "#/components/schemas/invoiceIdSchema" }, "example": "123" }, { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/invoiceNoteInputSchema" } } }, "description": "The invoice note payload." }, "responses": { "201": { "description": "Returns the created invoice note.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/invoiceNoteSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "collaborationBlockSchema": { "type": "object", "discriminator": { "propertyName": "type" }, "oneOf": [ { "type": "object", "properties": { "content": { "$ref": "#/components/schemas/collaborationInlineContentSchema" }, "type": { "type": "string", "enum": [ "paragraph" ] } }, "required": [ "content", "type" ], "additionalProperties": false }, { "type": "object", "properties": { "items": { "type": "array", "items": { "$ref": "#/components/schemas/collaborationInlineContentSchema" }, "minItems": 1, "maxItems": 500 }, "type": { "type": "string", "enum": [ "bullet-list" ] } }, "required": [ "items", "type" ], "additionalProperties": false }, { "type": "object", "properties": { "items": { "type": "array", "items": { "$ref": "#/components/schemas/collaborationInlineContentSchema" }, "minItems": 1, "maxItems": 500 }, "type": { "type": "string", "enum": [ "numbered-list" ] } }, "required": [ "items", "type" ], "additionalProperties": false } ] }, "collaborationCommentAuthorSchema": { "type": "object", "properties": { "avatarUrl": { "type": [ "string", "null" ], "format": "uri" }, "displayName": { "type": "string", "minLength": 1 }, "userId": { "type": "string", "format": "uuid" } }, "required": [ "avatarUrl", "displayName", "userId" ] }, "collaborationComposerInputSchema": { "type": "object", "properties": { "selections": { "type": "array", "items": { "$ref": "#/components/schemas/collaborationComposerSelectionSchema" }, "maxItems": 100 }, "text": { "type": "string", "minLength": 1, "maxLength": 20000 } }, "required": [ "selections", "text" ], "additionalProperties": false }, "collaborationComposerSelectionSchema": { "type": "object", "properties": { "end": { "type": "integer", "exclusiveMinimum": 0, "maximum": 20000 }, "reference": { "$ref": "#/components/schemas/collaborationReferenceIdSchema" }, "start": { "type": "integer", "minimum": 0, "maximum": 20000 } }, "required": [ "end", "reference", "start" ], "additionalProperties": false }, "collaborationDocumentSchema": { "type": "object", "properties": { "blocks": { "type": "array", "items": { "$ref": "#/components/schemas/collaborationBlockSchema" }, "minItems": 1, "maxItems": 1000 }, "version": { "type": "integer", "enum": [ 1 ] } }, "required": [ "blocks", "version" ], "additionalProperties": false }, "collaborationInlineContentSchema": { "type": "array", "items": { "$ref": "#/components/schemas/collaborationInlineSchema" }, "minItems": 1, "maxItems": 1000 }, "collaborationInlineSchema": { "type": "object", "discriminator": { "propertyName": "type" }, "oneOf": [ { "type": "object", "properties": { "text": { "type": "string", "minLength": 1, "maxLength": 20000 }, "type": { "type": "string", "enum": [ "text" ] } }, "required": [ "text", "type" ], "additionalProperties": false }, { "type": "object", "properties": { "reference": { "$ref": "#/components/schemas/collaborationReferenceSchema" }, "type": { "type": "string", "enum": [ "reference" ] } }, "required": [ "reference", "type" ], "additionalProperties": false } ] }, "collaborationReferenceIdSchema": { "type": "object", "discriminator": { "propertyName": "kind" }, "oneOf": [ { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "kind": { "type": "string", "enum": [ "user" ] } }, "required": [ "id", "kind" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "kind": { "type": "string", "enum": [ "team" ] } }, "required": [ "id", "kind" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "kind": { "type": "string", "enum": [ "task" ] } }, "required": [ "id", "kind" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "kind": { "type": "string", "enum": [ "customer" ] } }, "required": [ "id", "kind" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "$ref": "#/components/schemas/quoteIdSchema" }, "kind": { "type": "string", "enum": [ "quote" ] } }, "required": [ "id", "kind" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "$ref": "#/components/schemas/invoiceIdSchema" }, "kind": { "type": "string", "enum": [ "invoice" ] } }, "required": [ "id", "kind" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "$ref": "#/components/schemas/productIdSchema" }, "kind": { "type": "string", "enum": [ "product" ] } }, "required": [ "id", "kind" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "kind": { "type": "string", "enum": [ "customer-asset" ] } }, "required": [ "id", "kind" ], "additionalProperties": false } ] }, "collaborationReferenceSchema": { "type": "object", "discriminator": { "propertyName": "kind" }, "oneOf": [ { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "kind": { "type": "string", "enum": [ "user" ] }, "label": { "type": "string", "minLength": 1, "maxLength": 500 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "path": { "type": "string", "pattern": "^/", "maxLength": 2000 } }, "required": [ "id", "kind", "label", "organizationId", "path" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "kind": { "type": "string", "enum": [ "team" ] }, "label": { "type": "string", "minLength": 1, "maxLength": 500 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "path": { "type": "string", "pattern": "^/", "maxLength": 2000 } }, "required": [ "id", "kind", "label", "organizationId", "path" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "kind": { "type": "string", "enum": [ "task" ] }, "label": { "type": "string", "minLength": 1, "maxLength": 500 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "path": { "type": "string", "pattern": "^/", "maxLength": 2000 } }, "required": [ "id", "kind", "label", "organizationId", "path" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "kind": { "type": "string", "enum": [ "customer" ] }, "label": { "type": "string", "minLength": 1, "maxLength": 500 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "path": { "type": "string", "pattern": "^/", "maxLength": 2000 } }, "required": [ "id", "kind", "label", "organizationId", "path" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "$ref": "#/components/schemas/quoteIdSchema" }, "kind": { "type": "string", "enum": [ "quote" ] }, "label": { "type": "string", "minLength": 1, "maxLength": 500 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "path": { "type": "string", "pattern": "^/", "maxLength": 2000 } }, "required": [ "id", "kind", "label", "organizationId", "path" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "$ref": "#/components/schemas/invoiceIdSchema" }, "kind": { "type": "string", "enum": [ "invoice" ] }, "label": { "type": "string", "minLength": 1, "maxLength": 500 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "path": { "type": "string", "pattern": "^/", "maxLength": 2000 } }, "required": [ "id", "kind", "label", "organizationId", "path" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "$ref": "#/components/schemas/productIdSchema" }, "kind": { "type": "string", "enum": [ "product" ] }, "label": { "type": "string", "minLength": 1, "maxLength": 500 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "path": { "type": "string", "pattern": "^/", "maxLength": 2000 } }, "required": [ "id", "kind", "label", "organizationId", "path" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "kind": { "type": "string", "enum": [ "customer-asset" ] }, "label": { "type": "string", "minLength": 1, "maxLength": 500 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "path": { "type": "string", "pattern": "^/", "maxLength": 2000 } }, "required": [ "id", "kind", "label", "organizationId", "path" ], "additionalProperties": false } ] }, "invoiceIdSchema": { "type": "string", "format": "uuid" }, "invoiceNoteInputSchema": { "$ref": "#/components/schemas/collaborationComposerInputSchema" }, "invoiceNoteSchema": { "type": "object", "properties": { "author": { "anyOf": [ { "$ref": "#/components/schemas/collaborationCommentAuthorSchema" }, { "type": "null" } ] }, "authorId": { "type": [ "string", "null" ], "format": "uuid" }, "contentDoc": { "$ref": "#/components/schemas/collaborationDocumentSchema" }, "contentText": { "type": "string", "minLength": 1, "maxLength": 20000 }, "createdAt": { "type": "string", "format": "date-time" }, "id": { "type": "string", "format": "uuid" }, "invoiceId": { "$ref": "#/components/schemas/invoiceIdSchema" }, "organizationId": { "type": "string", "format": "uuid" }, "updatedAt": { "type": "string", "format": "date-time" } }, "required": [ "author", "authorId", "contentDoc", "contentText", "createdAt", "id", "invoiceId", "organizationId", "updatedAt" ] }, "organizationIdSchema": { "type": "string", "format": "uuid" }, "productIdSchema": { "type": "string", "format": "uuid" }, "quoteIdSchema": { "type": "string", "format": "uuid" } } ``` # Pause invoice reminders. (/docs/openapi/pauseInvoiceReminders) Stops payment reminders on the invoice, for example while a dispute or payment plan runs. With `until`, reminders resume on that date. ## POST /v1/organizations/{organizationId}/invoices/{invoiceId}/pause-reminders ```json { "operationId": "pauseInvoiceReminders", "summary": "Pause invoice reminders.", "description": "Stops payment reminders on the invoice, for example while a dispute or payment plan runs. With `until`, reminders resume on that date.", "tags": [ "Invoices" ], "parameters": [ { "in": "path", "name": "invoiceId", "required": true, "schema": { "$ref": "#/components/schemas/invoiceIdSchema" }, "example": "123" }, { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/pauseInvoiceRemindersInputSchema" } } }, "description": "Why reminders pause, and optionally the date they resume.", "required": true }, "responses": { "200": { "description": "Reminders were paused.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/publicApiSuccessResponseSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "invoiceIdSchema": { "type": "string", "format": "uuid" }, "organizationIdSchema": { "type": "string", "format": "uuid" }, "pauseInvoiceRemindersInputSchema": { "type": "object", "properties": { "reason": { "type": "string", "minLength": 1, "maxLength": 500 }, "until": { "type": [ "string", "null" ], "format": "date" } }, "required": [ "reason" ] }, "publicApiSuccessResponseSchema": { "type": "object", "properties": { "success": { "type": "boolean", "enum": [ true ] } }, "required": [ "success" ] } } ``` # Record an invoice payment. (/docs/openapi/recordInvoicePayment) Records a manual payment (bank transfer, cash, card, or other) against a sent or overdue invoice. Partial payments keep the invoice open; the payment that settles the balance marks it paid. ## POST /v1/organizations/{organizationId}/invoices/{invoiceId}/payments ```json { "operationId": "recordInvoicePayment", "summary": "Record an invoice payment.", "description": "Records a manual payment (bank transfer, cash, card, or other) against a sent or overdue invoice. Partial payments keep the invoice open; the payment that settles the balance marks it paid.", "tags": [ "Invoices" ], "parameters": [ { "in": "path", "name": "invoiceId", "required": true, "schema": { "$ref": "#/components/schemas/invoiceIdSchema" }, "example": "123" }, { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/recordInvoicePaymentInputSchema" } } }, "description": "The amount in major units, the method, the date received, and an optional reference and note.", "required": true }, "responses": { "200": { "description": "Returns the invoice, the recorded payment, and whether this payment settled the invoice.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/invoicePaymentResultSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "discountKindSchema": { "type": "string", "enum": [ "none", "percentage", "fixed" ] }, "documentVatRegimeSchema": { "type": "string", "enum": [ "standard", "reverse_charge", "exempt", "kor", "margin_scheme" ] }, "invoiceCurrencyCodeSchema": { "type": "string", "pattern": "^[A-Z]{3}$" }, "invoiceDisplayNumberSchema": { "type": "string", "minLength": 1, "maxLength": 40 }, "invoiceIdSchema": { "type": "string", "format": "uuid" }, "invoiceKindSchema": { "type": "string", "enum": [ "invoice", "credit_note", "deposit" ] }, "invoiceNumberSchema": { "type": "integer", "exclusiveMinimum": 0, "maximum": 9007199254740991 }, "invoicePaymentMethodSchema": { "type": "string", "enum": [ "stripe", "bank_transfer", "cash", "other" ] }, "invoicePaymentResultSchema": { "type": "object", "properties": { "invoice": { "$ref": "#/components/schemas/invoiceSchema" }, "newlyPaid": { "type": "boolean" }, "payment": { "$ref": "#/components/schemas/invoicePaymentSchema" } }, "required": [ "invoice", "newlyPaid", "payment" ] }, "invoicePaymentSchema": { "type": "object", "properties": { "amountCents": { "type": "integer", "minimum": 0 }, "applicationFeeBps": { "type": "integer", "minimum": 0, "maximum": 10000 }, "applicationFeeCents": { "type": "integer", "minimum": 0 }, "applicationFeeMode": { "type": "string", "enum": [ "percentage", "fixed" ] }, "applicationFeeNetCents": { "type": "integer", "minimum": 0 }, "applicationFeeRefundedCents": { "type": "integer", "minimum": 0 }, "applicationFeeVatCents": { "type": "integer", "minimum": 0 }, "applicationFeeVatTreatment": { "type": [ "string", "null" ], "enum": [ "domestic", "reverse_charge", "outside_scope" ] }, "canceledAt": { "type": [ "string", "null" ], "format": "date-time" }, "createdAt": { "type": "string", "format": "date-time" }, "currency": { "$ref": "#/components/schemas/invoiceCurrencyCodeSchema" }, "disputeLostAt": { "type": [ "string", "null" ], "format": "date-time" }, "disputeLostCents": { "type": "integer", "minimum": 0 }, "disputedAt": { "type": [ "string", "null" ], "format": "date-time" }, "failureCode": { "type": [ "string", "null" ] }, "failureMessage": { "type": [ "string", "null" ] }, "id": { "type": "string", "format": "uuid" }, "invoiceId": { "$ref": "#/components/schemas/invoiceIdSchema" }, "method": { "$ref": "#/components/schemas/invoicePaymentMethodSchema" }, "note": { "type": [ "string", "null" ] }, "organizationId": { "type": "string", "format": "uuid" }, "paidAt": { "type": [ "string", "null" ], "format": "date-time" }, "receivedOn": { "type": [ "string", "null" ], "format": "date" }, "recordedBy": { "type": [ "string", "null" ], "format": "uuid" }, "reference": { "type": [ "string", "null" ] }, "refundedAt": { "type": [ "string", "null" ], "format": "date-time" }, "refundedCents": { "type": "integer", "minimum": 0 }, "status": { "$ref": "#/components/schemas/invoicePaymentStatusSchema" }, "stripeAccountId": { "type": [ "string", "null" ], "minLength": 1, "maxLength": 255 }, "stripeChargeId": { "type": [ "string", "null" ], "minLength": 1, "maxLength": 255 }, "stripePaymentIntentId": { "type": [ "string", "null" ], "minLength": 1, "maxLength": 255 }, "updatedAt": { "type": "string", "format": "date-time" } }, "required": [ "amountCents", "applicationFeeBps", "applicationFeeCents", "applicationFeeMode", "applicationFeeNetCents", "applicationFeeRefundedCents", "applicationFeeVatCents", "applicationFeeVatTreatment", "canceledAt", "createdAt", "currency", "disputeLostAt", "disputeLostCents", "disputedAt", "failureCode", "failureMessage", "id", "invoiceId", "method", "note", "organizationId", "paidAt", "receivedOn", "recordedBy", "reference", "refundedAt", "refundedCents", "status", "stripeAccountId", "stripeChargeId", "stripePaymentIntentId", "updatedAt" ] }, "invoicePaymentStatusSchema": { "type": "string", "enum": [ "requires_payment_method", "requires_action", "processing", "succeeded", "canceled", "failed", "refunded", "disputed" ] }, "invoiceSchema": { "type": "object", "properties": { "id": { "$ref": "#/components/schemas/invoiceIdSchema" }, "organizationId": { "type": "string", "format": "uuid" }, "customerId": { "type": "string", "format": "uuid" }, "customerContactId": { "type": [ "string", "null" ], "format": "uuid" }, "customerReference": { "type": [ "string", "null" ] }, "number": { "anyOf": [ { "$ref": "#/components/schemas/invoiceNumberSchema" }, { "type": "null" } ] }, "numberYear": { "type": [ "integer", "null" ], "exclusiveMinimum": 0 }, "displayNumber": { "anyOf": [ { "$ref": "#/components/schemas/invoiceDisplayNumberSchema" }, { "type": "null" } ] }, "status": { "$ref": "#/components/schemas/invoiceStatusSchema" }, "dueDays": { "type": [ "integer", "null" ], "minimum": 0, "maximum": 365 }, "invoiceTemplateId": { "type": [ "string", "null" ], "format": "uuid" }, "title": { "type": "string" }, "intro": { "type": [ "string", "null" ] }, "terms": { "type": [ "string", "null" ] }, "currency": { "$ref": "#/components/schemas/invoiceCurrencyCodeSchema" }, "vatInclusive": { "type": "boolean" }, "discountKind": { "$ref": "#/components/schemas/discountKindSchema" }, "discountValue": { "type": "number" }, "discountDescription": { "type": [ "string", "null" ], "maxLength": 500 }, "promotionId": { "type": [ "string", "null" ], "format": "uuid" }, "subtotalExcl": { "type": "number" }, "vatTotal": { "type": "number" }, "totalIncl": { "type": "number" }, "issueDate": { "type": [ "string", "null" ] }, "dueDate": { "type": [ "string", "null" ] }, "publicMessage": { "type": [ "string", "null" ] }, "footerText": { "type": [ "string", "null" ] }, "sourceQuoteId": { "type": [ "string", "null" ], "format": "uuid" }, "deliveryDate": { "type": [ "string", "null" ] }, "kind": { "$ref": "#/components/schemas/invoiceKindSchema" }, "originalInvoiceId": { "type": [ "string", "null" ], "format": "uuid" }, "vatRegime": { "$ref": "#/components/schemas/documentVatRegimeSchema" }, "sellerName": { "type": [ "string", "null" ] }, "sellerAddressLine1": { "type": [ "string", "null" ] }, "sellerAddressLine2": { "type": [ "string", "null" ] }, "sellerAddressPostalCode": { "type": [ "string", "null" ] }, "sellerAddressCity": { "type": [ "string", "null" ] }, "sellerAddressCountry": { "type": [ "string", "null" ] }, "sellerEmail": { "type": [ "string", "null" ] }, "sellerPhone": { "type": [ "string", "null" ] }, "sellerKvk": { "type": [ "string", "null" ] }, "sellerBtw": { "type": [ "string", "null" ] }, "sellerIban": { "type": [ "string", "null" ] }, "buyerName": { "type": [ "string", "null" ] }, "buyerAddressLine1": { "type": [ "string", "null" ] }, "buyerAddressLine2": { "type": [ "string", "null" ] }, "buyerAddressPostalCode": { "type": [ "string", "null" ] }, "buyerAddressCity": { "type": [ "string", "null" ] }, "buyerAddressCountry": { "type": [ "string", "null" ] }, "buyerEmail": { "type": [ "string", "null" ] }, "buyerKvk": { "type": [ "string", "null" ] }, "buyerBtw": { "type": [ "string", "null" ] }, "termsPdfFileId": { "type": [ "string", "null" ], "format": "uuid" }, "termsPdfVersionId": { "type": [ "string", "null" ], "format": "uuid" }, "termsRichText": { "type": [ "string", "null" ], "maxLength": 100000 }, "termsUrl": { "type": [ "string", "null" ] }, "sentAt": { "type": [ "string", "null" ], "format": "date-time" }, "paidAt": { "type": [ "string", "null" ], "format": "date-time" }, "canceledAt": { "type": [ "string", "null" ], "format": "date-time" }, "archivedAt": { "type": [ "string", "null" ], "format": "date-time" }, "createdBy": { "type": [ "string", "null" ], "format": "uuid" }, "updatedBy": { "type": [ "string", "null" ], "format": "uuid" }, "createdAt": { "type": "string", "format": "date-time" }, "updatedAt": { "type": "string", "format": "date-time" }, "amountPaidCents": { "type": "integer", "minimum": 0 }, "amountDueCents": { "type": "integer", "minimum": 0 }, "uncollectibleAt": { "type": [ "string", "null" ], "format": "date-time" }, "uncollectibleReason": { "type": [ "string", "null" ] }, "onlinePaymentEnabled": { "type": "boolean", "default": true } }, "required": [ "id", "organizationId", "customerId", "customerContactId", "customerReference", "number", "numberYear", "displayNumber", "status", "dueDays", "invoiceTemplateId", "title", "intro", "terms", "currency", "vatInclusive", "discountKind", "discountValue", "discountDescription", "promotionId", "subtotalExcl", "vatTotal", "totalIncl", "issueDate", "dueDate", "publicMessage", "footerText", "sourceQuoteId", "deliveryDate", "kind", "originalInvoiceId", "vatRegime", "sellerName", "sellerAddressLine1", "sellerAddressLine2", "sellerAddressPostalCode", "sellerAddressCity", "sellerAddressCountry", "sellerEmail", "sellerPhone", "sellerKvk", "sellerBtw", "sellerIban", "buyerName", "buyerAddressLine1", "buyerAddressLine2", "buyerAddressPostalCode", "buyerAddressCity", "buyerAddressCountry", "buyerEmail", "buyerKvk", "buyerBtw", "termsPdfFileId", "termsPdfVersionId", "termsRichText", "termsUrl", "sentAt", "paidAt", "canceledAt", "archivedAt", "createdBy", "updatedBy", "createdAt", "updatedAt", "amountPaidCents", "amountDueCents", "uncollectibleAt", "uncollectibleReason", "onlinePaymentEnabled" ] }, "invoiceStatusSchema": { "type": "string", "enum": [ "draft", "sent", "paid", "overdue", "canceled", "uncollectible" ] }, "manualInvoicePaymentMethodSchema": { "type": "string", "enum": [ "bank_transfer", "cash", "other" ] }, "organizationIdSchema": { "type": "string", "format": "uuid" }, "recordInvoicePaymentInputSchema": { "type": "object", "properties": { "amount": { "type": "number", "exclusiveMinimum": 0, "maximum": 100000000 }, "method": { "$ref": "#/components/schemas/manualInvoicePaymentMethodSchema" }, "note": { "type": [ "string", "null" ], "maxLength": 2000 }, "receivedOn": { "type": "string", "format": "date" }, "reference": { "type": [ "string", "null" ], "maxLength": 240 } }, "required": [ "amount", "method", "receivedOn" ] } } ``` # Resume invoice reminders. (/docs/openapi/resumeInvoiceReminders) Resumes payment reminders that were paused on the invoice. ## POST /v1/organizations/{organizationId}/invoices/{invoiceId}/resume-reminders ```json { "operationId": "resumeInvoiceReminders", "summary": "Resume invoice reminders.", "description": "Resumes payment reminders that were paused on the invoice.", "tags": [ "Invoices" ], "parameters": [ { "in": "path", "name": "invoiceId", "required": true, "schema": { "$ref": "#/components/schemas/invoiceIdSchema" }, "example": "123" }, { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "responses": { "200": { "description": "Reminders were resumed.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/publicApiSuccessResponseSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "invoiceIdSchema": { "type": "string", "format": "uuid" }, "organizationIdSchema": { "type": "string", "format": "uuid" }, "publicApiSuccessResponseSchema": { "type": "object", "properties": { "success": { "type": "boolean", "enum": [ true ] } }, "required": [ "success" ] } } ``` # Send an invoice. (/docs/openapi/sendInvoice) Transition a draft invoice to sent, creating its Stripe payment option and public link. ## POST /v1/organizations/{organizationId}/invoices/{invoiceId}/send ```json { "operationId": "sendInvoice", "summary": "Send an invoice.", "description": "Transition a draft invoice to sent, creating its Stripe payment option and public link.", "tags": [ "Invoices" ], "parameters": [ { "in": "path", "name": "invoiceId", "required": true, "schema": { "$ref": "#/components/schemas/invoiceIdSchema" }, "example": "123" }, { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/invoiceSendInputSchema" } } }, "description": "Send options for the invoice." }, "responses": { "200": { "description": "Returns the sent invoice.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/invoiceSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "discountKindSchema": { "type": "string", "enum": [ "none", "percentage", "fixed" ] }, "documentVatRegimeSchema": { "type": "string", "enum": [ "standard", "reverse_charge", "exempt", "kor", "margin_scheme" ] }, "invoiceCurrencyCodeSchema": { "type": "string", "pattern": "^[A-Z]{3}$" }, "invoiceDisplayNumberSchema": { "type": "string", "minLength": 1, "maxLength": 40 }, "invoiceIdSchema": { "type": "string", "format": "uuid" }, "invoiceKindSchema": { "type": "string", "enum": [ "invoice", "credit_note", "deposit" ] }, "invoiceNumberSchema": { "type": "integer", "exclusiveMinimum": 0, "maximum": 9007199254740991 }, "invoiceSchema": { "type": "object", "properties": { "id": { "$ref": "#/components/schemas/invoiceIdSchema" }, "organizationId": { "type": "string", "format": "uuid" }, "customerId": { "type": "string", "format": "uuid" }, "customerContactId": { "type": [ "string", "null" ], "format": "uuid" }, "customerReference": { "type": [ "string", "null" ] }, "number": { "anyOf": [ { "$ref": "#/components/schemas/invoiceNumberSchema" }, { "type": "null" } ] }, "numberYear": { "type": [ "integer", "null" ], "exclusiveMinimum": 0 }, "displayNumber": { "anyOf": [ { "$ref": "#/components/schemas/invoiceDisplayNumberSchema" }, { "type": "null" } ] }, "status": { "$ref": "#/components/schemas/invoiceStatusSchema" }, "dueDays": { "type": [ "integer", "null" ], "minimum": 0, "maximum": 365 }, "invoiceTemplateId": { "type": [ "string", "null" ], "format": "uuid" }, "title": { "type": "string" }, "intro": { "type": [ "string", "null" ] }, "terms": { "type": [ "string", "null" ] }, "currency": { "$ref": "#/components/schemas/invoiceCurrencyCodeSchema" }, "vatInclusive": { "type": "boolean" }, "discountKind": { "$ref": "#/components/schemas/discountKindSchema" }, "discountValue": { "type": "number" }, "discountDescription": { "type": [ "string", "null" ], "maxLength": 500 }, "promotionId": { "type": [ "string", "null" ], "format": "uuid" }, "subtotalExcl": { "type": "number" }, "vatTotal": { "type": "number" }, "totalIncl": { "type": "number" }, "issueDate": { "type": [ "string", "null" ] }, "dueDate": { "type": [ "string", "null" ] }, "publicMessage": { "type": [ "string", "null" ] }, "footerText": { "type": [ "string", "null" ] }, "sourceQuoteId": { "type": [ "string", "null" ], "format": "uuid" }, "deliveryDate": { "type": [ "string", "null" ] }, "kind": { "$ref": "#/components/schemas/invoiceKindSchema" }, "originalInvoiceId": { "type": [ "string", "null" ], "format": "uuid" }, "vatRegime": { "$ref": "#/components/schemas/documentVatRegimeSchema" }, "sellerName": { "type": [ "string", "null" ] }, "sellerAddressLine1": { "type": [ "string", "null" ] }, "sellerAddressLine2": { "type": [ "string", "null" ] }, "sellerAddressPostalCode": { "type": [ "string", "null" ] }, "sellerAddressCity": { "type": [ "string", "null" ] }, "sellerAddressCountry": { "type": [ "string", "null" ] }, "sellerEmail": { "type": [ "string", "null" ] }, "sellerPhone": { "type": [ "string", "null" ] }, "sellerKvk": { "type": [ "string", "null" ] }, "sellerBtw": { "type": [ "string", "null" ] }, "sellerIban": { "type": [ "string", "null" ] }, "buyerName": { "type": [ "string", "null" ] }, "buyerAddressLine1": { "type": [ "string", "null" ] }, "buyerAddressLine2": { "type": [ "string", "null" ] }, "buyerAddressPostalCode": { "type": [ "string", "null" ] }, "buyerAddressCity": { "type": [ "string", "null" ] }, "buyerAddressCountry": { "type": [ "string", "null" ] }, "buyerEmail": { "type": [ "string", "null" ] }, "buyerKvk": { "type": [ "string", "null" ] }, "buyerBtw": { "type": [ "string", "null" ] }, "termsPdfFileId": { "type": [ "string", "null" ], "format": "uuid" }, "termsPdfVersionId": { "type": [ "string", "null" ], "format": "uuid" }, "termsRichText": { "type": [ "string", "null" ], "maxLength": 100000 }, "termsUrl": { "type": [ "string", "null" ] }, "sentAt": { "type": [ "string", "null" ], "format": "date-time" }, "paidAt": { "type": [ "string", "null" ], "format": "date-time" }, "canceledAt": { "type": [ "string", "null" ], "format": "date-time" }, "archivedAt": { "type": [ "string", "null" ], "format": "date-time" }, "createdBy": { "type": [ "string", "null" ], "format": "uuid" }, "updatedBy": { "type": [ "string", "null" ], "format": "uuid" }, "createdAt": { "type": "string", "format": "date-time" }, "updatedAt": { "type": "string", "format": "date-time" }, "amountPaidCents": { "type": "integer", "minimum": 0 }, "amountDueCents": { "type": "integer", "minimum": 0 }, "uncollectibleAt": { "type": [ "string", "null" ], "format": "date-time" }, "uncollectibleReason": { "type": [ "string", "null" ] }, "onlinePaymentEnabled": { "type": "boolean", "default": true } }, "required": [ "id", "organizationId", "customerId", "customerContactId", "customerReference", "number", "numberYear", "displayNumber", "status", "dueDays", "invoiceTemplateId", "title", "intro", "terms", "currency", "vatInclusive", "discountKind", "discountValue", "discountDescription", "promotionId", "subtotalExcl", "vatTotal", "totalIncl", "issueDate", "dueDate", "publicMessage", "footerText", "sourceQuoteId", "deliveryDate", "kind", "originalInvoiceId", "vatRegime", "sellerName", "sellerAddressLine1", "sellerAddressLine2", "sellerAddressPostalCode", "sellerAddressCity", "sellerAddressCountry", "sellerEmail", "sellerPhone", "sellerKvk", "sellerBtw", "sellerIban", "buyerName", "buyerAddressLine1", "buyerAddressLine2", "buyerAddressPostalCode", "buyerAddressCity", "buyerAddressCountry", "buyerEmail", "buyerKvk", "buyerBtw", "termsPdfFileId", "termsPdfVersionId", "termsRichText", "termsUrl", "sentAt", "paidAt", "canceledAt", "archivedAt", "createdBy", "updatedBy", "createdAt", "updatedAt", "amountPaidCents", "amountDueCents", "uncollectibleAt", "uncollectibleReason", "onlinePaymentEnabled" ] }, "invoiceSendInputSchema": { "type": "object", "properties": { "dueDate": { "allOf": [ { "$ref": "#/components/schemas/isoDateOrNull" } ] }, "issueDate": { "allOf": [ { "$ref": "#/components/schemas/isoDateOrNull" } ] }, "publicMessage": { "type": [ "string", "null" ], "maxLength": 20000 } } }, "invoiceStatusSchema": { "type": "string", "enum": [ "draft", "sent", "paid", "overdue", "canceled", "uncollectible" ] }, "isoDateOrNull": { "type": [ "string", "null" ], "pattern": "^\\d{4}-\\d{2}-\\d{2}$" }, "organizationIdSchema": { "type": "string", "format": "uuid" } } ``` # Write off an invoice. (/docs/openapi/writeOffInvoice) Marks a sent or overdue invoice uncollectible. Reminders stop and the open balance no longer counts as receivable. ## POST /v1/organizations/{organizationId}/invoices/{invoiceId}/write-off ```json { "operationId": "writeOffInvoice", "summary": "Write off an invoice.", "description": "Marks a sent or overdue invoice uncollectible. Reminders stop and the open balance no longer counts as receivable.", "tags": [ "Invoices" ], "parameters": [ { "in": "path", "name": "invoiceId", "required": true, "schema": { "$ref": "#/components/schemas/invoiceIdSchema" }, "example": "123" }, { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/markInvoiceUncollectibleInputSchema" } } }, "description": "Why the invoice is written off.", "required": true }, "responses": { "200": { "description": "Returns the uncollectible invoice.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/invoiceSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "discountKindSchema": { "type": "string", "enum": [ "none", "percentage", "fixed" ] }, "documentVatRegimeSchema": { "type": "string", "enum": [ "standard", "reverse_charge", "exempt", "kor", "margin_scheme" ] }, "invoiceCurrencyCodeSchema": { "type": "string", "pattern": "^[A-Z]{3}$" }, "invoiceDisplayNumberSchema": { "type": "string", "minLength": 1, "maxLength": 40 }, "invoiceIdSchema": { "type": "string", "format": "uuid" }, "invoiceKindSchema": { "type": "string", "enum": [ "invoice", "credit_note", "deposit" ] }, "invoiceNumberSchema": { "type": "integer", "exclusiveMinimum": 0, "maximum": 9007199254740991 }, "invoiceSchema": { "type": "object", "properties": { "id": { "$ref": "#/components/schemas/invoiceIdSchema" }, "organizationId": { "type": "string", "format": "uuid" }, "customerId": { "type": "string", "format": "uuid" }, "customerContactId": { "type": [ "string", "null" ], "format": "uuid" }, "customerReference": { "type": [ "string", "null" ] }, "number": { "anyOf": [ { "$ref": "#/components/schemas/invoiceNumberSchema" }, { "type": "null" } ] }, "numberYear": { "type": [ "integer", "null" ], "exclusiveMinimum": 0 }, "displayNumber": { "anyOf": [ { "$ref": "#/components/schemas/invoiceDisplayNumberSchema" }, { "type": "null" } ] }, "status": { "$ref": "#/components/schemas/invoiceStatusSchema" }, "dueDays": { "type": [ "integer", "null" ], "minimum": 0, "maximum": 365 }, "invoiceTemplateId": { "type": [ "string", "null" ], "format": "uuid" }, "title": { "type": "string" }, "intro": { "type": [ "string", "null" ] }, "terms": { "type": [ "string", "null" ] }, "currency": { "$ref": "#/components/schemas/invoiceCurrencyCodeSchema" }, "vatInclusive": { "type": "boolean" }, "discountKind": { "$ref": "#/components/schemas/discountKindSchema" }, "discountValue": { "type": "number" }, "discountDescription": { "type": [ "string", "null" ], "maxLength": 500 }, "promotionId": { "type": [ "string", "null" ], "format": "uuid" }, "subtotalExcl": { "type": "number" }, "vatTotal": { "type": "number" }, "totalIncl": { "type": "number" }, "issueDate": { "type": [ "string", "null" ] }, "dueDate": { "type": [ "string", "null" ] }, "publicMessage": { "type": [ "string", "null" ] }, "footerText": { "type": [ "string", "null" ] }, "sourceQuoteId": { "type": [ "string", "null" ], "format": "uuid" }, "deliveryDate": { "type": [ "string", "null" ] }, "kind": { "$ref": "#/components/schemas/invoiceKindSchema" }, "originalInvoiceId": { "type": [ "string", "null" ], "format": "uuid" }, "vatRegime": { "$ref": "#/components/schemas/documentVatRegimeSchema" }, "sellerName": { "type": [ "string", "null" ] }, "sellerAddressLine1": { "type": [ "string", "null" ] }, "sellerAddressLine2": { "type": [ "string", "null" ] }, "sellerAddressPostalCode": { "type": [ "string", "null" ] }, "sellerAddressCity": { "type": [ "string", "null" ] }, "sellerAddressCountry": { "type": [ "string", "null" ] }, "sellerEmail": { "type": [ "string", "null" ] }, "sellerPhone": { "type": [ "string", "null" ] }, "sellerKvk": { "type": [ "string", "null" ] }, "sellerBtw": { "type": [ "string", "null" ] }, "sellerIban": { "type": [ "string", "null" ] }, "buyerName": { "type": [ "string", "null" ] }, "buyerAddressLine1": { "type": [ "string", "null" ] }, "buyerAddressLine2": { "type": [ "string", "null" ] }, "buyerAddressPostalCode": { "type": [ "string", "null" ] }, "buyerAddressCity": { "type": [ "string", "null" ] }, "buyerAddressCountry": { "type": [ "string", "null" ] }, "buyerEmail": { "type": [ "string", "null" ] }, "buyerKvk": { "type": [ "string", "null" ] }, "buyerBtw": { "type": [ "string", "null" ] }, "termsPdfFileId": { "type": [ "string", "null" ], "format": "uuid" }, "termsPdfVersionId": { "type": [ "string", "null" ], "format": "uuid" }, "termsRichText": { "type": [ "string", "null" ], "maxLength": 100000 }, "termsUrl": { "type": [ "string", "null" ] }, "sentAt": { "type": [ "string", "null" ], "format": "date-time" }, "paidAt": { "type": [ "string", "null" ], "format": "date-time" }, "canceledAt": { "type": [ "string", "null" ], "format": "date-time" }, "archivedAt": { "type": [ "string", "null" ], "format": "date-time" }, "createdBy": { "type": [ "string", "null" ], "format": "uuid" }, "updatedBy": { "type": [ "string", "null" ], "format": "uuid" }, "createdAt": { "type": "string", "format": "date-time" }, "updatedAt": { "type": "string", "format": "date-time" }, "amountPaidCents": { "type": "integer", "minimum": 0 }, "amountDueCents": { "type": "integer", "minimum": 0 }, "uncollectibleAt": { "type": [ "string", "null" ], "format": "date-time" }, "uncollectibleReason": { "type": [ "string", "null" ] }, "onlinePaymentEnabled": { "type": "boolean", "default": true } }, "required": [ "id", "organizationId", "customerId", "customerContactId", "customerReference", "number", "numberYear", "displayNumber", "status", "dueDays", "invoiceTemplateId", "title", "intro", "terms", "currency", "vatInclusive", "discountKind", "discountValue", "discountDescription", "promotionId", "subtotalExcl", "vatTotal", "totalIncl", "issueDate", "dueDate", "publicMessage", "footerText", "sourceQuoteId", "deliveryDate", "kind", "originalInvoiceId", "vatRegime", "sellerName", "sellerAddressLine1", "sellerAddressLine2", "sellerAddressPostalCode", "sellerAddressCity", "sellerAddressCountry", "sellerEmail", "sellerPhone", "sellerKvk", "sellerBtw", "sellerIban", "buyerName", "buyerAddressLine1", "buyerAddressLine2", "buyerAddressPostalCode", "buyerAddressCity", "buyerAddressCountry", "buyerEmail", "buyerKvk", "buyerBtw", "termsPdfFileId", "termsPdfVersionId", "termsRichText", "termsUrl", "sentAt", "paidAt", "canceledAt", "archivedAt", "createdBy", "updatedBy", "createdAt", "updatedAt", "amountPaidCents", "amountDueCents", "uncollectibleAt", "uncollectibleReason", "onlinePaymentEnabled" ] }, "invoiceStatusSchema": { "type": "string", "enum": [ "draft", "sent", "paid", "overdue", "canceled", "uncollectible" ] }, "markInvoiceUncollectibleInputSchema": { "type": "object", "properties": { "reason": { "type": "string", "minLength": 1, "maxLength": 2000 } }, "required": [ "reason" ] }, "organizationIdSchema": { "type": "string", "format": "uuid" } } ``` # Advance a invoice lifecycle stage. (/docs/openapi/advanceInvoiceLifecycle) Moves the invoice to a manual milestone of its lifecycle. Only stages the lifecycle marks as manually advanceable are accepted. ## POST /v1/organizations/{organizationId}/invoices/{invoiceId}/lifecycle/advance ```json { "operationId": "advanceInvoiceLifecycle", "summary": "Advance a invoice lifecycle stage.", "description": "Moves the invoice to a manual milestone of its lifecycle. Only stages the lifecycle marks as manually advanceable are accepted.", "tags": [ "Invoices" ], "parameters": [ { "in": "path", "name": "invoiceId", "required": true, "schema": { "$ref": "#/components/schemas/invoiceIdSchema" }, "example": "123" }, { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/lifecycleAdvanceInputSchema" } } }, "description": "The stage key to move to.", "required": true }, "responses": { "200": { "description": "The stage was recorded.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/publicApiSuccessResponseSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "invoiceIdSchema": { "type": "string", "format": "uuid" }, "lifecycleAdvanceInputSchema": { "type": "object", "properties": { "stageKey": { "type": "string", "minLength": 1, "maxLength": 80 } }, "required": [ "stageKey" ] }, "organizationIdSchema": { "type": "string", "format": "uuid" }, "publicApiSuccessResponseSchema": { "type": "object", "properties": { "success": { "type": "boolean", "enum": [ true ] } }, "required": [ "success" ] } } ``` # Update an invoice note. (/docs/openapi/updateInvoiceNote) Update an internal note on an invoice. ## PATCH /v1/organizations/{organizationId}/invoices/{invoiceId}/notes/{noteId} ```json { "operationId": "updateInvoiceNote", "summary": "Update an invoice note.", "description": "Update an internal note on an invoice.", "tags": [ "Invoices" ], "parameters": [ { "in": "path", "name": "invoiceId", "required": true, "schema": { "$ref": "#/components/schemas/invoiceIdSchema" }, "example": "123" }, { "in": "path", "name": "noteId", "required": true, "schema": { "type": "string", "format": "uuid" }, "example": "123e4567-e89b-12d3-a456-426614174000" }, { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/invoiceNoteInputSchema" } } }, "description": "The invoice note payload." }, "responses": { "200": { "description": "Returns the updated invoice note.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/invoiceNoteSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "collaborationBlockSchema": { "type": "object", "discriminator": { "propertyName": "type" }, "oneOf": [ { "type": "object", "properties": { "content": { "$ref": "#/components/schemas/collaborationInlineContentSchema" }, "type": { "type": "string", "enum": [ "paragraph" ] } }, "required": [ "content", "type" ], "additionalProperties": false }, { "type": "object", "properties": { "items": { "type": "array", "items": { "$ref": "#/components/schemas/collaborationInlineContentSchema" }, "minItems": 1, "maxItems": 500 }, "type": { "type": "string", "enum": [ "bullet-list" ] } }, "required": [ "items", "type" ], "additionalProperties": false }, { "type": "object", "properties": { "items": { "type": "array", "items": { "$ref": "#/components/schemas/collaborationInlineContentSchema" }, "minItems": 1, "maxItems": 500 }, "type": { "type": "string", "enum": [ "numbered-list" ] } }, "required": [ "items", "type" ], "additionalProperties": false } ] }, "collaborationCommentAuthorSchema": { "type": "object", "properties": { "avatarUrl": { "type": [ "string", "null" ], "format": "uri" }, "displayName": { "type": "string", "minLength": 1 }, "userId": { "type": "string", "format": "uuid" } }, "required": [ "avatarUrl", "displayName", "userId" ] }, "collaborationComposerInputSchema": { "type": "object", "properties": { "selections": { "type": "array", "items": { "$ref": "#/components/schemas/collaborationComposerSelectionSchema" }, "maxItems": 100 }, "text": { "type": "string", "minLength": 1, "maxLength": 20000 } }, "required": [ "selections", "text" ], "additionalProperties": false }, "collaborationComposerSelectionSchema": { "type": "object", "properties": { "end": { "type": "integer", "exclusiveMinimum": 0, "maximum": 20000 }, "reference": { "$ref": "#/components/schemas/collaborationReferenceIdSchema" }, "start": { "type": "integer", "minimum": 0, "maximum": 20000 } }, "required": [ "end", "reference", "start" ], "additionalProperties": false }, "collaborationDocumentSchema": { "type": "object", "properties": { "blocks": { "type": "array", "items": { "$ref": "#/components/schemas/collaborationBlockSchema" }, "minItems": 1, "maxItems": 1000 }, "version": { "type": "integer", "enum": [ 1 ] } }, "required": [ "blocks", "version" ], "additionalProperties": false }, "collaborationInlineContentSchema": { "type": "array", "items": { "$ref": "#/components/schemas/collaborationInlineSchema" }, "minItems": 1, "maxItems": 1000 }, "collaborationInlineSchema": { "type": "object", "discriminator": { "propertyName": "type" }, "oneOf": [ { "type": "object", "properties": { "text": { "type": "string", "minLength": 1, "maxLength": 20000 }, "type": { "type": "string", "enum": [ "text" ] } }, "required": [ "text", "type" ], "additionalProperties": false }, { "type": "object", "properties": { "reference": { "$ref": "#/components/schemas/collaborationReferenceSchema" }, "type": { "type": "string", "enum": [ "reference" ] } }, "required": [ "reference", "type" ], "additionalProperties": false } ] }, "collaborationReferenceIdSchema": { "type": "object", "discriminator": { "propertyName": "kind" }, "oneOf": [ { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "kind": { "type": "string", "enum": [ "user" ] } }, "required": [ "id", "kind" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "kind": { "type": "string", "enum": [ "team" ] } }, "required": [ "id", "kind" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "kind": { "type": "string", "enum": [ "task" ] } }, "required": [ "id", "kind" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "kind": { "type": "string", "enum": [ "customer" ] } }, "required": [ "id", "kind" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "$ref": "#/components/schemas/quoteIdSchema" }, "kind": { "type": "string", "enum": [ "quote" ] } }, "required": [ "id", "kind" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "$ref": "#/components/schemas/invoiceIdSchema" }, "kind": { "type": "string", "enum": [ "invoice" ] } }, "required": [ "id", "kind" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "$ref": "#/components/schemas/productIdSchema" }, "kind": { "type": "string", "enum": [ "product" ] } }, "required": [ "id", "kind" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "kind": { "type": "string", "enum": [ "customer-asset" ] } }, "required": [ "id", "kind" ], "additionalProperties": false } ] }, "collaborationReferenceSchema": { "type": "object", "discriminator": { "propertyName": "kind" }, "oneOf": [ { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "kind": { "type": "string", "enum": [ "user" ] }, "label": { "type": "string", "minLength": 1, "maxLength": 500 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "path": { "type": "string", "pattern": "^/", "maxLength": 2000 } }, "required": [ "id", "kind", "label", "organizationId", "path" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "kind": { "type": "string", "enum": [ "team" ] }, "label": { "type": "string", "minLength": 1, "maxLength": 500 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "path": { "type": "string", "pattern": "^/", "maxLength": 2000 } }, "required": [ "id", "kind", "label", "organizationId", "path" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "kind": { "type": "string", "enum": [ "task" ] }, "label": { "type": "string", "minLength": 1, "maxLength": 500 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "path": { "type": "string", "pattern": "^/", "maxLength": 2000 } }, "required": [ "id", "kind", "label", "organizationId", "path" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "kind": { "type": "string", "enum": [ "customer" ] }, "label": { "type": "string", "minLength": 1, "maxLength": 500 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "path": { "type": "string", "pattern": "^/", "maxLength": 2000 } }, "required": [ "id", "kind", "label", "organizationId", "path" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "$ref": "#/components/schemas/quoteIdSchema" }, "kind": { "type": "string", "enum": [ "quote" ] }, "label": { "type": "string", "minLength": 1, "maxLength": 500 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "path": { "type": "string", "pattern": "^/", "maxLength": 2000 } }, "required": [ "id", "kind", "label", "organizationId", "path" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "$ref": "#/components/schemas/invoiceIdSchema" }, "kind": { "type": "string", "enum": [ "invoice" ] }, "label": { "type": "string", "minLength": 1, "maxLength": 500 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "path": { "type": "string", "pattern": "^/", "maxLength": 2000 } }, "required": [ "id", "kind", "label", "organizationId", "path" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "$ref": "#/components/schemas/productIdSchema" }, "kind": { "type": "string", "enum": [ "product" ] }, "label": { "type": "string", "minLength": 1, "maxLength": 500 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "path": { "type": "string", "pattern": "^/", "maxLength": 2000 } }, "required": [ "id", "kind", "label", "organizationId", "path" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "kind": { "type": "string", "enum": [ "customer-asset" ] }, "label": { "type": "string", "minLength": 1, "maxLength": 500 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "path": { "type": "string", "pattern": "^/", "maxLength": 2000 } }, "required": [ "id", "kind", "label", "organizationId", "path" ], "additionalProperties": false } ] }, "invoiceIdSchema": { "type": "string", "format": "uuid" }, "invoiceNoteInputSchema": { "$ref": "#/components/schemas/collaborationComposerInputSchema" }, "invoiceNoteSchema": { "type": "object", "properties": { "author": { "anyOf": [ { "$ref": "#/components/schemas/collaborationCommentAuthorSchema" }, { "type": "null" } ] }, "authorId": { "type": [ "string", "null" ], "format": "uuid" }, "contentDoc": { "$ref": "#/components/schemas/collaborationDocumentSchema" }, "contentText": { "type": "string", "minLength": 1, "maxLength": 20000 }, "createdAt": { "type": "string", "format": "date-time" }, "id": { "type": "string", "format": "uuid" }, "invoiceId": { "$ref": "#/components/schemas/invoiceIdSchema" }, "organizationId": { "type": "string", "format": "uuid" }, "updatedAt": { "type": "string", "format": "date-time" } }, "required": [ "author", "authorId", "contentDoc", "contentText", "createdAt", "id", "invoiceId", "organizationId", "updatedAt" ] }, "organizationIdSchema": { "type": "string", "format": "uuid" }, "productIdSchema": { "type": "string", "format": "uuid" }, "quoteIdSchema": { "type": "string", "format": "uuid" } } ``` # Delete an invoice note. (/docs/openapi/deleteInvoiceNote) Delete an internal note from an invoice. ## DELETE /v1/organizations/{organizationId}/invoices/{invoiceId}/notes/{noteId} ```json { "operationId": "deleteInvoiceNote", "summary": "Delete an invoice note.", "description": "Delete an internal note from an invoice.", "tags": [ "Invoices" ], "parameters": [ { "in": "path", "name": "invoiceId", "required": true, "schema": { "$ref": "#/components/schemas/invoiceIdSchema" }, "example": "123" }, { "in": "path", "name": "noteId", "required": true, "schema": { "type": "string", "format": "uuid" }, "example": "123e4567-e89b-12d3-a456-426614174000" }, { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "responses": { "200": { "description": "Returns a success payload when the note is deleted.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/publicApiSuccessResponseSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "invoiceIdSchema": { "type": "string", "format": "uuid" }, "organizationIdSchema": { "type": "string", "format": "uuid" }, "publicApiSuccessResponseSchema": { "type": "object", "properties": { "success": { "type": "boolean", "enum": [ true ] } }, "required": [ "success" ] } } ``` # Retrieve organization knowledge. (/docs/openapi/retrieveKnowledge) Search authorized organization and system knowledge and return matching text chunks with public provenance. Internal embeddings, vector data, service metadata, and storage paths are never returned. ## POST /v1/organizations/{organizationId}/knowledge/retrieve ```json { "operationId": "retrieveKnowledge", "summary": "Retrieve organization knowledge.", "description": "Search authorized organization and system knowledge and return matching text chunks with public provenance. Internal embeddings, vector data, service metadata, and storage paths are never returned.", "tags": [ "Knowledge" ], "parameters": [ { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/knowledgeRetrievalHttpRequestSchema" } } }, "description": "A bounded natural-language query with optional collection and system-knowledge scope.", "required": true }, "responses": { "200": { "description": "Returns ranked knowledge chunks and their public provenance.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/knowledgeRetrievalHttpResponseSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "knowledgeRetrievalHttpChunkSchema": { "type": "object", "properties": { "content": { "type": "string", "minLength": 1, "maxLength": 2000 }, "provenance": { "$ref": "#/components/schemas/knowledgeRetrievalHttpProvenanceSchema" }, "score": { "type": [ "number", "null" ], "minimum": 0, "maximum": 1 } }, "required": [ "content", "provenance", "score" ] }, "knowledgeRetrievalHttpProvenanceSchema": { "type": "object", "properties": { "chunkId": { "type": "string", "format": "uuid" }, "collectionId": { "type": "string", "format": "uuid" }, "fileId": { "type": "string", "format": "uuid" }, "sourceUri": { "type": [ "string", "null" ], "maxLength": 2048 }, "title": { "type": "string", "minLength": 1, "maxLength": 240 } }, "required": [ "chunkId", "collectionId", "fileId", "sourceUri", "title" ] }, "knowledgeRetrievalHttpRequestSchema": { "type": "object", "properties": { "collectionIds": { "type": "array", "items": { "type": "string", "format": "uuid" }, "maxItems": 50 }, "includeSystem": { "type": "boolean" }, "limit": { "type": "integer", "minimum": 1, "maximum": 50 }, "query": { "type": "string", "minLength": 1, "maxLength": 2000 } }, "required": [ "query" ], "additionalProperties": false }, "knowledgeRetrievalHttpResponseSchema": { "type": "object", "properties": { "chunks": { "type": "array", "items": { "$ref": "#/components/schemas/knowledgeRetrievalHttpChunkSchema" } } }, "required": [ "chunks" ] }, "organizationIdSchema": { "type": "string", "format": "uuid" } } ``` # List invitations. (/docs/openapi/listInvitations) Return pending and historical invitations for an organization. ## GET /v1/organizations/{organizationId}/invitations ```json { "operationId": "listInvitations", "summary": "List invitations.", "description": "Return pending and historical invitations for an organization.", "tags": [ "Members" ], "parameters": [ { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "responses": { "200": { "description": "Returns paginated invitations.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/invitationsListResponseSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "invitationsListResponseSchema": { "type": "object" }, "organizationIdSchema": { "type": "string", "format": "uuid" } } ``` # List organization members. (/docs/openapi/listMembers) Return members for an organization the authenticated user can access. ## GET /v1/organizations/{organizationId}/members ```json { "operationId": "listMembers", "summary": "List organization members.", "description": "Return members for an organization the authenticated user can access.", "tags": [ "Members" ], "parameters": [ { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "responses": { "200": { "description": "Returns paginated organization members.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/membersListResponseSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "membersListResponseSchema": { "type": "object" }, "organizationIdSchema": { "type": "string", "format": "uuid" } } ``` # Invite a user. (/docs/openapi/inviteUser) Create an invitation and dispatch the invite email. ## POST /v1/organizations/{organizationId}/members ```json { "operationId": "inviteUser", "summary": "Invite a user.", "description": "Create an invitation and dispatch the invite email.", "tags": [ "Members" ], "parameters": [ { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/inviteMemberInputSchema" } } }, "description": "The invitation payload." }, "responses": { "201": { "description": "Returns the created invitation.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/invitationRowSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "invitationRowSchema": { "type": "object", "properties": { "acceptedAt": { "type": [ "string", "null" ] }, "createdAt": { "type": "string" }, "declinedAt": { "type": [ "string", "null" ] }, "email": { "type": "string", "format": "email" }, "expiresAt": { "type": "string" }, "id": { "type": "string", "format": "uuid" }, "invitedBy": { "type": [ "string", "null" ], "format": "uuid" }, "organizationId": { "anyOf": [ { "$ref": "#/components/schemas/organizationIdSchema" }, { "type": "null" } ] }, "roleId": { "type": "string", "format": "uuid" }, "roleKey": { "type": "string" }, "roleName": { "type": "string" } }, "required": [ "acceptedAt", "createdAt", "declinedAt", "email", "expiresAt", "id", "invitedBy", "organizationId", "roleId", "roleKey", "roleName" ] }, "inviteMemberInputSchema": { "type": "object", "properties": { "email": { "type": "string", "format": "email" }, "prefill": { "type": "object", "properties": { "firstName": { "type": "string", "maxLength": 128 }, "lastName": { "type": "string", "maxLength": 128 } } }, "roleId": { "type": "string", "format": "uuid" } }, "required": [ "email", "roleId" ] }, "organizationIdSchema": { "type": "string", "format": "uuid" } } ``` # Get an invitation. (/docs/openapi/getInvitation) Return detail for an organization invitation. ## GET /v1/organizations/{organizationId}/invitations/{invitationId} ```json { "operationId": "getInvitation", "summary": "Get an invitation.", "description": "Return detail for an organization invitation.", "tags": [ "Members" ], "parameters": [ { "in": "path", "name": "invitationId", "required": true, "schema": { "type": "string", "format": "uuid" }, "example": "123e4567-e89b-12d3-a456-426614174000" }, { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "responses": { "200": { "description": "Returns the requested invitation.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/invitationDetailSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "invitationDetailSchema": { "type": "object", "properties": { "acceptedAt": { "type": [ "string", "null" ] }, "createdAt": { "type": "string" }, "declinedAt": { "type": [ "string", "null" ] }, "email": { "type": "string", "format": "email" }, "expiresAt": { "type": "string" }, "id": { "type": "string", "format": "uuid" }, "invitedBy": { "type": [ "string", "null" ], "format": "uuid" }, "organizationId": { "anyOf": [ { "$ref": "#/components/schemas/organizationIdSchema" }, { "type": "null" } ] }, "roleId": { "type": "string", "format": "uuid" }, "roleKey": { "type": "string" }, "roleName": { "type": "string" }, "prefill": { "$ref": "#/components/schemas/invitationPrefillSchema" }, "status": { "$ref": "#/components/schemas/invitationStatusSchema" } }, "required": [ "acceptedAt", "createdAt", "declinedAt", "email", "expiresAt", "id", "invitedBy", "organizationId", "roleId", "roleKey", "roleName", "prefill", "status" ] }, "invitationPrefillSchema": { "type": "object", "properties": { "firstName": { "type": "string", "maxLength": 128 }, "lastName": { "type": "string", "maxLength": 128 } } }, "invitationStatusSchema": { "type": "string", "enum": [ "pending", "expired", "accepted", "declined" ] }, "organizationIdSchema": { "type": "string", "format": "uuid" } } ``` # Update an invitation. (/docs/openapi/updateInvitation) Update a pending organization invitation. ## PATCH /v1/organizations/{organizationId}/invitations/{invitationId} ```json { "operationId": "updateInvitation", "summary": "Update an invitation.", "description": "Update a pending organization invitation.", "tags": [ "Members" ], "parameters": [ { "in": "path", "name": "invitationId", "required": true, "schema": { "type": "string", "format": "uuid" }, "example": "123e4567-e89b-12d3-a456-426614174000" }, { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/updateInvitationInputSchema" } } }, "description": "The invitation patch payload." }, "responses": { "200": { "description": "Returns the updated invitation.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/invitationDetailSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "invitationDetailSchema": { "type": "object", "properties": { "acceptedAt": { "type": [ "string", "null" ] }, "createdAt": { "type": "string" }, "declinedAt": { "type": [ "string", "null" ] }, "email": { "type": "string", "format": "email" }, "expiresAt": { "type": "string" }, "id": { "type": "string", "format": "uuid" }, "invitedBy": { "type": [ "string", "null" ], "format": "uuid" }, "organizationId": { "anyOf": [ { "$ref": "#/components/schemas/organizationIdSchema" }, { "type": "null" } ] }, "roleId": { "type": "string", "format": "uuid" }, "roleKey": { "type": "string" }, "roleName": { "type": "string" }, "prefill": { "$ref": "#/components/schemas/invitationPrefillSchema" }, "status": { "$ref": "#/components/schemas/invitationStatusSchema" } }, "required": [ "acceptedAt", "createdAt", "declinedAt", "email", "expiresAt", "id", "invitedBy", "organizationId", "roleId", "roleKey", "roleName", "prefill", "status" ] }, "invitationPrefillSchema": { "type": "object", "properties": { "firstName": { "type": "string", "maxLength": 128 }, "lastName": { "type": "string", "maxLength": 128 } } }, "invitationStatusSchema": { "type": "string", "enum": [ "pending", "expired", "accepted", "declined" ] }, "organizationIdSchema": { "type": "string", "format": "uuid" }, "updateInvitationInputSchema": { "type": "object", "properties": { "email": { "type": "string", "format": "email" }, "prefill": { "allOf": [ { "$ref": "#/components/schemas/invitationPrefillSchema" } ] }, "roleId": { "type": "string", "format": "uuid" } } } } ``` # Revoke an invitation. (/docs/openapi/revokeInvitation) Revoke a pending organization invitation. ## DELETE /v1/organizations/{organizationId}/invitations/{invitationId} ```json { "operationId": "revokeInvitation", "summary": "Revoke an invitation.", "description": "Revoke a pending organization invitation.", "tags": [ "Members" ], "parameters": [ { "in": "path", "name": "invitationId", "required": true, "schema": { "type": "string", "format": "uuid" }, "example": "123e4567-e89b-12d3-a456-426614174000" }, { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "responses": { "200": { "description": "Returns a success payload when the invitation is revoked.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/publicApiSuccessResponseSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "organizationIdSchema": { "type": "string", "format": "uuid" }, "publicApiSuccessResponseSchema": { "type": "object", "properties": { "success": { "type": "boolean", "enum": [ true ] } }, "required": [ "success" ] } } ``` # Get an organization member. (/docs/openapi/getMember) Return detail for a member in the specified organization. ## GET /v1/organizations/{organizationId}/members/{userId} ```json { "operationId": "getMember", "summary": "Get an organization member.", "description": "Return detail for a member in the specified organization.", "tags": [ "Members" ], "parameters": [ { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" }, { "in": "path", "name": "userId", "required": true, "schema": { "type": "string", "format": "uuid" }, "example": "123e4567-e89b-12d3-a456-426614174000" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "responses": { "200": { "description": "Returns the requested member detail.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/userDetailSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "memberRowSchema": { "type": "object", "properties": { "avatarUrl": { "type": [ "string", "null" ], "format": "uri" }, "disabledAt": { "type": [ "string", "null" ] }, "email": { "type": [ "string", "null" ], "format": "email" }, "firstName": { "type": [ "string", "null" ] }, "joinedAt": { "type": "string" }, "lastActiveAt": { "type": [ "string", "null" ] }, "lastName": { "type": [ "string", "null" ] }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "roleId": { "type": "string", "format": "uuid" }, "roleKey": { "type": "string" }, "roleName": { "type": "string" }, "suspendedAt": { "type": [ "string", "null" ] }, "userId": { "type": "string", "format": "uuid" } }, "required": [ "avatarUrl", "disabledAt", "email", "firstName", "joinedAt", "lastActiveAt", "lastName", "organizationId", "roleId", "roleKey", "roleName", "suspendedAt", "userId" ] }, "organizationIdSchema": { "type": "string", "format": "uuid" }, "systemUserOrganizationSchema": { "type": "object", "properties": { "disabledAt": { "type": [ "string", "null" ] }, "id": { "$ref": "#/components/schemas/organizationIdSchema" }, "name": { "type": "string" }, "slug": { "type": "string" } }, "required": [ "disabledAt", "id", "name", "slug" ] }, "userDetailSchema": { "type": "object", "properties": { "avatarUrl": { "type": [ "string", "null" ], "format": "uri" }, "bannedUntil": { "type": [ "string", "null" ] }, "createdAt": { "type": [ "string", "null" ] }, "disabledAt": { "type": [ "string", "null" ] }, "email": { "type": [ "string", "null" ], "format": "email" }, "firstName": { "type": [ "string", "null" ] }, "lastActiveAt": { "type": [ "string", "null" ] }, "lastName": { "type": [ "string", "null" ] }, "organizationMembership": { "anyOf": [ { "$ref": "#/components/schemas/memberRowSchema" }, { "type": "null" } ] }, "organizationMemberships": { "type": "array", "items": { "$ref": "#/components/schemas/userOrganizationMembershipSchema" } }, "organizations": { "type": "array", "items": { "$ref": "#/components/schemas/systemUserOrganizationSchema" } }, "systemRoleIds": { "type": "array", "items": { "type": "string", "format": "uuid" } }, "systemRoleKeys": { "type": "array", "items": { "type": "string" } }, "userId": { "type": "string", "format": "uuid" }, "username": { "$ref": "#/components/schemas/usernameSchema" } }, "required": [ "avatarUrl", "bannedUntil", "createdAt", "disabledAt", "email", "firstName", "lastActiveAt", "lastName", "organizationMembership", "organizationMemberships", "organizations", "systemRoleIds", "systemRoleKeys", "userId", "username" ] }, "userOrganizationMembershipSchema": { "type": "object", "properties": { "disabledAt": { "type": [ "string", "null" ] }, "id": { "$ref": "#/components/schemas/organizationIdSchema" }, "name": { "type": "string" }, "slug": { "type": "string" }, "joinedAt": { "type": "string" }, "lastActiveAt": { "type": [ "string", "null" ] }, "roleId": { "type": "string", "format": "uuid" }, "roleKey": { "type": "string" }, "roleName": { "type": "string" } }, "required": [ "disabledAt", "id", "name", "slug", "joinedAt", "lastActiveAt", "roleId", "roleKey", "roleName" ] }, "usernameSchema": { "type": "string", "minLength": 3, "maxLength": 30 } } ``` # Update a member role. (/docs/openapi/updateMemberRole) Update the role assigned to an organization member. ## PATCH /v1/organizations/{organizationId}/members/{userId} ```json { "operationId": "updateMemberRole", "summary": "Update a member role.", "description": "Update the role assigned to an organization member.", "tags": [ "Members" ], "parameters": [ { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" }, { "in": "path", "name": "userId", "required": true, "schema": { "type": "string", "format": "uuid" }, "example": "123e4567-e89b-12d3-a456-426614174000" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/organizationMemberRoleUpdateInputSchema" } } }, "description": "The role assignment payload." }, "responses": { "200": { "description": "Returns a success payload when the member role is updated.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/publicApiSuccessResponseSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "organizationIdSchema": { "type": "string", "format": "uuid" }, "organizationMemberRoleUpdateInputSchema": { "type": "object", "properties": { "roleId": { "type": "string", "format": "uuid" } }, "required": [ "roleId" ] }, "publicApiSuccessResponseSchema": { "type": "object", "properties": { "success": { "type": "boolean", "enum": [ true ] } }, "required": [ "success" ] } } ``` # List notification preferences. (/docs/openapi/listNotificationPreferences) Return account and organization channel preferences for the authenticated member. ## GET /v1/organizations/{organizationId}/notification-preferences ```json { "operationId": "listNotificationPreferences", "summary": "List notification preferences.", "description": "Return account and organization channel preferences for the authenticated member.", "tags": [ "Notifications" ], "parameters": [ { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "responses": { "200": { "description": "Returns notification channel preferences.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/notificationPreferencesListSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "notificationChannelSchema": { "type": "string", "enum": [ "in_app", "email", "push" ] }, "notificationPreferenceSchema": { "type": "object", "properties": { "channel": { "$ref": "#/components/schemas/notificationChannelSchema" }, "enabled": { "type": "boolean" }, "organizationId": { "type": [ "string", "null" ], "format": "uuid" }, "type": { "anyOf": [ { "$ref": "#/components/schemas/notificationTypeSchema" }, { "type": "string", "enum": [ "*" ] } ] }, "userId": { "type": "string", "format": "uuid" } }, "required": [ "channel", "enabled", "organizationId", "type", "userId" ] }, "notificationPreferencesListSchema": { "type": "array", "items": { "$ref": "#/components/schemas/notificationPreferenceSchema" } }, "notificationTypeSchema": { "type": "string", "enum": [ "task.assigned", "task.comment", "task.updated", "collaboration.mention", "collaboration.note", "task.due-soon", "task.overdue", "agenda.assigned", "agenda.updated", "agent.approval-required", "agent.run-failed", "agent.run-completed", "workflow.approval-required", "workflow.run-failed", "workflow.run-completed", "workflow.notification", "inbox.message", "quote.sent", "quote.viewed", "quote.accepted", "quote.rejected", "quote.changes-requested", "quote.expired", "quote.deposit-paid", "invoice.paid", "invoice.overdue", "invoice.viewed", "invoice.partially-paid", "invoice.payment-failed", "invoice.disputed", "invoice.refunded", "invoice.dispute-lost", "invoice.final-notice-sent", "order.backordered", "order.confirmed", "shipment.exception", "purchase-order.received", "purchase-order.overdue", "inventory.low-stock", "task.stage-changed", "billing.usage-threshold", "organization.invite", "organization.membership-updated", "time-off.requested", "time-off.decided", "time-off.canceled", "sick-leave.reported", "sick-leave.returned", "sick-leave.follow-up", "timesheet.submitted", "timesheet.decided", "expense.submitted", "expense.decided", "expense.reimbursed", "employee.document-expiring", "employee.contract-ending", "equipment.assigned", "onboarding.item-assigned", "hiring.application-received", "time.timer-forgotten", "time.budget-warning", "approval.requested", "approval.decided", "approval.escalated" ] }, "organizationIdSchema": { "type": "string", "format": "uuid" } } ``` # Upsert a notification preference. (/docs/openapi/putNotificationPreference) Create or update a channel preference for the authenticated member. ## PUT /v1/organizations/{organizationId}/notification-preferences ```json { "operationId": "putNotificationPreference", "summary": "Upsert a notification preference.", "description": "Create or update a channel preference for the authenticated member.", "tags": [ "Notifications" ], "parameters": [ { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/notificationPreferencePutInputSchema" } } }, "description": "Channel, type, and enabled state. Omit organizationId to use the path organization." }, "responses": { "200": { "description": "Returns the saved preference.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/notificationPreferenceSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "notificationChannelSchema": { "type": "string", "enum": [ "in_app", "email", "push" ] }, "notificationPreferencePutInputSchema": { "$ref": "#/components/schemas/upsertNotificationPreferenceInputSchema" }, "notificationPreferenceSchema": { "type": "object", "properties": { "channel": { "$ref": "#/components/schemas/notificationChannelSchema" }, "enabled": { "type": "boolean" }, "organizationId": { "type": [ "string", "null" ], "format": "uuid" }, "type": { "anyOf": [ { "$ref": "#/components/schemas/notificationTypeSchema" }, { "type": "string", "enum": [ "*" ] } ] }, "userId": { "type": "string", "format": "uuid" } }, "required": [ "channel", "enabled", "organizationId", "type", "userId" ] }, "notificationTypeSchema": { "type": "string", "enum": [ "task.assigned", "task.comment", "task.updated", "collaboration.mention", "collaboration.note", "task.due-soon", "task.overdue", "agenda.assigned", "agenda.updated", "agent.approval-required", "agent.run-failed", "agent.run-completed", "workflow.approval-required", "workflow.run-failed", "workflow.run-completed", "workflow.notification", "inbox.message", "quote.sent", "quote.viewed", "quote.accepted", "quote.rejected", "quote.changes-requested", "quote.expired", "quote.deposit-paid", "invoice.paid", "invoice.overdue", "invoice.viewed", "invoice.partially-paid", "invoice.payment-failed", "invoice.disputed", "invoice.refunded", "invoice.dispute-lost", "invoice.final-notice-sent", "order.backordered", "order.confirmed", "shipment.exception", "purchase-order.received", "purchase-order.overdue", "inventory.low-stock", "task.stage-changed", "billing.usage-threshold", "organization.invite", "organization.membership-updated", "time-off.requested", "time-off.decided", "time-off.canceled", "sick-leave.reported", "sick-leave.returned", "sick-leave.follow-up", "timesheet.submitted", "timesheet.decided", "expense.submitted", "expense.decided", "expense.reimbursed", "employee.document-expiring", "employee.contract-ending", "equipment.assigned", "onboarding.item-assigned", "hiring.application-received", "time.timer-forgotten", "time.budget-warning", "approval.requested", "approval.decided", "approval.escalated" ] }, "organizationIdSchema": { "type": "string", "format": "uuid" }, "upsertNotificationPreferenceInputSchema": { "type": "object", "properties": { "channel": { "$ref": "#/components/schemas/notificationChannelSchema" }, "enabled": { "type": "boolean" }, "organizationId": { "type": [ "string", "null" ], "format": "uuid" }, "type": { "anyOf": [ { "$ref": "#/components/schemas/notificationTypeSchema" }, { "type": "string", "enum": [ "*" ] } ] } }, "required": [ "channel", "enabled", "type" ] } } ``` # List notification subscriptions. (/docs/openapi/listNotificationSubscriptions) Return notification levels for the authenticated member. ## GET /v1/organizations/{organizationId}/notification-subscriptions ```json { "operationId": "listNotificationSubscriptions", "summary": "List notification subscriptions.", "description": "Return notification levels for the authenticated member.", "tags": [ "Notifications" ], "parameters": [ { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "responses": { "200": { "description": "Returns notification subscriptions.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/notificationSubscriptionsListSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "notificationDateTimeSchema": { "type": "string", "format": "date-time" }, "notificationSubscriptionLevelSchema": { "type": "string", "enum": [ "participating", "all", "ignore" ] }, "notificationSubscriptionSchema": { "type": "object", "properties": { "createdAt": { "$ref": "#/components/schemas/notificationDateTimeSchema" }, "level": { "$ref": "#/components/schemas/notificationSubscriptionLevelSchema" }, "organizationId": { "type": "string", "format": "uuid" }, "subjectId": { "type": "string", "minLength": 1 }, "subjectType": { "$ref": "#/components/schemas/notificationSubscriptionSubjectTypeSchema" }, "userId": { "type": "string", "format": "uuid" } }, "required": [ "createdAt", "level", "organizationId", "subjectId", "subjectType", "userId" ] }, "notificationSubscriptionSubjectTypeSchema": { "type": "string", "enum": [ "task", "chat-thread", "inbox-thread", "workflow-run", "agenda-item", "customer", "quote", "invoice", "workflow", "hiring-application", "order" ] }, "notificationSubscriptionsListSchema": { "type": "array", "items": { "$ref": "#/components/schemas/notificationSubscriptionSchema" } }, "organizationIdSchema": { "type": "string", "format": "uuid" } } ``` # Create or update a notification subscription. (/docs/openapi/createNotificationSubscription) Set the notification level for a subject for the authenticated member. ## POST /v1/organizations/{organizationId}/notification-subscriptions ```json { "operationId": "createNotificationSubscription", "summary": "Create or update a notification subscription.", "description": "Set the notification level for a subject for the authenticated member.", "tags": [ "Notifications" ], "parameters": [ { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/notificationSubscriptionCreateInputSchema" } } }, "description": "Subject type, subject id, and notification level." }, "responses": { "200": { "description": "Returns the saved subscription.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/notificationSubscriptionSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "notificationDateTimeSchema": { "type": "string", "format": "date-time" }, "notificationSubscriptionCreateInputSchema": { "type": "object", "properties": { "level": { "$ref": "#/components/schemas/notificationSubscriptionLevelSchema" }, "subjectId": { "type": "string", "minLength": 1, "maxLength": 240 }, "subjectType": { "$ref": "#/components/schemas/notificationSubscriptionSubjectTypeSchema" } }, "required": [ "level", "subjectId", "subjectType" ] }, "notificationSubscriptionLevelSchema": { "type": "string", "enum": [ "participating", "all", "ignore" ] }, "notificationSubscriptionSchema": { "type": "object", "properties": { "createdAt": { "$ref": "#/components/schemas/notificationDateTimeSchema" }, "level": { "$ref": "#/components/schemas/notificationSubscriptionLevelSchema" }, "organizationId": { "type": "string", "format": "uuid" }, "subjectId": { "type": "string", "minLength": 1 }, "subjectType": { "$ref": "#/components/schemas/notificationSubscriptionSubjectTypeSchema" }, "userId": { "type": "string", "format": "uuid" } }, "required": [ "createdAt", "level", "organizationId", "subjectId", "subjectType", "userId" ] }, "notificationSubscriptionSubjectTypeSchema": { "type": "string", "enum": [ "task", "chat-thread", "inbox-thread", "workflow-run", "agenda-item", "customer", "quote", "invoice", "workflow", "hiring-application", "order" ] }, "organizationIdSchema": { "type": "string", "format": "uuid" } } ``` # Delete a notification subscription. (/docs/openapi/deleteNotificationSubscription) Stop watching a subject for the authenticated member. ## DELETE /v1/organizations/{organizationId}/notification-subscriptions ```json { "operationId": "deleteNotificationSubscription", "summary": "Delete a notification subscription.", "description": "Stop watching a subject for the authenticated member.", "tags": [ "Notifications" ], "parameters": [ { "in": "query", "name": "subjectId", "required": true, "schema": { "type": "string", "minLength": 1, "maxLength": 240 }, "example": "123" }, { "in": "query", "name": "subjectType", "required": true, "schema": { "$ref": "#/components/schemas/notificationSubscriptionSubjectTypeSchema" }, "example": "example" }, { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "responses": { "200": { "description": "Confirms the subscription was deleted.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/publicApiSuccessResponseSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "notificationSubscriptionSubjectTypeSchema": { "type": "string", "enum": [ "task", "chat-thread", "inbox-thread", "workflow-run", "agenda-item", "customer", "quote", "invoice", "workflow", "hiring-application", "order" ] }, "organizationIdSchema": { "type": "string", "format": "uuid" }, "publicApiSuccessResponseSchema": { "type": "object", "properties": { "success": { "type": "boolean", "enum": [ true ] } }, "required": [ "success" ] } } ``` # List notifications. (/docs/openapi/listNotifications) Return paginated in-app notifications for the authenticated member of an organization. ## GET /v1/organizations/{organizationId}/notifications ```json { "operationId": "listNotifications", "summary": "List notifications.", "description": "Return paginated in-app notifications for the authenticated member of an organization.", "tags": [ "Notifications" ], "parameters": [ { "in": "query", "name": "offset", "required": true, "schema": { "type": "string" }, "example": "example" }, { "in": "query", "name": "limit", "required": true, "schema": { "type": "string" }, "example": "example" }, { "in": "query", "name": "group", "required": true, "schema": { "type": "string", "enum": [ "all", "needs-action", "running", "happened" ], "default": "all" }, "example": "all" }, { "in": "query", "name": "q", "required": false, "schema": { "type": "string", "maxLength": 200 }, "example": "example" }, { "in": "query", "name": "status", "required": true, "schema": { "type": "string", "enum": [ "all", "unread", "read" ], "default": "all" }, "example": "all" }, { "in": "query", "name": "subjectType", "required": false, "schema": { "allOf": [ { "$ref": "#/components/schemas/notificationSubjectTypeSchema" } ] }, "example": "example" }, { "in": "query", "name": "type", "required": false, "schema": { "allOf": [ { "$ref": "#/components/schemas/notificationTypeSchema" } ] }, "example": "example" }, { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "responses": { "200": { "description": "Returns paginated notifications.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/notificationsListResponseSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "jsonValueSchema": { "anyOf": [ { "type": "string" }, { "type": "number" }, { "type": "boolean" }, { "type": "null" }, { "type": "array", "items": { "$ref": "#/components/schemas/jsonValueSchema" } }, { "type": "object", "additionalProperties": { "$ref": "#/components/schemas/jsonValueSchema" }, "propertyNames": { "type": "string" } } ] }, "listPaginationSchema": { "type": "object", "properties": { "hasMore": { "type": "boolean" }, "limit": { "type": "integer", "minimum": 1, "maximum": 100 }, "offset": { "type": "integer", "minimum": 0 }, "total": { "type": "integer", "minimum": 0 }, "totalIsEstimate": { "type": "boolean" } }, "required": [ "hasMore", "limit", "offset", "total" ] }, "notificationActorSchema": { "type": "object", "properties": { "avatarUrl": { "type": [ "string", "null" ] }, "displayName": { "type": "string", "minLength": 1 }, "userId": { "type": "string", "format": "uuid" } }, "required": [ "avatarUrl", "displayName", "userId" ] }, "notificationDateTimeSchema": { "type": "string", "format": "date-time" }, "notificationMetadataSchema": { "type": "object", "additionalProperties": { "$ref": "#/components/schemas/jsonValueSchema" }, "propertyNames": { "type": "string" } }, "notificationOriginatorKindSchema": { "type": "string", "enum": [ "user", "customer", "system" ] }, "notificationOriginatorSchema": { "type": "object", "properties": { "avatarUrl": { "type": [ "string", "null" ] }, "displayName": { "type": "string", "minLength": 1 }, "kind": { "$ref": "#/components/schemas/notificationOriginatorKindSchema" } }, "required": [ "avatarUrl", "displayName", "kind" ] }, "notificationPrioritySchema": { "type": "string", "enum": [ "low", "normal", "high", "urgent" ] }, "notificationSchema": { "type": "object", "properties": { "actionPath": { "type": [ "string", "null" ], "minLength": 1 }, "actor": { "anyOf": [ { "$ref": "#/components/schemas/notificationActorSchema" }, { "type": "null" } ] }, "actorUserId": { "type": [ "string", "null" ], "format": "uuid" }, "createdAt": { "$ref": "#/components/schemas/notificationDateTimeSchema" }, "deliveredAt": { "$ref": "#/components/schemas/notificationDateTimeSchema" }, "dismissedAt": { "anyOf": [ { "$ref": "#/components/schemas/notificationDateTimeSchema" }, { "type": "null" } ] }, "eventId": { "type": "string", "format": "uuid" }, "id": { "type": "string", "format": "uuid" }, "metadata": { "$ref": "#/components/schemas/notificationMetadataSchema" }, "organizationId": { "type": "string", "format": "uuid" }, "originator": { "$ref": "#/components/schemas/notificationOriginatorSchema" }, "priority": { "$ref": "#/components/schemas/notificationPrioritySchema" }, "readAt": { "anyOf": [ { "$ref": "#/components/schemas/notificationDateTimeSchema" }, { "type": "null" } ] }, "recipientUserId": { "type": "string", "format": "uuid" }, "resolvedAt": { "anyOf": [ { "$ref": "#/components/schemas/notificationDateTimeSchema" }, { "type": "null" } ] }, "subjectId": { "type": "string", "minLength": 1 }, "subjectLabel": { "type": "string", "minLength": 1 }, "subjectType": { "$ref": "#/components/schemas/notificationSubjectTypeSchema" }, "summary": { "type": [ "string", "null" ] }, "type": { "$ref": "#/components/schemas/notificationTypeSchema" } }, "required": [ "actionPath", "actor", "actorUserId", "createdAt", "deliveredAt", "dismissedAt", "eventId", "id", "metadata", "organizationId", "originator", "priority", "readAt", "recipientUserId", "resolvedAt", "subjectId", "subjectLabel", "subjectType", "summary", "type" ] }, "notificationSubjectTypeSchema": { "type": "string", "enum": [ "task", "customer", "agenda-item", "agent-run", "billing", "organization", "workflow-run", "inbox-thread", "quote", "invoice", "chat-thread", "artifact", "employee", "time-off-request", "timesheet", "expense", "hiring-application", "employee-journey", "approval", "order", "shipment", "purchase-order", "product" ] }, "notificationTypeSchema": { "type": "string", "enum": [ "task.assigned", "task.comment", "task.updated", "collaboration.mention", "collaboration.note", "task.due-soon", "task.overdue", "agenda.assigned", "agenda.updated", "agent.approval-required", "agent.run-failed", "agent.run-completed", "workflow.approval-required", "workflow.run-failed", "workflow.run-completed", "workflow.notification", "inbox.message", "quote.sent", "quote.viewed", "quote.accepted", "quote.rejected", "quote.changes-requested", "quote.expired", "quote.deposit-paid", "invoice.paid", "invoice.overdue", "invoice.viewed", "invoice.partially-paid", "invoice.payment-failed", "invoice.disputed", "invoice.refunded", "invoice.dispute-lost", "invoice.final-notice-sent", "order.backordered", "order.confirmed", "shipment.exception", "purchase-order.received", "purchase-order.overdue", "inventory.low-stock", "task.stage-changed", "billing.usage-threshold", "organization.invite", "organization.membership-updated", "time-off.requested", "time-off.decided", "time-off.canceled", "sick-leave.reported", "sick-leave.returned", "sick-leave.follow-up", "timesheet.submitted", "timesheet.decided", "expense.submitted", "expense.decided", "expense.reimbursed", "employee.document-expiring", "employee.contract-ending", "equipment.assigned", "onboarding.item-assigned", "hiring.application-received", "time.timer-forgotten", "time.budget-warning", "approval.requested", "approval.decided", "approval.escalated" ] }, "notificationsListResponseSchema": { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/components/schemas/notificationSchema" } }, "pagination": { "$ref": "#/components/schemas/listPaginationSchema" } }, "required": [ "data", "pagination" ] }, "organizationIdSchema": { "type": "string", "format": "uuid" } } ``` # Update a notification. (/docs/openapi/patchNotification) Mark a notification read, unread, or dismissed for the authenticated member. ## PATCH /v1/organizations/{organizationId}/notifications/{notificationId} ```json { "operationId": "patchNotification", "summary": "Update a notification.", "description": "Mark a notification read, unread, or dismissed for the authenticated member.", "tags": [ "Notifications" ], "parameters": [ { "in": "path", "name": "notificationId", "required": true, "schema": { "type": "string", "format": "uuid" }, "example": "123e4567-e89b-12d3-a456-426614174000" }, { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/notificationPatchInputSchema" } } }, "description": "Read, unread, or dismiss the notification." }, "responses": { "200": { "description": "Returns the updated notification.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/notificationSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "jsonValueSchema": { "anyOf": [ { "type": "string" }, { "type": "number" }, { "type": "boolean" }, { "type": "null" }, { "type": "array", "items": { "$ref": "#/components/schemas/jsonValueSchema" } }, { "type": "object", "additionalProperties": { "$ref": "#/components/schemas/jsonValueSchema" }, "propertyNames": { "type": "string" } } ] }, "notificationActorSchema": { "type": "object", "properties": { "avatarUrl": { "type": [ "string", "null" ] }, "displayName": { "type": "string", "minLength": 1 }, "userId": { "type": "string", "format": "uuid" } }, "required": [ "avatarUrl", "displayName", "userId" ] }, "notificationDateTimeSchema": { "type": "string", "format": "date-time" }, "notificationMetadataSchema": { "type": "object", "additionalProperties": { "$ref": "#/components/schemas/jsonValueSchema" }, "propertyNames": { "type": "string" } }, "notificationOriginatorKindSchema": { "type": "string", "enum": [ "user", "customer", "system" ] }, "notificationOriginatorSchema": { "type": "object", "properties": { "avatarUrl": { "type": [ "string", "null" ] }, "displayName": { "type": "string", "minLength": 1 }, "kind": { "$ref": "#/components/schemas/notificationOriginatorKindSchema" } }, "required": [ "avatarUrl", "displayName", "kind" ] }, "notificationPatchInputSchema": { "type": "object", "properties": { "dismissed": { "type": "boolean" }, "read": { "type": "boolean" } } }, "notificationPrioritySchema": { "type": "string", "enum": [ "low", "normal", "high", "urgent" ] }, "notificationSchema": { "type": "object", "properties": { "actionPath": { "type": [ "string", "null" ], "minLength": 1 }, "actor": { "anyOf": [ { "$ref": "#/components/schemas/notificationActorSchema" }, { "type": "null" } ] }, "actorUserId": { "type": [ "string", "null" ], "format": "uuid" }, "createdAt": { "$ref": "#/components/schemas/notificationDateTimeSchema" }, "deliveredAt": { "$ref": "#/components/schemas/notificationDateTimeSchema" }, "dismissedAt": { "anyOf": [ { "$ref": "#/components/schemas/notificationDateTimeSchema" }, { "type": "null" } ] }, "eventId": { "type": "string", "format": "uuid" }, "id": { "type": "string", "format": "uuid" }, "metadata": { "$ref": "#/components/schemas/notificationMetadataSchema" }, "organizationId": { "type": "string", "format": "uuid" }, "originator": { "$ref": "#/components/schemas/notificationOriginatorSchema" }, "priority": { "$ref": "#/components/schemas/notificationPrioritySchema" }, "readAt": { "anyOf": [ { "$ref": "#/components/schemas/notificationDateTimeSchema" }, { "type": "null" } ] }, "recipientUserId": { "type": "string", "format": "uuid" }, "resolvedAt": { "anyOf": [ { "$ref": "#/components/schemas/notificationDateTimeSchema" }, { "type": "null" } ] }, "subjectId": { "type": "string", "minLength": 1 }, "subjectLabel": { "type": "string", "minLength": 1 }, "subjectType": { "$ref": "#/components/schemas/notificationSubjectTypeSchema" }, "summary": { "type": [ "string", "null" ] }, "type": { "$ref": "#/components/schemas/notificationTypeSchema" } }, "required": [ "actionPath", "actor", "actorUserId", "createdAt", "deliveredAt", "dismissedAt", "eventId", "id", "metadata", "organizationId", "originator", "priority", "readAt", "recipientUserId", "resolvedAt", "subjectId", "subjectLabel", "subjectType", "summary", "type" ] }, "notificationSubjectTypeSchema": { "type": "string", "enum": [ "task", "customer", "agenda-item", "agent-run", "billing", "organization", "workflow-run", "inbox-thread", "quote", "invoice", "chat-thread", "artifact", "employee", "time-off-request", "timesheet", "expense", "hiring-application", "employee-journey", "approval", "order", "shipment", "purchase-order", "product" ] }, "notificationTypeSchema": { "type": "string", "enum": [ "task.assigned", "task.comment", "task.updated", "collaboration.mention", "collaboration.note", "task.due-soon", "task.overdue", "agenda.assigned", "agenda.updated", "agent.approval-required", "agent.run-failed", "agent.run-completed", "workflow.approval-required", "workflow.run-failed", "workflow.run-completed", "workflow.notification", "inbox.message", "quote.sent", "quote.viewed", "quote.accepted", "quote.rejected", "quote.changes-requested", "quote.expired", "quote.deposit-paid", "invoice.paid", "invoice.overdue", "invoice.viewed", "invoice.partially-paid", "invoice.payment-failed", "invoice.disputed", "invoice.refunded", "invoice.dispute-lost", "invoice.final-notice-sent", "order.backordered", "order.confirmed", "shipment.exception", "purchase-order.received", "purchase-order.overdue", "inventory.low-stock", "task.stage-changed", "billing.usage-threshold", "organization.invite", "organization.membership-updated", "time-off.requested", "time-off.decided", "time-off.canceled", "sick-leave.reported", "sick-leave.returned", "sick-leave.follow-up", "timesheet.submitted", "timesheet.decided", "expense.submitted", "expense.decided", "expense.reimbursed", "employee.document-expiring", "employee.contract-ending", "equipment.assigned", "onboarding.item-assigned", "hiring.application-received", "time.timer-forgotten", "time.budget-warning", "approval.requested", "approval.decided", "approval.escalated" ] }, "organizationIdSchema": { "type": "string", "format": "uuid" } } ``` # Get the composed notification inbox. (/docs/openapi/getNotificationInbox) Return the mixed attention inbox for the authenticated member, including live running work and pending invitations. ## GET /v1/organizations/{organizationId}/notifications/inbox ```json { "operationId": "getNotificationInbox", "summary": "Get the composed notification inbox.", "description": "Return the mixed attention inbox for the authenticated member, including live running work and pending invitations.", "tags": [ "Notifications" ], "parameters": [ { "in": "query", "name": "limit", "required": true, "schema": { "type": "integer", "minimum": 1, "maximum": 20, "default": 10 }, "example": 1 }, { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "responses": { "200": { "description": "Returns the composed notification inbox.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/notificationInboxSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "jsonValueSchema": { "anyOf": [ { "type": "string" }, { "type": "number" }, { "type": "boolean" }, { "type": "null" }, { "type": "array", "items": { "$ref": "#/components/schemas/jsonValueSchema" } }, { "type": "object", "additionalProperties": { "$ref": "#/components/schemas/jsonValueSchema" }, "propertyNames": { "type": "string" } } ] }, "notificationActorSchema": { "type": "object", "properties": { "avatarUrl": { "type": [ "string", "null" ] }, "displayName": { "type": "string", "minLength": 1 }, "userId": { "type": "string", "format": "uuid" } }, "required": [ "avatarUrl", "displayName", "userId" ] }, "notificationApprovalPreviewSchema": { "type": "object", "properties": { "actionPath": { "type": [ "string", "null" ] }, "agentRunId": { "type": [ "string", "null" ], "format": "uuid" }, "escalated": { "type": "boolean" }, "id": { "type": "string", "format": "uuid" }, "requestedAt": { "$ref": "#/components/schemas/notificationDateTimeSchema" }, "requesterName": { "type": [ "string", "null" ] }, "summary": { "type": [ "string", "null" ] }, "title": { "type": "string", "minLength": 1 }, "workflowRunId": { "type": [ "string", "null" ], "format": "uuid" } }, "required": [ "actionPath", "agentRunId", "escalated", "id", "requestedAt", "requesterName", "summary", "title", "workflowRunId" ] }, "notificationDateTimeSchema": { "type": "string", "format": "date-time" }, "notificationInboxItemSchema": { "type": "object", "properties": { "actionPath": { "type": [ "string", "null" ] }, "activity": { "anyOf": [ { "$ref": "#/components/schemas/notificationLiveActivitySchema" }, { "type": "null" } ] }, "approval": { "anyOf": [ { "$ref": "#/components/schemas/notificationApprovalPreviewSchema" }, { "type": "null" } ] }, "createdAt": { "$ref": "#/components/schemas/notificationDateTimeSchema" }, "group": { "type": "string", "enum": [ "needs-action", "running", "happened" ] }, "id": { "type": "string", "minLength": 1 }, "invite": { "anyOf": [ { "$ref": "#/components/schemas/notificationInvitePreviewSchema" }, { "type": "null" } ] }, "notification": { "anyOf": [ { "$ref": "#/components/schemas/notificationSchema" }, { "type": "null" } ] }, "originator": { "$ref": "#/components/schemas/notificationOriginatorSchema" }, "readAt": { "anyOf": [ { "$ref": "#/components/schemas/notificationDateTimeSchema" }, { "type": "null" } ] }, "resolvedAt": { "anyOf": [ { "$ref": "#/components/schemas/notificationDateTimeSchema" }, { "type": "null" } ] }, "source": { "$ref": "#/components/schemas/notificationInboxSourceSchema" }, "subjectLabel": { "type": "string", "minLength": 1 }, "summary": { "type": [ "string", "null" ] }, "type": { "anyOf": [ { "$ref": "#/components/schemas/notificationTypeSchema" }, { "type": "string", "enum": [ "activity.running", "activity.artifact", "organization.invite" ] } ] } }, "required": [ "actionPath", "activity", "approval", "createdAt", "group", "id", "invite", "notification", "originator", "readAt", "resolvedAt", "source", "subjectLabel", "summary", "type" ] }, "notificationInboxSchema": { "type": "object", "properties": { "items": { "type": "array", "items": { "$ref": "#/components/schemas/notificationInboxItemSchema" } }, "pendingActionCount": { "type": "integer", "minimum": 0 }, "unreadCount": { "type": "integer", "minimum": 0 } }, "required": [ "items", "pendingActionCount", "unreadCount" ] }, "notificationInboxSourceSchema": { "type": "string", "enum": [ "notification", "activity", "invite", "approval" ] }, "notificationInvitePreviewSchema": { "type": "object", "properties": { "createdAt": { "type": "string", "minLength": 1 }, "id": { "type": "string", "format": "uuid" }, "organizationName": { "type": "string", "minLength": 1 } }, "required": [ "createdAt", "id", "organizationName" ] }, "notificationLiveActivityKindSchema": { "type": "string", "enum": [ "agent-run", "artifact", "workflow-run" ] }, "notificationLiveActivitySchema": { "type": "object", "properties": { "actionPath": { "type": [ "string", "null" ] }, "activeStreamId": { "type": [ "string", "null" ], "minLength": 1 }, "artifactContextFingerprint": { "type": [ "string", "null" ], "minLength": 1 }, "createdAt": { "$ref": "#/components/schemas/notificationDateTimeSchema" }, "group": { "type": "string", "enum": [ "running", "needs-review", "completed", "failed" ] }, "id": { "type": "string", "minLength": 1 }, "kind": { "$ref": "#/components/schemas/notificationLiveActivityKindSchema" }, "originator": { "$ref": "#/components/schemas/notificationOriginatorSchema" }, "pendingApprovalCount": { "type": "integer", "minimum": 0 }, "pendingApprovalId": { "type": [ "string", "null" ], "minLength": 1 }, "pendingApprovalToolCallId": { "type": [ "string", "null" ], "minLength": 1 }, "status": { "type": "string", "minLength": 1 }, "summary": { "type": [ "string", "null" ] }, "threadId": { "type": [ "string", "null" ] }, "title": { "type": "string", "minLength": 1 }, "updatedAt": { "$ref": "#/components/schemas/notificationDateTimeSchema" }, "workflowDefinitionId": { "type": [ "string", "null" ] }, "workflowRunId": { "type": [ "string", "null" ] } }, "required": [ "actionPath", "activeStreamId", "artifactContextFingerprint", "createdAt", "group", "id", "kind", "originator", "pendingApprovalCount", "pendingApprovalId", "pendingApprovalToolCallId", "status", "summary", "threadId", "title", "updatedAt", "workflowDefinitionId", "workflowRunId" ] }, "notificationMetadataSchema": { "type": "object", "additionalProperties": { "$ref": "#/components/schemas/jsonValueSchema" }, "propertyNames": { "type": "string" } }, "notificationOriginatorKindSchema": { "type": "string", "enum": [ "user", "customer", "system" ] }, "notificationOriginatorSchema": { "type": "object", "properties": { "avatarUrl": { "type": [ "string", "null" ] }, "displayName": { "type": "string", "minLength": 1 }, "kind": { "$ref": "#/components/schemas/notificationOriginatorKindSchema" } }, "required": [ "avatarUrl", "displayName", "kind" ] }, "notificationPrioritySchema": { "type": "string", "enum": [ "low", "normal", "high", "urgent" ] }, "notificationSchema": { "type": "object", "properties": { "actionPath": { "type": [ "string", "null" ], "minLength": 1 }, "actor": { "anyOf": [ { "$ref": "#/components/schemas/notificationActorSchema" }, { "type": "null" } ] }, "actorUserId": { "type": [ "string", "null" ], "format": "uuid" }, "createdAt": { "$ref": "#/components/schemas/notificationDateTimeSchema" }, "deliveredAt": { "$ref": "#/components/schemas/notificationDateTimeSchema" }, "dismissedAt": { "anyOf": [ { "$ref": "#/components/schemas/notificationDateTimeSchema" }, { "type": "null" } ] }, "eventId": { "type": "string", "format": "uuid" }, "id": { "type": "string", "format": "uuid" }, "metadata": { "$ref": "#/components/schemas/notificationMetadataSchema" }, "organizationId": { "type": "string", "format": "uuid" }, "originator": { "$ref": "#/components/schemas/notificationOriginatorSchema" }, "priority": { "$ref": "#/components/schemas/notificationPrioritySchema" }, "readAt": { "anyOf": [ { "$ref": "#/components/schemas/notificationDateTimeSchema" }, { "type": "null" } ] }, "recipientUserId": { "type": "string", "format": "uuid" }, "resolvedAt": { "anyOf": [ { "$ref": "#/components/schemas/notificationDateTimeSchema" }, { "type": "null" } ] }, "subjectId": { "type": "string", "minLength": 1 }, "subjectLabel": { "type": "string", "minLength": 1 }, "subjectType": { "$ref": "#/components/schemas/notificationSubjectTypeSchema" }, "summary": { "type": [ "string", "null" ] }, "type": { "$ref": "#/components/schemas/notificationTypeSchema" } }, "required": [ "actionPath", "actor", "actorUserId", "createdAt", "deliveredAt", "dismissedAt", "eventId", "id", "metadata", "organizationId", "originator", "priority", "readAt", "recipientUserId", "resolvedAt", "subjectId", "subjectLabel", "subjectType", "summary", "type" ] }, "notificationSubjectTypeSchema": { "type": "string", "enum": [ "task", "customer", "agenda-item", "agent-run", "billing", "organization", "workflow-run", "inbox-thread", "quote", "invoice", "chat-thread", "artifact", "employee", "time-off-request", "timesheet", "expense", "hiring-application", "employee-journey", "approval", "order", "shipment", "purchase-order", "product" ] }, "notificationTypeSchema": { "type": "string", "enum": [ "task.assigned", "task.comment", "task.updated", "collaboration.mention", "collaboration.note", "task.due-soon", "task.overdue", "agenda.assigned", "agenda.updated", "agent.approval-required", "agent.run-failed", "agent.run-completed", "workflow.approval-required", "workflow.run-failed", "workflow.run-completed", "workflow.notification", "inbox.message", "quote.sent", "quote.viewed", "quote.accepted", "quote.rejected", "quote.changes-requested", "quote.expired", "quote.deposit-paid", "invoice.paid", "invoice.overdue", "invoice.viewed", "invoice.partially-paid", "invoice.payment-failed", "invoice.disputed", "invoice.refunded", "invoice.dispute-lost", "invoice.final-notice-sent", "order.backordered", "order.confirmed", "shipment.exception", "purchase-order.received", "purchase-order.overdue", "inventory.low-stock", "task.stage-changed", "billing.usage-threshold", "organization.invite", "organization.membership-updated", "time-off.requested", "time-off.decided", "time-off.canceled", "sick-leave.reported", "sick-leave.returned", "sick-leave.follow-up", "timesheet.submitted", "timesheet.decided", "expense.submitted", "expense.decided", "expense.reimbursed", "employee.document-expiring", "employee.contract-ending", "equipment.assigned", "onboarding.item-assigned", "hiring.application-received", "time.timer-forgotten", "time.budget-warning", "approval.requested", "approval.decided", "approval.escalated" ] }, "organizationIdSchema": { "type": "string", "format": "uuid" } } ``` # Mark all notifications read. (/docs/openapi/markAllNotificationsRead) Mark every unread notification in the current organization as read for the authenticated member. ## POST /v1/organizations/{organizationId}/notifications/mark-all-read ```json { "operationId": "markAllNotificationsRead", "summary": "Mark all notifications read.", "description": "Mark every unread notification in the current organization as read for the authenticated member.", "tags": [ "Notifications" ], "parameters": [ { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "responses": { "200": { "description": "Returns how many notifications were marked read.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/notificationMarkAllReadResponseSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "notificationMarkAllReadResponseSchema": { "type": "object", "properties": { "count": { "type": "integer", "minimum": 0 } }, "required": [ "count" ] }, "organizationIdSchema": { "type": "string", "format": "uuid" } } ``` # Read a new-hire pre-boarding page. (/docs/openapi/getPublicPreboardingPage) Returns the organization, start date, and requested documents for a pre-boarding token link. Expired, revoked, and already-linked links return 404. ## GET /v1/public/onboarding/{token} ```json { "operationId": "getPublicPreboardingPage", "summary": "Read a new-hire pre-boarding page.", "description": "Returns the organization, start date, and requested documents for a pre-boarding token link. Expired, revoked, and already-linked links return 404.", "tags": [ "Onboarding" ], "parameters": [ { "in": "path", "name": "token", "required": true, "schema": { "type": "string", "minLength": 16, "maxLength": 200 }, "example": "example" } ], "responses": { "200": { "description": "Returns the pre-boarding page for the new hire.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/publicPreboardingPageResponseSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "preboardingPageSchema": { "type": "object", "properties": { "detailsSubmitted": { "type": "boolean" }, "expiresAt": { "type": [ "string", "null" ] }, "firstName": { "type": "string" }, "organizationName": { "type": "string" }, "requestedDocuments": { "type": "array", "items": { "type": "object", "properties": { "done": { "type": "boolean" }, "itemId": { "type": "string", "format": "uuid" }, "title": { "type": "string" } }, "required": [ "done", "itemId", "title" ] } }, "startDate": { "type": "object" } }, "required": [ "detailsSubmitted", "expiresAt", "firstName", "organizationName", "requestedDocuments", "startDate" ] }, "publicPreboardingPageResponseSchema": { "$ref": "#/components/schemas/preboardingPageSchema" } } ``` # Submit new-hire private details. (/docs/openapi/submitPublicPreboardingDetails) Saves the new hire's private details (birth date, contact, bank account, emergency contact, address) through a pre-boarding token link and completes the pre-boarding form step. ## POST /v1/public/onboarding/{token} ```json { "operationId": "submitPublicPreboardingDetails", "summary": "Submit new-hire private details.", "description": "Saves the new hire's private details (birth date, contact, bank account, emergency contact, address) through a pre-boarding token link and completes the pre-boarding form step.", "tags": [ "Onboarding" ], "parameters": [ { "in": "path", "name": "token", "required": true, "schema": { "type": "string", "minLength": 16, "maxLength": 200 }, "example": "example" } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/publicPreboardingDetailsBodySchema" } } }, "description": "Private details entered by the new hire." }, "responses": { "200": { "description": "The details were saved.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/publicPreboardingOkResponseSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "employeePrivateDetailsInputSchema": { "type": "object", "properties": { "birthDate": { "type": "object" }, "emergencyContactName": { "type": [ "string", "null" ], "maxLength": 160 }, "emergencyContactPhone": { "type": [ "string", "null" ], "maxLength": 40 }, "emergencyContactRelation": { "type": [ "string", "null" ], "maxLength": 80 }, "homeAddressLine1": { "type": [ "string", "null" ], "maxLength": 200 }, "homeCity": { "type": [ "string", "null" ], "maxLength": 120 }, "homeCountry": { "type": [ "string", "null" ], "maxLength": 2 }, "homePostalCode": { "type": [ "string", "null" ], "maxLength": 20 }, "iban": { "anyOf": [ { "type": [ "string", "null" ], "maxLength": 40 }, { "type": "string", "enum": [ "" ] } ] }, "personalEmail": { "anyOf": [ { "type": [ "string", "null" ], "format": "email", "maxLength": 320 }, { "type": "string", "enum": [ "" ] } ] }, "phone": { "type": [ "string", "null" ], "maxLength": 40 } }, "required": [ "emergencyContactName", "emergencyContactPhone", "emergencyContactRelation", "homeAddressLine1", "homeCity", "homeCountry", "homePostalCode", "phone" ] }, "preboardingSubmitInputSchema": { "$ref": "#/components/schemas/employeePrivateDetailsInputSchema" }, "publicPreboardingDetailsBodySchema": { "$ref": "#/components/schemas/preboardingSubmitInputSchema" }, "publicPreboardingOkResponseSchema": { "type": "object", "properties": { "ok": { "type": "boolean", "enum": [ true ] } }, "required": [ "ok" ] } } ``` # Upload a requested pre-boarding document. (/docs/openapi/uploadPublicPreboardingDocument) Uploads a document the new hire was asked for (multipart form with `itemId` and `file`, at most 20 MB) through a pre-boarding token link and completes that checklist step. ## POST /v1/public/onboarding/{token}/documents ```json { "operationId": "uploadPublicPreboardingDocument", "summary": "Upload a requested pre-boarding document.", "description": "Uploads a document the new hire was asked for (multipart form with `itemId` and `file`, at most 20 MB) through a pre-boarding token link and completes that checklist step.", "tags": [ "Onboarding" ], "parameters": [ { "in": "path", "name": "token", "required": true, "schema": { "type": "string", "minLength": 16, "maxLength": 200 }, "example": "example" } ], "requestBody": { "content": { "multipart/form-data": { "schema": { "$ref": "#/components/schemas/publicPreboardingDocumentBodySchema" }, "encoding": { "file": { "contentType": "application/octet-stream" } } } }, "description": "The checklist item id and the file." }, "responses": { "200": { "description": "The document was stored on the employee record.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/publicPreboardingDocumentResponseSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "publicPreboardingDocumentBodySchema": { "type": "object", "properties": { "file": { "type": "string", "contentMediaType": "application/octet-stream" }, "itemId": { "type": "string", "format": "uuid" } }, "required": [ "file", "itemId" ] }, "publicPreboardingDocumentResponseSchema": { "type": "object", "properties": { "ok": { "type": "boolean", "enum": [ true ] } }, "required": [ "ok" ] } } ``` # List sales orders. (/docs/openapi/listOrders) Return sales orders, newest first. Search matches the order number, external id, and buyer name. ## GET /v1/organizations/{organizationId}/orders ```json { "operationId": "listOrders", "summary": "List sales orders.", "description": "Return sales orders, newest first. Search matches the order number, external id, and buyer name.", "tags": [ "Orders" ], "parameters": [ { "in": "query", "name": "customerId", "required": false, "schema": { "type": "string", "format": "uuid" }, "example": "123e4567-e89b-12d3-a456-426614174000" }, { "in": "query", "name": "limit", "required": true, "schema": { "default": 50, "type": "integer", "minimum": 1, "maximum": 100 }, "example": 1 }, { "in": "query", "name": "offset", "required": true, "schema": { "default": 0, "type": "integer", "minimum": 0, "maximum": 9007199254740991 }, "example": 1 }, { "in": "query", "name": "q", "required": false, "schema": { "type": "string", "maxLength": 200 }, "example": "example" }, { "in": "query", "name": "status", "required": false, "schema": { "allOf": [ { "$ref": "#/components/schemas/orderStatusSchema" } ] }, "example": "example" }, { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "responses": { "200": { "description": "Returns sales orders.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ordersResponseSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "isoDateSchema": { "type": "string", "format": "date" }, "listPaginationSchema": { "type": "object", "properties": { "hasMore": { "type": "boolean" }, "limit": { "type": "integer", "minimum": 1, "maximum": 100 }, "offset": { "type": "integer", "minimum": 0 }, "total": { "type": "integer", "minimum": 0 }, "totalIsEstimate": { "type": "boolean" } }, "required": [ "hasMore", "limit", "offset", "total" ] }, "orderChannelSchema": { "type": "string", "enum": [ "manual", "quote", "inbox", "api", "integration" ] }, "orderCustomerSchema": { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "name": { "type": "string" } }, "required": [ "id", "name" ] }, "orderDeliveryMethodSchema": { "type": "string", "enum": [ "pickup", "own_delivery", "carrier", "digital", "none" ] }, "orderInvoicingPolicySchema": { "type": "string", "enum": [ "before_fulfillment", "on_fulfillment", "manual" ] }, "orderStatusSchema": { "type": "string", "enum": [ "draft", "confirmed", "on_hold", "completed", "canceled" ] }, "orderSummarySchema": { "type": "object", "properties": { "canceledAt": { "type": [ "string", "null" ] }, "channel": { "$ref": "#/components/schemas/orderChannelSchema" }, "completedAt": { "type": [ "string", "null" ] }, "confirmedAt": { "type": [ "string", "null" ] }, "createdAt": { "type": "string" }, "currency": { "type": "string" }, "customer": { "$ref": "#/components/schemas/orderCustomerSchema" }, "deliveryMethod": { "$ref": "#/components/schemas/orderDeliveryMethodSchema" }, "formattedNumber": { "type": "string" }, "id": { "type": "string", "format": "uuid" }, "invoicingPolicy": { "$ref": "#/components/schemas/orderInvoicingPolicySchema" }, "number": { "type": "integer" }, "quoteId": { "type": [ "string", "null" ], "format": "uuid" }, "quoteNumber": { "type": [ "integer", "null" ] }, "requestedDeliveryDate": { "anyOf": [ { "$ref": "#/components/schemas/isoDateSchema" }, { "type": "null" } ] }, "status": { "$ref": "#/components/schemas/orderStatusSchema" }, "subtotalExcl": { "type": "number" }, "totalIncl": { "type": "number" }, "updatedAt": { "type": "string" }, "vatTotal": { "type": "number" } }, "required": [ "canceledAt", "channel", "completedAt", "confirmedAt", "createdAt", "currency", "customer", "deliveryMethod", "formattedNumber", "id", "invoicingPolicy", "number", "quoteId", "quoteNumber", "requestedDeliveryDate", "status", "subtotalExcl", "totalIncl", "updatedAt", "vatTotal" ] }, "ordersResponseSchema": { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/components/schemas/orderSummarySchema" } }, "pagination": { "$ref": "#/components/schemas/listPaginationSchema" } }, "required": [ "data", "pagination" ] }, "organizationIdSchema": { "type": "string", "format": "uuid" } } ``` # Take in a sales order. (/docs/openapi/createOrder) Create an order from a webshop or other channel. The buyer is matched on email or created, lines are matched to variants by id, SKU, or EAN, and a promotion code is applied to the whole order. Posting the same `source` and `externalId` again returns the existing order with status 200 instead of creating a second one. Set `confirm` to reserve stock right away. ## POST /v1/organizations/{organizationId}/orders ```json { "operationId": "createOrder", "summary": "Take in a sales order.", "description": "Create an order from a webshop or other channel. The buyer is matched on email or created, lines are matched to variants by id, SKU, or EAN, and a promotion code is applied to the whole order. Posting the same `source` and `externalId` again returns the existing order with status 200 instead of creating a second one. Set `confirm` to reserve stock right away.", "tags": [ "Orders" ], "parameters": [ { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/orderIntakeInputSchema" } } }, "description": "The incoming order." }, "responses": { "201": { "description": "Returns the order and whether it was created.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/orderIntakeResultSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "currencySchema": { "type": "string", "pattern": "^[A-Z]{3}$" }, "isoDateSchema": { "type": "string", "format": "date" }, "orderAddressInputSchema": { "type": "object", "properties": { "city": { "type": [ "string", "null" ], "maxLength": 120 }, "country": { "type": [ "string", "null" ], "pattern": "^[A-Z]{2}$" }, "line1": { "type": [ "string", "null" ], "maxLength": 200 }, "line2": { "type": [ "string", "null" ], "maxLength": 200 }, "name": { "type": [ "string", "null" ], "maxLength": 200 }, "phone": { "type": [ "string", "null" ], "maxLength": 40 }, "postalCode": { "type": [ "string", "null" ], "maxLength": 20 } }, "required": [ "city", "line1", "line2", "name", "phone", "postalCode" ] }, "orderAddressSchema": { "type": "object", "properties": { "city": { "type": [ "string", "null" ] }, "country": { "type": [ "string", "null" ] }, "line1": { "type": [ "string", "null" ] }, "line2": { "type": [ "string", "null" ] }, "name": { "type": [ "string", "null" ] }, "phone": { "type": [ "string", "null" ] }, "postalCode": { "type": [ "string", "null" ] } }, "required": [ "city", "country", "line1", "line2", "name", "phone", "postalCode" ] }, "orderChannelSchema": { "type": "string", "enum": [ "manual", "quote", "inbox", "api", "integration" ] }, "orderCustomerSchema": { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "name": { "type": "string" } }, "required": [ "id", "name" ] }, "orderDeliveryMethodSchema": { "type": "string", "enum": [ "pickup", "own_delivery", "carrier", "digital", "none" ] }, "orderDiscountKindSchema": { "type": "string", "enum": [ "none", "percentage", "fixed" ] }, "orderFulfillmentStatusSchema": { "type": "string", "enum": [ "unfulfilled", "partial", "fulfilled" ] }, "orderIntakeCustomerSchema": { "type": "object", "properties": { "companyName": { "type": [ "string", "null" ], "maxLength": 240 }, "email": { "type": "string", "format": "email", "maxLength": 320 }, "name": { "type": "string", "minLength": 1, "maxLength": 240 }, "phone": { "type": [ "string", "null" ], "maxLength": 40 } }, "required": [ "companyName", "email", "name", "phone" ] }, "orderIntakeInputSchema": { "type": "object", "properties": { "confirm": { "type": "boolean", "default": false }, "currency": { "allOf": [ { "$ref": "#/components/schemas/currencySchema" } ] }, "customer": { "allOf": [ { "$ref": "#/components/schemas/orderIntakeCustomerSchema" } ] }, "customerId": { "type": "string", "format": "uuid" }, "customerNote": { "type": [ "string", "null" ], "maxLength": 4000 }, "deliveryMethod": { "allOf": [ { "$ref": "#/components/schemas/orderDeliveryMethodSchema" } ] }, "externalId": { "type": "string", "minLength": 1, "maxLength": 200 }, "lines": { "type": "array", "items": { "$ref": "#/components/schemas/orderIntakeLineSchema" }, "minItems": 1, "maxItems": 500 }, "notes": { "type": [ "string", "null" ], "maxLength": 4000 }, "promotionCode": { "type": "string", "minLength": 1, "maxLength": 40 }, "requestedDeliveryDate": { "allOf": [ { "$ref": "#/components/schemas/isoDateSchema" } ] }, "shippingAddress": { "allOf": [ { "$ref": "#/components/schemas/orderAddressInputSchema" } ] }, "source": { "type": "string", "minLength": 1, "maxLength": 80 }, "vatInclusive": { "type": "boolean" } }, "required": [ "confirm", "customerNote", "externalId", "lines", "notes", "source" ] }, "orderIntakeLineSchema": { "type": "object", "properties": { "ean": { "type": "string", "minLength": 1, "maxLength": 40 }, "quantity": { "type": "number", "exclusiveMinimum": 0, "maximum": 1000000 }, "sku": { "type": "string", "minLength": 1, "maxLength": 120 }, "title": { "type": [ "string", "null" ], "maxLength": 300 }, "unitPrice": { "type": "number", "minimum": 0, "maximum": 100000000 }, "variantId": { "type": "string", "format": "uuid" }, "vatRate": { "type": "number", "minimum": 0, "maximum": 100 } }, "required": [ "quantity", "title" ] }, "orderIntakeResultSchema": { "type": "object", "properties": { "created": { "type": "boolean" }, "order": { "$ref": "#/components/schemas/orderSchema" } }, "required": [ "created", "order" ] }, "orderInvoiceSchema": { "type": "object", "properties": { "amountDueCents": { "type": "number" }, "amountPaidCents": { "type": "number" }, "createdAt": { "type": "string" }, "displayNumber": { "type": [ "string", "null" ] }, "id": { "type": "string", "format": "uuid" }, "kind": { "type": "string" }, "status": { "type": "string" }, "totalIncl": { "type": "number" } }, "required": [ "amountDueCents", "amountPaidCents", "createdAt", "displayNumber", "id", "kind", "status", "totalIncl" ] }, "orderInvoicingPolicySchema": { "type": "string", "enum": [ "before_fulfillment", "on_fulfillment", "manual" ] }, "orderLineSchema": { "type": "object", "properties": { "allocated": { "type": "number" }, "backordered": { "type": "number" }, "description": { "type": [ "string", "null" ] }, "discountKind": { "$ref": "#/components/schemas/orderDiscountKindSchema" }, "discountValue": { "type": "number" }, "fulfilled": { "type": "number" }, "id": { "type": "string", "format": "uuid" }, "invoiced": { "type": "number" }, "lineType": { "$ref": "#/components/schemas/orderLineTypeSchema" }, "position": { "type": "number" }, "productId": { "type": [ "string", "null" ], "format": "uuid" }, "productType": { "type": [ "string", "null" ] }, "promotionId": { "type": [ "string", "null" ], "format": "uuid" }, "quantity": { "type": "number" }, "quantityCanceled": { "type": "number" }, "returned": { "type": "number" }, "sku": { "type": [ "string", "null" ] }, "title": { "type": "string" }, "trackInventory": { "type": "boolean" }, "unit": { "type": [ "string", "null" ] }, "unitPrice": { "type": "number" }, "variantId": { "type": [ "string", "null" ], "format": "uuid" }, "vatRate": { "type": "number" } }, "required": [ "allocated", "backordered", "description", "discountKind", "discountValue", "fulfilled", "id", "invoiced", "lineType", "position", "productId", "productType", "promotionId", "quantity", "quantityCanceled", "returned", "sku", "title", "trackInventory", "unit", "unitPrice", "variantId", "vatRate" ] }, "orderLineTypeSchema": { "type": "string", "enum": [ "product", "custom", "shipping" ] }, "orderPaymentStatusSchema": { "type": "string", "enum": [ "none", "unpaid", "partial", "paid" ] }, "orderSchema": { "type": "object", "properties": { "canceledAt": { "type": [ "string", "null" ] }, "channel": { "$ref": "#/components/schemas/orderChannelSchema" }, "completedAt": { "type": [ "string", "null" ] }, "confirmedAt": { "type": [ "string", "null" ] }, "createdAt": { "type": "string" }, "currency": { "type": "string" }, "customer": { "$ref": "#/components/schemas/orderCustomerSchema" }, "deliveryMethod": { "$ref": "#/components/schemas/orderDeliveryMethodSchema" }, "formattedNumber": { "type": "string" }, "id": { "type": "string", "format": "uuid" }, "invoicingPolicy": { "$ref": "#/components/schemas/orderInvoicingPolicySchema" }, "number": { "type": "integer" }, "quoteId": { "type": [ "string", "null" ], "format": "uuid" }, "quoteNumber": { "type": [ "integer", "null" ] }, "requestedDeliveryDate": { "anyOf": [ { "$ref": "#/components/schemas/isoDateSchema" }, { "type": "null" } ] }, "status": { "$ref": "#/components/schemas/orderStatusSchema" }, "subtotalExcl": { "type": "number" }, "totalIncl": { "type": "number" }, "updatedAt": { "type": "string" }, "vatTotal": { "type": "number" }, "buyerEmail": { "type": [ "string", "null" ] }, "buyerName": { "type": [ "string", "null" ] }, "buyerPhone": { "type": [ "string", "null" ] }, "cancelReason": { "type": [ "string", "null" ] }, "createdBy": { "type": [ "string", "null" ], "format": "uuid" }, "customerContactId": { "type": [ "string", "null" ], "format": "uuid" }, "customerNote": { "type": [ "string", "null" ] }, "discountDescription": { "type": [ "string", "null" ] }, "discountKind": { "$ref": "#/components/schemas/orderDiscountKindSchema" }, "discountValue": { "type": "number" }, "externalId": { "type": [ "string", "null" ] }, "fulfillmentStatus": { "$ref": "#/components/schemas/orderFulfillmentStatusSchema" }, "invoices": { "type": "array", "items": { "$ref": "#/components/schemas/orderInvoiceSchema" } }, "lines": { "type": "array", "items": { "$ref": "#/components/schemas/orderLineSchema" } }, "notes": { "type": [ "string", "null" ] }, "paymentRequired": { "type": "boolean" }, "paymentStatus": { "$ref": "#/components/schemas/orderPaymentStatusSchema" }, "promotionId": { "type": [ "string", "null" ], "format": "uuid" }, "quoteInvoice": { "anyOf": [ { "$ref": "#/components/schemas/orderInvoiceSchema" }, { "type": "null" } ] }, "shippingAddress": { "$ref": "#/components/schemas/orderAddressSchema" }, "source": { "type": [ "string", "null" ] }, "templateId": { "type": [ "string", "null" ], "format": "uuid" }, "vatInclusive": { "type": "boolean" }, "vatRegime": { "$ref": "#/components/schemas/orderVatRegimeSchema" } }, "required": [ "canceledAt", "channel", "completedAt", "confirmedAt", "createdAt", "currency", "customer", "deliveryMethod", "formattedNumber", "id", "invoicingPolicy", "number", "quoteId", "quoteNumber", "requestedDeliveryDate", "status", "subtotalExcl", "totalIncl", "updatedAt", "vatTotal", "buyerEmail", "buyerName", "buyerPhone", "cancelReason", "createdBy", "customerContactId", "customerNote", "discountDescription", "discountKind", "discountValue", "externalId", "fulfillmentStatus", "invoices", "lines", "notes", "paymentRequired", "paymentStatus", "promotionId", "quoteInvoice", "shippingAddress", "source", "templateId", "vatInclusive", "vatRegime" ] }, "orderStatusSchema": { "type": "string", "enum": [ "draft", "confirmed", "on_hold", "completed", "canceled" ] }, "orderVatRegimeSchema": { "type": "string", "enum": [ "standard", "reverse_charge", "exempt", "kor", "margin_scheme" ] }, "organizationIdSchema": { "type": "string", "format": "uuid" } } ``` # Get a sales order. (/docs/openapi/getOrder) Return one sales order with its lines, fulfillment and payment progress. ## GET /v1/organizations/{organizationId}/orders/{orderId} ```json { "operationId": "getOrder", "summary": "Get a sales order.", "description": "Return one sales order with its lines, fulfillment and payment progress.", "tags": [ "Orders" ], "parameters": [ { "in": "path", "name": "orderId", "required": true, "schema": { "type": "string", "format": "uuid" }, "example": "123e4567-e89b-12d3-a456-426614174000" }, { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "responses": { "200": { "description": "Returns the order.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/orderSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "isoDateSchema": { "type": "string", "format": "date" }, "orderAddressSchema": { "type": "object", "properties": { "city": { "type": [ "string", "null" ] }, "country": { "type": [ "string", "null" ] }, "line1": { "type": [ "string", "null" ] }, "line2": { "type": [ "string", "null" ] }, "name": { "type": [ "string", "null" ] }, "phone": { "type": [ "string", "null" ] }, "postalCode": { "type": [ "string", "null" ] } }, "required": [ "city", "country", "line1", "line2", "name", "phone", "postalCode" ] }, "orderChannelSchema": { "type": "string", "enum": [ "manual", "quote", "inbox", "api", "integration" ] }, "orderCustomerSchema": { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "name": { "type": "string" } }, "required": [ "id", "name" ] }, "orderDeliveryMethodSchema": { "type": "string", "enum": [ "pickup", "own_delivery", "carrier", "digital", "none" ] }, "orderDiscountKindSchema": { "type": "string", "enum": [ "none", "percentage", "fixed" ] }, "orderFulfillmentStatusSchema": { "type": "string", "enum": [ "unfulfilled", "partial", "fulfilled" ] }, "orderInvoiceSchema": { "type": "object", "properties": { "amountDueCents": { "type": "number" }, "amountPaidCents": { "type": "number" }, "createdAt": { "type": "string" }, "displayNumber": { "type": [ "string", "null" ] }, "id": { "type": "string", "format": "uuid" }, "kind": { "type": "string" }, "status": { "type": "string" }, "totalIncl": { "type": "number" } }, "required": [ "amountDueCents", "amountPaidCents", "createdAt", "displayNumber", "id", "kind", "status", "totalIncl" ] }, "orderInvoicingPolicySchema": { "type": "string", "enum": [ "before_fulfillment", "on_fulfillment", "manual" ] }, "orderLineSchema": { "type": "object", "properties": { "allocated": { "type": "number" }, "backordered": { "type": "number" }, "description": { "type": [ "string", "null" ] }, "discountKind": { "$ref": "#/components/schemas/orderDiscountKindSchema" }, "discountValue": { "type": "number" }, "fulfilled": { "type": "number" }, "id": { "type": "string", "format": "uuid" }, "invoiced": { "type": "number" }, "lineType": { "$ref": "#/components/schemas/orderLineTypeSchema" }, "position": { "type": "number" }, "productId": { "type": [ "string", "null" ], "format": "uuid" }, "productType": { "type": [ "string", "null" ] }, "promotionId": { "type": [ "string", "null" ], "format": "uuid" }, "quantity": { "type": "number" }, "quantityCanceled": { "type": "number" }, "returned": { "type": "number" }, "sku": { "type": [ "string", "null" ] }, "title": { "type": "string" }, "trackInventory": { "type": "boolean" }, "unit": { "type": [ "string", "null" ] }, "unitPrice": { "type": "number" }, "variantId": { "type": [ "string", "null" ], "format": "uuid" }, "vatRate": { "type": "number" } }, "required": [ "allocated", "backordered", "description", "discountKind", "discountValue", "fulfilled", "id", "invoiced", "lineType", "position", "productId", "productType", "promotionId", "quantity", "quantityCanceled", "returned", "sku", "title", "trackInventory", "unit", "unitPrice", "variantId", "vatRate" ] }, "orderLineTypeSchema": { "type": "string", "enum": [ "product", "custom", "shipping" ] }, "orderPaymentStatusSchema": { "type": "string", "enum": [ "none", "unpaid", "partial", "paid" ] }, "orderSchema": { "type": "object", "properties": { "canceledAt": { "type": [ "string", "null" ] }, "channel": { "$ref": "#/components/schemas/orderChannelSchema" }, "completedAt": { "type": [ "string", "null" ] }, "confirmedAt": { "type": [ "string", "null" ] }, "createdAt": { "type": "string" }, "currency": { "type": "string" }, "customer": { "$ref": "#/components/schemas/orderCustomerSchema" }, "deliveryMethod": { "$ref": "#/components/schemas/orderDeliveryMethodSchema" }, "formattedNumber": { "type": "string" }, "id": { "type": "string", "format": "uuid" }, "invoicingPolicy": { "$ref": "#/components/schemas/orderInvoicingPolicySchema" }, "number": { "type": "integer" }, "quoteId": { "type": [ "string", "null" ], "format": "uuid" }, "quoteNumber": { "type": [ "integer", "null" ] }, "requestedDeliveryDate": { "anyOf": [ { "$ref": "#/components/schemas/isoDateSchema" }, { "type": "null" } ] }, "status": { "$ref": "#/components/schemas/orderStatusSchema" }, "subtotalExcl": { "type": "number" }, "totalIncl": { "type": "number" }, "updatedAt": { "type": "string" }, "vatTotal": { "type": "number" }, "buyerEmail": { "type": [ "string", "null" ] }, "buyerName": { "type": [ "string", "null" ] }, "buyerPhone": { "type": [ "string", "null" ] }, "cancelReason": { "type": [ "string", "null" ] }, "createdBy": { "type": [ "string", "null" ], "format": "uuid" }, "customerContactId": { "type": [ "string", "null" ], "format": "uuid" }, "customerNote": { "type": [ "string", "null" ] }, "discountDescription": { "type": [ "string", "null" ] }, "discountKind": { "$ref": "#/components/schemas/orderDiscountKindSchema" }, "discountValue": { "type": "number" }, "externalId": { "type": [ "string", "null" ] }, "fulfillmentStatus": { "$ref": "#/components/schemas/orderFulfillmentStatusSchema" }, "invoices": { "type": "array", "items": { "$ref": "#/components/schemas/orderInvoiceSchema" } }, "lines": { "type": "array", "items": { "$ref": "#/components/schemas/orderLineSchema" } }, "notes": { "type": [ "string", "null" ] }, "paymentRequired": { "type": "boolean" }, "paymentStatus": { "$ref": "#/components/schemas/orderPaymentStatusSchema" }, "promotionId": { "type": [ "string", "null" ], "format": "uuid" }, "quoteInvoice": { "anyOf": [ { "$ref": "#/components/schemas/orderInvoiceSchema" }, { "type": "null" } ] }, "shippingAddress": { "$ref": "#/components/schemas/orderAddressSchema" }, "source": { "type": [ "string", "null" ] }, "templateId": { "type": [ "string", "null" ], "format": "uuid" }, "vatInclusive": { "type": "boolean" }, "vatRegime": { "$ref": "#/components/schemas/orderVatRegimeSchema" } }, "required": [ "canceledAt", "channel", "completedAt", "confirmedAt", "createdAt", "currency", "customer", "deliveryMethod", "formattedNumber", "id", "invoicingPolicy", "number", "quoteId", "quoteNumber", "requestedDeliveryDate", "status", "subtotalExcl", "totalIncl", "updatedAt", "vatTotal", "buyerEmail", "buyerName", "buyerPhone", "cancelReason", "createdBy", "customerContactId", "customerNote", "discountDescription", "discountKind", "discountValue", "externalId", "fulfillmentStatus", "invoices", "lines", "notes", "paymentRequired", "paymentStatus", "promotionId", "quoteInvoice", "shippingAddress", "source", "templateId", "vatInclusive", "vatRegime" ] }, "orderStatusSchema": { "type": "string", "enum": [ "draft", "confirmed", "on_hold", "completed", "canceled" ] }, "orderVatRegimeSchema": { "type": "string", "enum": [ "standard", "reverse_charge", "exempt", "kor", "margin_scheme" ] }, "organizationIdSchema": { "type": "string", "format": "uuid" } } ``` # Cancel a sales order. (/docs/openapi/cancelOrder) Cancel an order and release its reserved stock. A partly shipped order keeps what shipped and cancels the remainder. Returns 409 `APPROVAL_REQUIRED` with an `approvalId` when an approval policy wants a decision first. ## POST /v1/organizations/{organizationId}/orders/{orderId}/cancel ```json { "operationId": "cancelOrder", "summary": "Cancel a sales order.", "description": "Cancel an order and release its reserved stock. A partly shipped order keeps what shipped and cancels the remainder. Returns 409 `APPROVAL_REQUIRED` with an `approvalId` when an approval policy wants a decision first.", "tags": [ "Orders" ], "parameters": [ { "in": "path", "name": "orderId", "required": true, "schema": { "type": "string", "format": "uuid" }, "example": "123e4567-e89b-12d3-a456-426614174000" }, { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/orderCancelInputSchema" } } }, "description": "An optional reason." }, "responses": { "200": { "description": "Returns the order after cancelling.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/orderSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "isoDateSchema": { "type": "string", "format": "date" }, "orderAddressSchema": { "type": "object", "properties": { "city": { "type": [ "string", "null" ] }, "country": { "type": [ "string", "null" ] }, "line1": { "type": [ "string", "null" ] }, "line2": { "type": [ "string", "null" ] }, "name": { "type": [ "string", "null" ] }, "phone": { "type": [ "string", "null" ] }, "postalCode": { "type": [ "string", "null" ] } }, "required": [ "city", "country", "line1", "line2", "name", "phone", "postalCode" ] }, "orderCancelInputSchema": { "type": "object", "properties": { "reason": { "type": [ "string", "null" ], "maxLength": 1000 } }, "required": [ "reason" ] }, "orderChannelSchema": { "type": "string", "enum": [ "manual", "quote", "inbox", "api", "integration" ] }, "orderCustomerSchema": { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "name": { "type": "string" } }, "required": [ "id", "name" ] }, "orderDeliveryMethodSchema": { "type": "string", "enum": [ "pickup", "own_delivery", "carrier", "digital", "none" ] }, "orderDiscountKindSchema": { "type": "string", "enum": [ "none", "percentage", "fixed" ] }, "orderFulfillmentStatusSchema": { "type": "string", "enum": [ "unfulfilled", "partial", "fulfilled" ] }, "orderInvoiceSchema": { "type": "object", "properties": { "amountDueCents": { "type": "number" }, "amountPaidCents": { "type": "number" }, "createdAt": { "type": "string" }, "displayNumber": { "type": [ "string", "null" ] }, "id": { "type": "string", "format": "uuid" }, "kind": { "type": "string" }, "status": { "type": "string" }, "totalIncl": { "type": "number" } }, "required": [ "amountDueCents", "amountPaidCents", "createdAt", "displayNumber", "id", "kind", "status", "totalIncl" ] }, "orderInvoicingPolicySchema": { "type": "string", "enum": [ "before_fulfillment", "on_fulfillment", "manual" ] }, "orderLineSchema": { "type": "object", "properties": { "allocated": { "type": "number" }, "backordered": { "type": "number" }, "description": { "type": [ "string", "null" ] }, "discountKind": { "$ref": "#/components/schemas/orderDiscountKindSchema" }, "discountValue": { "type": "number" }, "fulfilled": { "type": "number" }, "id": { "type": "string", "format": "uuid" }, "invoiced": { "type": "number" }, "lineType": { "$ref": "#/components/schemas/orderLineTypeSchema" }, "position": { "type": "number" }, "productId": { "type": [ "string", "null" ], "format": "uuid" }, "productType": { "type": [ "string", "null" ] }, "promotionId": { "type": [ "string", "null" ], "format": "uuid" }, "quantity": { "type": "number" }, "quantityCanceled": { "type": "number" }, "returned": { "type": "number" }, "sku": { "type": [ "string", "null" ] }, "title": { "type": "string" }, "trackInventory": { "type": "boolean" }, "unit": { "type": [ "string", "null" ] }, "unitPrice": { "type": "number" }, "variantId": { "type": [ "string", "null" ], "format": "uuid" }, "vatRate": { "type": "number" } }, "required": [ "allocated", "backordered", "description", "discountKind", "discountValue", "fulfilled", "id", "invoiced", "lineType", "position", "productId", "productType", "promotionId", "quantity", "quantityCanceled", "returned", "sku", "title", "trackInventory", "unit", "unitPrice", "variantId", "vatRate" ] }, "orderLineTypeSchema": { "type": "string", "enum": [ "product", "custom", "shipping" ] }, "orderPaymentStatusSchema": { "type": "string", "enum": [ "none", "unpaid", "partial", "paid" ] }, "orderSchema": { "type": "object", "properties": { "canceledAt": { "type": [ "string", "null" ] }, "channel": { "$ref": "#/components/schemas/orderChannelSchema" }, "completedAt": { "type": [ "string", "null" ] }, "confirmedAt": { "type": [ "string", "null" ] }, "createdAt": { "type": "string" }, "currency": { "type": "string" }, "customer": { "$ref": "#/components/schemas/orderCustomerSchema" }, "deliveryMethod": { "$ref": "#/components/schemas/orderDeliveryMethodSchema" }, "formattedNumber": { "type": "string" }, "id": { "type": "string", "format": "uuid" }, "invoicingPolicy": { "$ref": "#/components/schemas/orderInvoicingPolicySchema" }, "number": { "type": "integer" }, "quoteId": { "type": [ "string", "null" ], "format": "uuid" }, "quoteNumber": { "type": [ "integer", "null" ] }, "requestedDeliveryDate": { "anyOf": [ { "$ref": "#/components/schemas/isoDateSchema" }, { "type": "null" } ] }, "status": { "$ref": "#/components/schemas/orderStatusSchema" }, "subtotalExcl": { "type": "number" }, "totalIncl": { "type": "number" }, "updatedAt": { "type": "string" }, "vatTotal": { "type": "number" }, "buyerEmail": { "type": [ "string", "null" ] }, "buyerName": { "type": [ "string", "null" ] }, "buyerPhone": { "type": [ "string", "null" ] }, "cancelReason": { "type": [ "string", "null" ] }, "createdBy": { "type": [ "string", "null" ], "format": "uuid" }, "customerContactId": { "type": [ "string", "null" ], "format": "uuid" }, "customerNote": { "type": [ "string", "null" ] }, "discountDescription": { "type": [ "string", "null" ] }, "discountKind": { "$ref": "#/components/schemas/orderDiscountKindSchema" }, "discountValue": { "type": "number" }, "externalId": { "type": [ "string", "null" ] }, "fulfillmentStatus": { "$ref": "#/components/schemas/orderFulfillmentStatusSchema" }, "invoices": { "type": "array", "items": { "$ref": "#/components/schemas/orderInvoiceSchema" } }, "lines": { "type": "array", "items": { "$ref": "#/components/schemas/orderLineSchema" } }, "notes": { "type": [ "string", "null" ] }, "paymentRequired": { "type": "boolean" }, "paymentStatus": { "$ref": "#/components/schemas/orderPaymentStatusSchema" }, "promotionId": { "type": [ "string", "null" ], "format": "uuid" }, "quoteInvoice": { "anyOf": [ { "$ref": "#/components/schemas/orderInvoiceSchema" }, { "type": "null" } ] }, "shippingAddress": { "$ref": "#/components/schemas/orderAddressSchema" }, "source": { "type": [ "string", "null" ] }, "templateId": { "type": [ "string", "null" ], "format": "uuid" }, "vatInclusive": { "type": "boolean" }, "vatRegime": { "$ref": "#/components/schemas/orderVatRegimeSchema" } }, "required": [ "canceledAt", "channel", "completedAt", "confirmedAt", "createdAt", "currency", "customer", "deliveryMethod", "formattedNumber", "id", "invoicingPolicy", "number", "quoteId", "quoteNumber", "requestedDeliveryDate", "status", "subtotalExcl", "totalIncl", "updatedAt", "vatTotal", "buyerEmail", "buyerName", "buyerPhone", "cancelReason", "createdBy", "customerContactId", "customerNote", "discountDescription", "discountKind", "discountValue", "externalId", "fulfillmentStatus", "invoices", "lines", "notes", "paymentRequired", "paymentStatus", "promotionId", "quoteInvoice", "shippingAddress", "source", "templateId", "vatInclusive", "vatRegime" ] }, "orderStatusSchema": { "type": "string", "enum": [ "draft", "confirmed", "on_hold", "completed", "canceled" ] }, "orderVatRegimeSchema": { "type": "string", "enum": [ "standard", "reverse_charge", "exempt", "kor", "margin_scheme" ] }, "organizationIdSchema": { "type": "string", "format": "uuid" } } ``` # Confirm a sales order. (/docs/openapi/confirmOrder) Confirm a draft order and reserve stock. Lines that cannot be reserved become backorders. Returns 409 `APPROVAL_REQUIRED` with an `approvalId` when an approval policy wants a decision first. ## POST /v1/organizations/{organizationId}/orders/{orderId}/confirm ```json { "operationId": "confirmOrder", "summary": "Confirm a sales order.", "description": "Confirm a draft order and reserve stock. Lines that cannot be reserved become backorders. Returns 409 `APPROVAL_REQUIRED` with an `approvalId` when an approval policy wants a decision first.", "tags": [ "Orders" ], "parameters": [ { "in": "path", "name": "orderId", "required": true, "schema": { "type": "string", "format": "uuid" }, "example": "123e4567-e89b-12d3-a456-426614174000" }, { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "responses": { "200": { "description": "Returns the confirmed order and what could be reserved.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/orderConfirmResponseSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "isoDateSchema": { "type": "string", "format": "date" }, "orderAddressSchema": { "type": "object", "properties": { "city": { "type": [ "string", "null" ] }, "country": { "type": [ "string", "null" ] }, "line1": { "type": [ "string", "null" ] }, "line2": { "type": [ "string", "null" ] }, "name": { "type": [ "string", "null" ] }, "phone": { "type": [ "string", "null" ] }, "postalCode": { "type": [ "string", "null" ] } }, "required": [ "city", "country", "line1", "line2", "name", "phone", "postalCode" ] }, "orderChannelSchema": { "type": "string", "enum": [ "manual", "quote", "inbox", "api", "integration" ] }, "orderConfirmResponseSchema": { "type": "object", "properties": { "allocation": { "$ref": "#/components/schemas/orderConfirmResultSchema" }, "order": { "$ref": "#/components/schemas/orderSchema" } }, "required": [ "allocation", "order" ] }, "orderConfirmResultSchema": { "type": "object", "properties": { "backorderedLines": { "type": "integer" }, "shortLines": { "type": "integer" } }, "required": [ "backorderedLines", "shortLines" ] }, "orderCustomerSchema": { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "name": { "type": "string" } }, "required": [ "id", "name" ] }, "orderDeliveryMethodSchema": { "type": "string", "enum": [ "pickup", "own_delivery", "carrier", "digital", "none" ] }, "orderDiscountKindSchema": { "type": "string", "enum": [ "none", "percentage", "fixed" ] }, "orderFulfillmentStatusSchema": { "type": "string", "enum": [ "unfulfilled", "partial", "fulfilled" ] }, "orderInvoiceSchema": { "type": "object", "properties": { "amountDueCents": { "type": "number" }, "amountPaidCents": { "type": "number" }, "createdAt": { "type": "string" }, "displayNumber": { "type": [ "string", "null" ] }, "id": { "type": "string", "format": "uuid" }, "kind": { "type": "string" }, "status": { "type": "string" }, "totalIncl": { "type": "number" } }, "required": [ "amountDueCents", "amountPaidCents", "createdAt", "displayNumber", "id", "kind", "status", "totalIncl" ] }, "orderInvoicingPolicySchema": { "type": "string", "enum": [ "before_fulfillment", "on_fulfillment", "manual" ] }, "orderLineSchema": { "type": "object", "properties": { "allocated": { "type": "number" }, "backordered": { "type": "number" }, "description": { "type": [ "string", "null" ] }, "discountKind": { "$ref": "#/components/schemas/orderDiscountKindSchema" }, "discountValue": { "type": "number" }, "fulfilled": { "type": "number" }, "id": { "type": "string", "format": "uuid" }, "invoiced": { "type": "number" }, "lineType": { "$ref": "#/components/schemas/orderLineTypeSchema" }, "position": { "type": "number" }, "productId": { "type": [ "string", "null" ], "format": "uuid" }, "productType": { "type": [ "string", "null" ] }, "promotionId": { "type": [ "string", "null" ], "format": "uuid" }, "quantity": { "type": "number" }, "quantityCanceled": { "type": "number" }, "returned": { "type": "number" }, "sku": { "type": [ "string", "null" ] }, "title": { "type": "string" }, "trackInventory": { "type": "boolean" }, "unit": { "type": [ "string", "null" ] }, "unitPrice": { "type": "number" }, "variantId": { "type": [ "string", "null" ], "format": "uuid" }, "vatRate": { "type": "number" } }, "required": [ "allocated", "backordered", "description", "discountKind", "discountValue", "fulfilled", "id", "invoiced", "lineType", "position", "productId", "productType", "promotionId", "quantity", "quantityCanceled", "returned", "sku", "title", "trackInventory", "unit", "unitPrice", "variantId", "vatRate" ] }, "orderLineTypeSchema": { "type": "string", "enum": [ "product", "custom", "shipping" ] }, "orderPaymentStatusSchema": { "type": "string", "enum": [ "none", "unpaid", "partial", "paid" ] }, "orderSchema": { "type": "object", "properties": { "canceledAt": { "type": [ "string", "null" ] }, "channel": { "$ref": "#/components/schemas/orderChannelSchema" }, "completedAt": { "type": [ "string", "null" ] }, "confirmedAt": { "type": [ "string", "null" ] }, "createdAt": { "type": "string" }, "currency": { "type": "string" }, "customer": { "$ref": "#/components/schemas/orderCustomerSchema" }, "deliveryMethod": { "$ref": "#/components/schemas/orderDeliveryMethodSchema" }, "formattedNumber": { "type": "string" }, "id": { "type": "string", "format": "uuid" }, "invoicingPolicy": { "$ref": "#/components/schemas/orderInvoicingPolicySchema" }, "number": { "type": "integer" }, "quoteId": { "type": [ "string", "null" ], "format": "uuid" }, "quoteNumber": { "type": [ "integer", "null" ] }, "requestedDeliveryDate": { "anyOf": [ { "$ref": "#/components/schemas/isoDateSchema" }, { "type": "null" } ] }, "status": { "$ref": "#/components/schemas/orderStatusSchema" }, "subtotalExcl": { "type": "number" }, "totalIncl": { "type": "number" }, "updatedAt": { "type": "string" }, "vatTotal": { "type": "number" }, "buyerEmail": { "type": [ "string", "null" ] }, "buyerName": { "type": [ "string", "null" ] }, "buyerPhone": { "type": [ "string", "null" ] }, "cancelReason": { "type": [ "string", "null" ] }, "createdBy": { "type": [ "string", "null" ], "format": "uuid" }, "customerContactId": { "type": [ "string", "null" ], "format": "uuid" }, "customerNote": { "type": [ "string", "null" ] }, "discountDescription": { "type": [ "string", "null" ] }, "discountKind": { "$ref": "#/components/schemas/orderDiscountKindSchema" }, "discountValue": { "type": "number" }, "externalId": { "type": [ "string", "null" ] }, "fulfillmentStatus": { "$ref": "#/components/schemas/orderFulfillmentStatusSchema" }, "invoices": { "type": "array", "items": { "$ref": "#/components/schemas/orderInvoiceSchema" } }, "lines": { "type": "array", "items": { "$ref": "#/components/schemas/orderLineSchema" } }, "notes": { "type": [ "string", "null" ] }, "paymentRequired": { "type": "boolean" }, "paymentStatus": { "$ref": "#/components/schemas/orderPaymentStatusSchema" }, "promotionId": { "type": [ "string", "null" ], "format": "uuid" }, "quoteInvoice": { "anyOf": [ { "$ref": "#/components/schemas/orderInvoiceSchema" }, { "type": "null" } ] }, "shippingAddress": { "$ref": "#/components/schemas/orderAddressSchema" }, "source": { "type": [ "string", "null" ] }, "templateId": { "type": [ "string", "null" ], "format": "uuid" }, "vatInclusive": { "type": "boolean" }, "vatRegime": { "$ref": "#/components/schemas/orderVatRegimeSchema" } }, "required": [ "canceledAt", "channel", "completedAt", "confirmedAt", "createdAt", "currency", "customer", "deliveryMethod", "formattedNumber", "id", "invoicingPolicy", "number", "quoteId", "quoteNumber", "requestedDeliveryDate", "status", "subtotalExcl", "totalIncl", "updatedAt", "vatTotal", "buyerEmail", "buyerName", "buyerPhone", "cancelReason", "createdBy", "customerContactId", "customerNote", "discountDescription", "discountKind", "discountValue", "externalId", "fulfillmentStatus", "invoices", "lines", "notes", "paymentRequired", "paymentStatus", "promotionId", "quoteInvoice", "shippingAddress", "source", "templateId", "vatInclusive", "vatRegime" ] }, "orderStatusSchema": { "type": "string", "enum": [ "draft", "confirmed", "on_hold", "completed", "canceled" ] }, "orderVatRegimeSchema": { "type": "string", "enum": [ "standard", "reverse_charge", "exempt", "kor", "margin_scheme" ] }, "organizationIdSchema": { "type": "string", "format": "uuid" } } ``` # Read an order lifecycle. (/docs/openapi/getOrderLifecycle) The order's lifecycle: the pinned workflow version, the current stage, the stage history, and what the lifecycle waits on now. `lifecycle` is null when no lifecycle run started for the order. ## GET /v1/organizations/{organizationId}/orders/{orderId}/lifecycle ```json { "operationId": "getOrderLifecycle", "summary": "Read an order lifecycle.", "description": "The order's lifecycle: the pinned workflow version, the current stage, the stage history, and what the lifecycle waits on now. `lifecycle` is null when no lifecycle run started for the order.", "tags": [ "Orders" ], "parameters": [ { "in": "path", "name": "orderId", "required": true, "schema": { "type": "string", "format": "uuid" }, "example": "123e4567-e89b-12d3-a456-426614174000" }, { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "responses": { "200": { "description": "Returns the order's lifecycle.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/recordLifecycleSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "dateTimeSchema": { "type": "string" }, "organizationIdSchema": { "type": "string", "format": "uuid" }, "recordLifecycleSchema": { "type": "object", "properties": { "definition": { "type": [ "object", "null" ], "properties": { "id": { "type": "string", "format": "uuid" }, "key": { "type": "string" }, "name": { "type": "string" }, "source": { "type": "string", "enum": [ "organization", "record", "system", "template" ] }, "version": { "type": "integer", "exclusiveMinimum": 0 } }, "required": [ "id", "key", "name", "source", "version" ] }, "events": { "type": "array", "items": { "$ref": "#/components/schemas/workflowLifecycleEventSchema" } }, "lifecycle": { "anyOf": [ { "$ref": "#/components/schemas/workflowLifecycleSchema" }, { "type": "null" } ] }, "waiting": { "type": [ "object", "null" ], "properties": { "at": { "anyOf": [ { "$ref": "#/components/schemas/dateTimeSchema" }, { "type": "null" } ] }, "events": { "type": "array", "items": { "type": "string" } }, "kind": { "type": "string", "enum": [ "approval", "delay", "event", "manual" ] }, "nextStageKey": { "type": [ "string", "null" ] }, "nextStageLabel": { "type": [ "string", "null" ] } }, "required": [ "at", "events", "kind", "nextStageKey", "nextStageLabel" ] } }, "required": [ "definition", "events", "lifecycle", "waiting" ] }, "workflowLifecycleEventSchema": { "type": "object", "properties": { "actorCustomerContactId": { "type": [ "string", "null" ], "format": "uuid" }, "actorUserId": { "type": [ "string", "null" ], "format": "uuid" }, "clearedAt": { "anyOf": [ { "$ref": "#/components/schemas/dateTimeSchema" }, { "type": "null" } ] }, "createdAt": { "$ref": "#/components/schemas/dateTimeSchema" }, "display": { "type": "string", "enum": [ "main", "sub", "flag", "hidden" ] }, "id": { "type": "string", "format": "uuid" }, "label": { "type": "string", "minLength": 1 }, "lifecycleId": { "type": "string", "format": "uuid" }, "organizationId": { "type": "string", "format": "uuid" }, "outcome": { "type": "string", "enum": [ "progress", "success", "failure", "neutral" ] }, "reachedAt": { "$ref": "#/components/schemas/dateTimeSchema" }, "runStepId": { "type": [ "string", "null" ], "format": "uuid" }, "sourceRunId": { "type": [ "string", "null" ], "format": "uuid" }, "stageKey": { "type": "string", "minLength": 1 } }, "required": [ "actorCustomerContactId", "actorUserId", "clearedAt", "createdAt", "display", "id", "label", "lifecycleId", "organizationId", "outcome", "reachedAt", "runStepId", "sourceRunId", "stageKey" ] }, "workflowLifecycleSchema": { "type": "object", "properties": { "createdAt": { "$ref": "#/components/schemas/dateTimeSchema" }, "currentStageKey": { "type": [ "string", "null" ] }, "definitionId": { "type": "string", "format": "uuid" }, "definitionVersion": { "type": "integer", "exclusiveMinimum": 0 }, "endedAt": { "anyOf": [ { "$ref": "#/components/schemas/dateTimeSchema" }, { "type": "null" } ] }, "id": { "type": "string", "format": "uuid" }, "organizationId": { "type": "string", "format": "uuid" }, "pauseReason": { "type": [ "string", "null" ] }, "pausedAt": { "anyOf": [ { "$ref": "#/components/schemas/dateTimeSchema" }, { "type": "null" } ] }, "pausedBy": { "type": [ "string", "null" ], "format": "uuid" }, "pausedUntil": { "anyOf": [ { "$ref": "#/components/schemas/dateTimeSchema" }, { "type": "null" } ] }, "runId": { "type": [ "string", "null" ], "format": "uuid" }, "startedAt": { "$ref": "#/components/schemas/dateTimeSchema" }, "subjectId": { "type": "string", "format": "uuid" }, "subjectType": { "$ref": "#/components/schemas/workflowSubjectTypeSchema" }, "updatedAt": { "$ref": "#/components/schemas/dateTimeSchema" } }, "required": [ "createdAt", "currentStageKey", "definitionId", "definitionVersion", "endedAt", "id", "organizationId", "pauseReason", "pausedAt", "pausedBy", "pausedUntil", "runId", "startedAt", "subjectId", "subjectType", "updatedAt" ] }, "workflowSubjectTypeSchema": { "type": "string", "enum": [ "invoice", "order", "purchase_order", "quote", "shipment", "task" ] } } ``` # Advance an order lifecycle stage. (/docs/openapi/advanceOrderLifecycle) Moves the order to a manual milestone of its lifecycle. Only stages the lifecycle marks as manually advanceable are accepted. ## POST /v1/organizations/{organizationId}/orders/{orderId}/lifecycle/advance ```json { "operationId": "advanceOrderLifecycle", "summary": "Advance an order lifecycle stage.", "description": "Moves the order to a manual milestone of its lifecycle. Only stages the lifecycle marks as manually advanceable are accepted.", "tags": [ "Orders" ], "parameters": [ { "in": "path", "name": "orderId", "required": true, "schema": { "type": "string", "format": "uuid" }, "example": "123e4567-e89b-12d3-a456-426614174000" }, { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/lifecycleAdvanceInputSchema" } } }, "description": "The stage key to move to.", "required": true }, "responses": { "200": { "description": "The stage was recorded.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/publicApiSuccessResponseSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "lifecycleAdvanceInputSchema": { "type": "object", "properties": { "stageKey": { "type": "string", "minLength": 1, "maxLength": 80 } }, "required": [ "stageKey" ] }, "organizationIdSchema": { "type": "string", "format": "uuid" }, "publicApiSuccessResponseSchema": { "type": "object", "properties": { "success": { "type": "boolean", "enum": [ true ] } }, "required": [ "success" ] } } ``` # List the organizations available to the authenticated user. (/docs/openapi/listOrganizations) Return the organizations the authenticated user can access, ordered by recent usage. ## GET /v1/organizations ```json { "operationId": "listOrganizations", "summary": "List the organizations available to the authenticated user.", "description": "Return the organizations the authenticated user can access, ordered by recent usage.", "tags": [ "Organizations" ], "parameters": [], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "responses": { "200": { "description": "Returns the organizations available to the authenticated user.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/organizationsListResponseSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "BrandId": { "type": "string", "enum": [ "centrakit", "mechup", "stabledock", "caresly", "propdock", "marryduck", "signaldock", "spacelio", "legaldock" ] }, "brandingColorSchema": { "type": [ "string", "null" ], "pattern": "^#(?:[0-9a-f]{3}|[0-9a-f]{6})$" }, "lifecycleKeySchema": { "type": [ "string", "null" ], "minLength": 1, "maxLength": 120, "pattern": "^[a-z0-9][a-z0-9-]*$" }, "listPaginationSchema": { "type": "object", "properties": { "hasMore": { "type": "boolean" }, "limit": { "type": "integer", "minimum": 1, "maximum": 100 }, "offset": { "type": "integer", "minimum": 0 }, "total": { "type": "integer", "minimum": 0 }, "totalIsEstimate": { "type": "boolean" } }, "required": [ "hasMore", "limit", "offset", "total" ] }, "nullablePhoneInputSchema": { "type": [ "string", "null" ], "maxLength": 20 }, "nullableShortTextSchema": { "type": [ "string", "null" ], "maxLength": 200 }, "organizationAiInferenceRegionSchema": { "type": "string", "enum": [ "eu", "us", "global" ] }, "organizationBrandImageIdSchema": { "type": "string", "format": "uuid" }, "organizationBrandImageSchema": { "type": "object", "properties": { "height": { "type": "integer", "exclusiveMinimum": 0 }, "id": { "$ref": "#/components/schemas/organizationBrandImageIdSchema" }, "url": { "type": "string", "format": "uri" }, "width": { "type": "integer", "exclusiveMinimum": 0 } }, "required": [ "height", "id", "url", "width" ] }, "organizationBrandingColorsSchema": { "type": "object", "properties": { "primary": { "$ref": "#/components/schemas/brandingColorSchema" }, "secondary": { "$ref": "#/components/schemas/brandingColorSchema" } }, "required": [ "primary", "secondary" ] }, "organizationBrandingSchema": { "type": "object", "properties": { "colors": { "$ref": "#/components/schemas/organizationBrandingColorsSchema" }, "images": { "type": "array", "items": { "$ref": "#/components/schemas/organizationBrandImageSchema" }, "maxItems": 12 }, "logoUrl": { "type": [ "string", "null" ], "format": "uri" } }, "required": [ "colors", "images", "logoUrl" ] }, "organizationIdSchema": { "type": "string", "format": "uuid" }, "organizationLifecycleDefaultsSchema": { "type": "object", "properties": { "invoice": { "$ref": "#/components/schemas/lifecycleKeySchema" }, "order": { "$ref": "#/components/schemas/lifecycleKeySchema" }, "purchase_order": { "$ref": "#/components/schemas/lifecycleKeySchema" }, "quote": { "$ref": "#/components/schemas/lifecycleKeySchema" }, "shipment": { "$ref": "#/components/schemas/lifecycleKeySchema" }, "task": { "$ref": "#/components/schemas/lifecycleKeySchema" } }, "required": [ "invoice", "order", "purchase_order", "quote", "shipment", "task" ] }, "organizationOperatingDefaultsSchema": { "type": "object" }, "organizationSchema": { "type": "object", "properties": { "addressCity": { "$ref": "#/components/schemas/nullableShortTextSchema" }, "addressCountry": { "$ref": "#/components/schemas/nullableShortTextSchema" }, "addressLine1": { "$ref": "#/components/schemas/nullableShortTextSchema" }, "addressLine2": { "$ref": "#/components/schemas/nullableShortTextSchema" }, "addressPostalCode": { "$ref": "#/components/schemas/nullableShortTextSchema" }, "addressState": { "$ref": "#/components/schemas/nullableShortTextSchema" }, "aiInferenceRegion": { "$ref": "#/components/schemas/organizationAiInferenceRegionSchema" }, "brand": { "$ref": "#/components/schemas/BrandId" }, "branding": { "$ref": "#/components/schemas/organizationBrandingSchema" }, "btw": { "$ref": "#/components/schemas/nullableShortTextSchema" }, "createdAt": { "type": "string", "format": "date-time" }, "disabledAt": { "type": [ "string", "null" ], "format": "date-time" }, "iban": { "$ref": "#/components/schemas/nullableShortTextSchema" }, "id": { "$ref": "#/components/schemas/organizationIdSchema" }, "kvk": { "$ref": "#/components/schemas/nullableShortTextSchema" }, "lifecycleDefaults": { "$ref": "#/components/schemas/organizationLifecycleDefaultsSchema" }, "name": { "type": "string", "minLength": 1 }, "operatingDefaults": { "$ref": "#/components/schemas/organizationOperatingDefaultsSchema" }, "phone": { "$ref": "#/components/schemas/nullablePhoneInputSchema" }, "slug": { "$ref": "#/components/schemas/organizationSlugSchema" }, "socials": { "type": "array", "items": { "$ref": "#/components/schemas/organizationSocialLinkSchema" } }, "updatedAt": { "type": "string", "format": "date-time" }, "website": { "type": [ "string", "null" ], "format": "uri" } }, "required": [ "addressCity", "addressCountry", "addressLine1", "addressLine2", "addressPostalCode", "addressState", "aiInferenceRegion", "brand", "branding", "btw", "createdAt", "disabledAt", "iban", "id", "kvk", "lifecycleDefaults", "name", "operatingDefaults", "phone", "slug", "socials", "updatedAt", "website" ] }, "organizationSlugSchema": { "type": "string", "minLength": 2, "maxLength": 64, "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$" }, "organizationSocialLinkSchema": { "type": "object", "properties": { "label": { "type": "string", "minLength": 1, "maxLength": 50 }, "url": { "type": "string", "format": "uri" } }, "required": [ "label", "url" ] }, "organizationsListResponseSchema": { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/components/schemas/userOrganizationSchema" } }, "pagination": { "$ref": "#/components/schemas/listPaginationSchema" } }, "required": [ "data", "pagination" ] }, "userOrganizationSchema": { "type": "object", "properties": { "hasTransferOwnership": { "type": "boolean" }, "lastUsedAt": { "type": "string", "format": "date-time" }, "organization": { "$ref": "#/components/schemas/organizationSchema" }, "roleKey": { "type": "string", "minLength": 1 }, "roleName": { "type": "string", "minLength": 1 } }, "required": [ "hasTransferOwnership", "lastUsedAt", "organization", "roleKey", "roleName" ] } } ``` # Create a new organization for the authenticated user. (/docs/openapi/createOrganization) Create an organization, assign the current user as owner, and return the created organization. ## POST /v1/organizations ```json { "operationId": "createOrganization", "summary": "Create a new organization for the authenticated user.", "description": "Create an organization, assign the current user as owner, and return the created organization.", "tags": [ "Organizations" ], "parameters": [], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/organizationCreateSchema" } } }, "description": "The organization settings used to create the new organization." }, "responses": { "201": { "description": "Returns the created organization.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/organizationResponseSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "BrandId": { "type": "string", "enum": [ "centrakit", "mechup", "stabledock", "caresly", "propdock", "marryduck", "signaldock", "spacelio", "legaldock" ] }, "brandingColorSchema": { "type": [ "string", "null" ], "pattern": "^#(?:[0-9a-f]{3}|[0-9a-f]{6})$" }, "lifecycleKeySchema": { "type": [ "string", "null" ], "minLength": 1, "maxLength": 120, "pattern": "^[a-z0-9][a-z0-9-]*$" }, "nullableKvkNumberInputSchema": { "type": [ "string", "null" ], "maxLength": 40 }, "nullablePhoneInputSchema": { "type": [ "string", "null" ], "maxLength": 20 }, "nullableShortTextSchema": { "type": [ "string", "null" ], "maxLength": 200 }, "nullableVatIdInputSchema": { "type": [ "string", "null" ], "maxLength": 40 }, "organizationAiInferenceRegionSchema": { "type": "string", "enum": [ "eu", "us", "global" ] }, "organizationBrandImageIdSchema": { "type": "string", "format": "uuid" }, "organizationBrandImageSchema": { "type": "object", "properties": { "height": { "type": "integer", "exclusiveMinimum": 0 }, "id": { "$ref": "#/components/schemas/organizationBrandImageIdSchema" }, "url": { "type": "string", "format": "uri" }, "width": { "type": "integer", "exclusiveMinimum": 0 } }, "required": [ "height", "id", "url", "width" ] }, "organizationBrandingColorsSchema": { "type": "object", "properties": { "primary": { "$ref": "#/components/schemas/brandingColorSchema" }, "secondary": { "$ref": "#/components/schemas/brandingColorSchema" } }, "required": [ "primary", "secondary" ] }, "organizationBrandingSchema": { "type": "object", "properties": { "colors": { "$ref": "#/components/schemas/organizationBrandingColorsSchema" }, "images": { "type": "array", "items": { "$ref": "#/components/schemas/organizationBrandImageSchema" }, "maxItems": 12 }, "logoUrl": { "type": [ "string", "null" ], "format": "uri" } }, "required": [ "colors", "images", "logoUrl" ] }, "organizationCreateSchema": { "$ref": "#/components/schemas/organizationSettingsInputSchema" }, "organizationIdSchema": { "type": "string", "format": "uuid" }, "organizationLifecycleDefaultsSchema": { "type": "object", "properties": { "invoice": { "$ref": "#/components/schemas/lifecycleKeySchema" }, "order": { "$ref": "#/components/schemas/lifecycleKeySchema" }, "purchase_order": { "$ref": "#/components/schemas/lifecycleKeySchema" }, "quote": { "$ref": "#/components/schemas/lifecycleKeySchema" }, "shipment": { "$ref": "#/components/schemas/lifecycleKeySchema" }, "task": { "$ref": "#/components/schemas/lifecycleKeySchema" } }, "required": [ "invoice", "order", "purchase_order", "quote", "shipment", "task" ] }, "organizationOperatingDefaultsSchema": { "type": "object" }, "organizationResponseSchema": { "$ref": "#/components/schemas/organizationSchema" }, "organizationSchema": { "type": "object", "properties": { "addressCity": { "$ref": "#/components/schemas/nullableShortTextSchema" }, "addressCountry": { "$ref": "#/components/schemas/nullableShortTextSchema" }, "addressLine1": { "$ref": "#/components/schemas/nullableShortTextSchema" }, "addressLine2": { "$ref": "#/components/schemas/nullableShortTextSchema" }, "addressPostalCode": { "$ref": "#/components/schemas/nullableShortTextSchema" }, "addressState": { "$ref": "#/components/schemas/nullableShortTextSchema" }, "aiInferenceRegion": { "$ref": "#/components/schemas/organizationAiInferenceRegionSchema" }, "brand": { "$ref": "#/components/schemas/BrandId" }, "branding": { "$ref": "#/components/schemas/organizationBrandingSchema" }, "btw": { "$ref": "#/components/schemas/nullableShortTextSchema" }, "createdAt": { "type": "string", "format": "date-time" }, "disabledAt": { "type": [ "string", "null" ], "format": "date-time" }, "iban": { "$ref": "#/components/schemas/nullableShortTextSchema" }, "id": { "$ref": "#/components/schemas/organizationIdSchema" }, "kvk": { "$ref": "#/components/schemas/nullableShortTextSchema" }, "lifecycleDefaults": { "$ref": "#/components/schemas/organizationLifecycleDefaultsSchema" }, "name": { "type": "string", "minLength": 1 }, "operatingDefaults": { "$ref": "#/components/schemas/organizationOperatingDefaultsSchema" }, "phone": { "$ref": "#/components/schemas/nullablePhoneInputSchema" }, "slug": { "$ref": "#/components/schemas/organizationSlugSchema" }, "socials": { "type": "array", "items": { "$ref": "#/components/schemas/organizationSocialLinkSchema" } }, "updatedAt": { "type": "string", "format": "date-time" }, "website": { "type": [ "string", "null" ], "format": "uri" } }, "required": [ "addressCity", "addressCountry", "addressLine1", "addressLine2", "addressPostalCode", "addressState", "aiInferenceRegion", "brand", "branding", "btw", "createdAt", "disabledAt", "iban", "id", "kvk", "lifecycleDefaults", "name", "operatingDefaults", "phone", "slug", "socials", "updatedAt", "website" ] }, "organizationSettingsInputSchema": { "type": "object", "properties": { "addressCity": { "$ref": "#/components/schemas/nullableShortTextSchema" }, "addressCountry": { "$ref": "#/components/schemas/nullableShortTextSchema" }, "addressLine1": { "$ref": "#/components/schemas/nullableShortTextSchema" }, "addressLine2": { "$ref": "#/components/schemas/nullableShortTextSchema" }, "addressPostalCode": { "$ref": "#/components/schemas/nullableShortTextSchema" }, "addressState": { "$ref": "#/components/schemas/nullableShortTextSchema" }, "btw": { "$ref": "#/components/schemas/nullableVatIdInputSchema" }, "colors": { "$ref": "#/components/schemas/organizationBrandingColorsSchema" }, "iban": { "$ref": "#/components/schemas/nullableShortTextSchema" }, "kvk": { "$ref": "#/components/schemas/nullableKvkNumberInputSchema" }, "name": { "type": "string", "minLength": 2, "maxLength": 120 }, "phone": { "$ref": "#/components/schemas/nullablePhoneInputSchema" }, "slug": { "$ref": "#/components/schemas/organizationSlugSchema" }, "socials": { "type": "array", "items": { "$ref": "#/components/schemas/organizationSocialLinkSchema" }, "maxItems": 12 }, "website": { "type": [ "string", "null" ], "format": "uri" } }, "required": [ "addressCity", "addressCountry", "addressLine1", "addressLine2", "addressPostalCode", "addressState", "btw", "colors", "iban", "kvk", "name", "phone", "slug", "socials", "website" ] }, "organizationSlugSchema": { "type": "string", "minLength": 2, "maxLength": 64, "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$" }, "organizationSocialLinkSchema": { "type": "object", "properties": { "label": { "type": "string", "minLength": 1, "maxLength": 50 }, "url": { "type": "string", "format": "uri" } }, "required": [ "label", "url" ] } } ``` # Cancel a scheduled organization deletion. (/docs/openapi/cancelOrganizationDeletion) Cancel the pending deletion of an organization and enable it again. Only the person who requested the deletion or an owner may cancel it, before the grace period ends. ## DELETE /v1/organization-deletions/{organizationId} ```json { "operationId": "cancelOrganizationDeletion", "summary": "Cancel a scheduled organization deletion.", "description": "Cancel the pending deletion of an organization and enable it again. Only the person who requested the deletion or an owner may cancel it, before the grace period ends.", "tags": [ "Organizations" ], "parameters": [ { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "responses": { "200": { "description": "Returns the cancelled deletion.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/organizationDeletionSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "organizationDeletionSchema": { "type": "object", "properties": { "organizationId": { "type": "string", "format": "uuid" }, "purgeAfter": { "type": "string", "format": "date-time" }, "requestedAt": { "type": "string", "format": "date-time" } }, "required": [ "organizationId", "purgeAfter", "requestedAt" ] }, "organizationIdSchema": { "type": "string", "format": "uuid" } } ``` # Get a single organization available to the authenticated user. (/docs/openapi/getOrganizationById) Return the organization identified by the provided ID when the authenticated user can access it. ## GET /v1/organizations/{organizationId} ```json { "operationId": "getOrganizationById", "summary": "Get a single organization available to the authenticated user.", "description": "Return the organization identified by the provided ID when the authenticated user can access it.", "tags": [ "Organizations" ], "parameters": [ { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "responses": { "200": { "description": "Returns the requested organization.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/organizationResponseSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "BrandId": { "type": "string", "enum": [ "centrakit", "mechup", "stabledock", "caresly", "propdock", "marryduck", "signaldock", "spacelio", "legaldock" ] }, "brandingColorSchema": { "type": [ "string", "null" ], "pattern": "^#(?:[0-9a-f]{3}|[0-9a-f]{6})$" }, "lifecycleKeySchema": { "type": [ "string", "null" ], "minLength": 1, "maxLength": 120, "pattern": "^[a-z0-9][a-z0-9-]*$" }, "nullablePhoneInputSchema": { "type": [ "string", "null" ], "maxLength": 20 }, "nullableShortTextSchema": { "type": [ "string", "null" ], "maxLength": 200 }, "organizationAiInferenceRegionSchema": { "type": "string", "enum": [ "eu", "us", "global" ] }, "organizationBrandImageIdSchema": { "type": "string", "format": "uuid" }, "organizationBrandImageSchema": { "type": "object", "properties": { "height": { "type": "integer", "exclusiveMinimum": 0 }, "id": { "$ref": "#/components/schemas/organizationBrandImageIdSchema" }, "url": { "type": "string", "format": "uri" }, "width": { "type": "integer", "exclusiveMinimum": 0 } }, "required": [ "height", "id", "url", "width" ] }, "organizationBrandingColorsSchema": { "type": "object", "properties": { "primary": { "$ref": "#/components/schemas/brandingColorSchema" }, "secondary": { "$ref": "#/components/schemas/brandingColorSchema" } }, "required": [ "primary", "secondary" ] }, "organizationBrandingSchema": { "type": "object", "properties": { "colors": { "$ref": "#/components/schemas/organizationBrandingColorsSchema" }, "images": { "type": "array", "items": { "$ref": "#/components/schemas/organizationBrandImageSchema" }, "maxItems": 12 }, "logoUrl": { "type": [ "string", "null" ], "format": "uri" } }, "required": [ "colors", "images", "logoUrl" ] }, "organizationIdSchema": { "type": "string", "format": "uuid" }, "organizationLifecycleDefaultsSchema": { "type": "object", "properties": { "invoice": { "$ref": "#/components/schemas/lifecycleKeySchema" }, "order": { "$ref": "#/components/schemas/lifecycleKeySchema" }, "purchase_order": { "$ref": "#/components/schemas/lifecycleKeySchema" }, "quote": { "$ref": "#/components/schemas/lifecycleKeySchema" }, "shipment": { "$ref": "#/components/schemas/lifecycleKeySchema" }, "task": { "$ref": "#/components/schemas/lifecycleKeySchema" } }, "required": [ "invoice", "order", "purchase_order", "quote", "shipment", "task" ] }, "organizationOperatingDefaultsSchema": { "type": "object" }, "organizationResponseSchema": { "$ref": "#/components/schemas/organizationSchema" }, "organizationSchema": { "type": "object", "properties": { "addressCity": { "$ref": "#/components/schemas/nullableShortTextSchema" }, "addressCountry": { "$ref": "#/components/schemas/nullableShortTextSchema" }, "addressLine1": { "$ref": "#/components/schemas/nullableShortTextSchema" }, "addressLine2": { "$ref": "#/components/schemas/nullableShortTextSchema" }, "addressPostalCode": { "$ref": "#/components/schemas/nullableShortTextSchema" }, "addressState": { "$ref": "#/components/schemas/nullableShortTextSchema" }, "aiInferenceRegion": { "$ref": "#/components/schemas/organizationAiInferenceRegionSchema" }, "brand": { "$ref": "#/components/schemas/BrandId" }, "branding": { "$ref": "#/components/schemas/organizationBrandingSchema" }, "btw": { "$ref": "#/components/schemas/nullableShortTextSchema" }, "createdAt": { "type": "string", "format": "date-time" }, "disabledAt": { "type": [ "string", "null" ], "format": "date-time" }, "iban": { "$ref": "#/components/schemas/nullableShortTextSchema" }, "id": { "$ref": "#/components/schemas/organizationIdSchema" }, "kvk": { "$ref": "#/components/schemas/nullableShortTextSchema" }, "lifecycleDefaults": { "$ref": "#/components/schemas/organizationLifecycleDefaultsSchema" }, "name": { "type": "string", "minLength": 1 }, "operatingDefaults": { "$ref": "#/components/schemas/organizationOperatingDefaultsSchema" }, "phone": { "$ref": "#/components/schemas/nullablePhoneInputSchema" }, "slug": { "$ref": "#/components/schemas/organizationSlugSchema" }, "socials": { "type": "array", "items": { "$ref": "#/components/schemas/organizationSocialLinkSchema" } }, "updatedAt": { "type": "string", "format": "date-time" }, "website": { "type": [ "string", "null" ], "format": "uri" } }, "required": [ "addressCity", "addressCountry", "addressLine1", "addressLine2", "addressPostalCode", "addressState", "aiInferenceRegion", "brand", "branding", "btw", "createdAt", "disabledAt", "iban", "id", "kvk", "lifecycleDefaults", "name", "operatingDefaults", "phone", "slug", "socials", "updatedAt", "website" ] }, "organizationSlugSchema": { "type": "string", "minLength": 2, "maxLength": 64, "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$" }, "organizationSocialLinkSchema": { "type": "object", "properties": { "label": { "type": "string", "minLength": 1, "maxLength": 50 }, "url": { "type": "string", "format": "uri" } }, "required": [ "label", "url" ] } } ``` # Update a single organization. (/docs/openapi/updateOrganization) Update the settings for the specified organization when the authenticated user has permission. ## PATCH /v1/organizations/{organizationId} ```json { "operationId": "updateOrganization", "summary": "Update a single organization.", "description": "Update the settings for the specified organization when the authenticated user has permission.", "tags": [ "Organizations" ], "parameters": [ { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/organizationUpdateSchema" } } }, "description": "The updated organization settings." }, "responses": { "200": { "description": "Returns the updated organization.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/organizationResponseSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "BrandId": { "type": "string", "enum": [ "centrakit", "mechup", "stabledock", "caresly", "propdock", "marryduck", "signaldock", "spacelio", "legaldock" ] }, "brandingColorSchema": { "type": [ "string", "null" ], "pattern": "^#(?:[0-9a-f]{3}|[0-9a-f]{6})$" }, "lifecycleKeySchema": { "type": [ "string", "null" ], "minLength": 1, "maxLength": 120, "pattern": "^[a-z0-9][a-z0-9-]*$" }, "nullableKvkNumberInputSchema": { "type": [ "string", "null" ], "maxLength": 40 }, "nullablePhoneInputSchema": { "type": [ "string", "null" ], "maxLength": 20 }, "nullableShortTextSchema": { "type": [ "string", "null" ], "maxLength": 200 }, "nullableVatIdInputSchema": { "type": [ "string", "null" ], "maxLength": 40 }, "organizationAiInferenceRegionSchema": { "type": "string", "enum": [ "eu", "us", "global" ] }, "organizationBrandImageIdSchema": { "type": "string", "format": "uuid" }, "organizationBrandImageSchema": { "type": "object", "properties": { "height": { "type": "integer", "exclusiveMinimum": 0 }, "id": { "$ref": "#/components/schemas/organizationBrandImageIdSchema" }, "url": { "type": "string", "format": "uri" }, "width": { "type": "integer", "exclusiveMinimum": 0 } }, "required": [ "height", "id", "url", "width" ] }, "organizationBrandingColorsSchema": { "type": "object", "properties": { "primary": { "$ref": "#/components/schemas/brandingColorSchema" }, "secondary": { "$ref": "#/components/schemas/brandingColorSchema" } }, "required": [ "primary", "secondary" ] }, "organizationBrandingSchema": { "type": "object", "properties": { "colors": { "$ref": "#/components/schemas/organizationBrandingColorsSchema" }, "images": { "type": "array", "items": { "$ref": "#/components/schemas/organizationBrandImageSchema" }, "maxItems": 12 }, "logoUrl": { "type": [ "string", "null" ], "format": "uri" } }, "required": [ "colors", "images", "logoUrl" ] }, "organizationIdSchema": { "type": "string", "format": "uuid" }, "organizationLifecycleDefaultsSchema": { "type": "object", "properties": { "invoice": { "$ref": "#/components/schemas/lifecycleKeySchema" }, "order": { "$ref": "#/components/schemas/lifecycleKeySchema" }, "purchase_order": { "$ref": "#/components/schemas/lifecycleKeySchema" }, "quote": { "$ref": "#/components/schemas/lifecycleKeySchema" }, "shipment": { "$ref": "#/components/schemas/lifecycleKeySchema" }, "task": { "$ref": "#/components/schemas/lifecycleKeySchema" } }, "required": [ "invoice", "order", "purchase_order", "quote", "shipment", "task" ] }, "organizationOperatingDefaultsSchema": { "type": "object" }, "organizationResponseSchema": { "$ref": "#/components/schemas/organizationSchema" }, "organizationSchema": { "type": "object", "properties": { "addressCity": { "$ref": "#/components/schemas/nullableShortTextSchema" }, "addressCountry": { "$ref": "#/components/schemas/nullableShortTextSchema" }, "addressLine1": { "$ref": "#/components/schemas/nullableShortTextSchema" }, "addressLine2": { "$ref": "#/components/schemas/nullableShortTextSchema" }, "addressPostalCode": { "$ref": "#/components/schemas/nullableShortTextSchema" }, "addressState": { "$ref": "#/components/schemas/nullableShortTextSchema" }, "aiInferenceRegion": { "$ref": "#/components/schemas/organizationAiInferenceRegionSchema" }, "brand": { "$ref": "#/components/schemas/BrandId" }, "branding": { "$ref": "#/components/schemas/organizationBrandingSchema" }, "btw": { "$ref": "#/components/schemas/nullableShortTextSchema" }, "createdAt": { "type": "string", "format": "date-time" }, "disabledAt": { "type": [ "string", "null" ], "format": "date-time" }, "iban": { "$ref": "#/components/schemas/nullableShortTextSchema" }, "id": { "$ref": "#/components/schemas/organizationIdSchema" }, "kvk": { "$ref": "#/components/schemas/nullableShortTextSchema" }, "lifecycleDefaults": { "$ref": "#/components/schemas/organizationLifecycleDefaultsSchema" }, "name": { "type": "string", "minLength": 1 }, "operatingDefaults": { "$ref": "#/components/schemas/organizationOperatingDefaultsSchema" }, "phone": { "$ref": "#/components/schemas/nullablePhoneInputSchema" }, "slug": { "$ref": "#/components/schemas/organizationSlugSchema" }, "socials": { "type": "array", "items": { "$ref": "#/components/schemas/organizationSocialLinkSchema" } }, "updatedAt": { "type": "string", "format": "date-time" }, "website": { "type": [ "string", "null" ], "format": "uri" } }, "required": [ "addressCity", "addressCountry", "addressLine1", "addressLine2", "addressPostalCode", "addressState", "aiInferenceRegion", "brand", "branding", "btw", "createdAt", "disabledAt", "iban", "id", "kvk", "lifecycleDefaults", "name", "operatingDefaults", "phone", "slug", "socials", "updatedAt", "website" ] }, "organizationSettingsInputSchema": { "type": "object", "properties": { "addressCity": { "$ref": "#/components/schemas/nullableShortTextSchema" }, "addressCountry": { "$ref": "#/components/schemas/nullableShortTextSchema" }, "addressLine1": { "$ref": "#/components/schemas/nullableShortTextSchema" }, "addressLine2": { "$ref": "#/components/schemas/nullableShortTextSchema" }, "addressPostalCode": { "$ref": "#/components/schemas/nullableShortTextSchema" }, "addressState": { "$ref": "#/components/schemas/nullableShortTextSchema" }, "btw": { "$ref": "#/components/schemas/nullableVatIdInputSchema" }, "colors": { "$ref": "#/components/schemas/organizationBrandingColorsSchema" }, "iban": { "$ref": "#/components/schemas/nullableShortTextSchema" }, "kvk": { "$ref": "#/components/schemas/nullableKvkNumberInputSchema" }, "name": { "type": "string", "minLength": 2, "maxLength": 120 }, "phone": { "$ref": "#/components/schemas/nullablePhoneInputSchema" }, "slug": { "$ref": "#/components/schemas/organizationSlugSchema" }, "socials": { "type": "array", "items": { "$ref": "#/components/schemas/organizationSocialLinkSchema" }, "maxItems": 12 }, "website": { "type": [ "string", "null" ], "format": "uri" } }, "required": [ "addressCity", "addressCountry", "addressLine1", "addressLine2", "addressPostalCode", "addressState", "btw", "colors", "iban", "kvk", "name", "phone", "slug", "socials", "website" ] }, "organizationSlugSchema": { "type": "string", "minLength": 2, "maxLength": 64, "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$" }, "organizationSocialLinkSchema": { "type": "object", "properties": { "label": { "type": "string", "minLength": 1, "maxLength": 50 }, "url": { "type": "string", "format": "uri" } }, "required": [ "label", "url" ] }, "organizationUpdateSchema": { "$ref": "#/components/schemas/organizationSettingsInputSchema" } } ``` # Delete a single organization. (/docs/openapi/deleteOrganization) Schedule the specified organization for deletion when the authenticated user is allowed to remove it. The organization is disabled at once and its data is purged after the grace period. ## DELETE /v1/organizations/{organizationId} ```json { "operationId": "deleteOrganization", "summary": "Delete a single organization.", "description": "Schedule the specified organization for deletion when the authenticated user is allowed to remove it. The organization is disabled at once and its data is purged after the grace period.", "tags": [ "Organizations" ], "parameters": [ { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "responses": { "200": { "description": "Returns the time after which the organization is purged.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/organizationDeleteResponseSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "organizationDeleteResponseSchema": { "type": "object", "properties": { "purgeAfter": { "type": "string", "format": "date-time" }, "success": { "type": "boolean", "enum": [ true ] } }, "required": [ "purgeAfter", "success" ] }, "organizationIdSchema": { "type": "string", "format": "uuid" } } ``` # Set the active organization preference for the authenticated user. (/docs/openapi/setActiveOrganization) Store the preferred active organization on the current user's profile. This helps clients choose a default organization when no explicit organization id is supplied on a request, but it does not replace per-request membership checks or authorization. ## PATCH /v1/users/me/active-organization ```json { "operationId": "setActiveOrganization", "summary": "Set the active organization preference for the authenticated user.", "description": "Store the preferred active organization on the current user's profile. This helps clients choose a default organization when no explicit organization id is supplied on a request, but it does not replace per-request membership checks or authorization.", "tags": [ "Organizations" ], "parameters": [], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/organizationSelectionSchema" } } }, "description": "The organization to mark as the preferred active organization." }, "responses": { "200": { "description": "Returns a success payload when the active organization preference is updated.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/activeOrganizationResponseSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "activeOrganizationResponseSchema": { "type": "object", "properties": { "success": { "type": "boolean", "enum": [ true ] } }, "required": [ "success" ] }, "organizationIdSchema": { "type": "string", "format": "uuid" }, "organizationSelectionSchema": { "type": "object", "properties": { "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" } }, "required": [ "organizationId" ] } } ``` # Upload a brand image to the organization gallery. (/docs/openapi/uploadOrganizationBrandImage) Upload a PNG, JPEG, or WebP brand image. The server normalizes the image, strips EXIF metadata, and stores one standardized WebP image for rendering. Organizations are capped at 12 brand images. ## POST /v1/organizations/{organizationId}/branding/brand-images ```json { "operationId": "uploadOrganizationBrandImage", "summary": "Upload a brand image to the organization gallery.", "description": "Upload a PNG, JPEG, or WebP brand image. The server normalizes the image, strips EXIF metadata, and stores one standardized WebP image for rendering. Organizations are capped at 12 brand images.", "tags": [ "Organizations" ], "parameters": [ { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "requestBody": { "content": { "multipart/form-data": { "schema": { "$ref": "#/components/schemas/brandingFileUploadBodySchema" }, "encoding": { "file": { "contentType": "application/octet-stream" } } } }, "description": "Multipart form data with a single `file` field containing the brand image." }, "responses": { "200": { "description": "Returns the metadata and public URL for the stored brand image.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/organizationBrandImageResponseSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "brandingFileUploadBodySchema": { "type": "object", "properties": { "file": { "type": "string", "contentMediaType": "application/octet-stream", "description": "PNG, JPEG, or WebP image file" } }, "required": [ "file" ] }, "organizationBrandImageIdSchema": { "type": "string", "format": "uuid" }, "organizationBrandImageResponseSchema": { "$ref": "#/components/schemas/organizationBrandImageSchema" }, "organizationBrandImageSchema": { "type": "object", "properties": { "height": { "type": "integer", "exclusiveMinimum": 0 }, "id": { "$ref": "#/components/schemas/organizationBrandImageIdSchema" }, "url": { "type": "string", "format": "uri" }, "width": { "type": "integer", "exclusiveMinimum": 0 } }, "required": [ "height", "id", "url", "width" ] }, "organizationIdSchema": { "type": "string", "format": "uuid" } } ``` # Upload the organization logo. (/docs/openapi/uploadOrganizationLogo) Upload a PNG, JPEG, or WebP file to replace the organization logo. The server normalizes the image, strips EXIF metadata, and stores one standardized WebP image for rendering. ## POST /v1/organizations/{organizationId}/branding/logo ```json { "operationId": "uploadOrganizationLogo", "summary": "Upload the organization logo.", "description": "Upload a PNG, JPEG, or WebP file to replace the organization logo. The server normalizes the image, strips EXIF metadata, and stores one standardized WebP image for rendering.", "tags": [ "Organizations" ], "parameters": [ { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "requestBody": { "content": { "multipart/form-data": { "schema": { "$ref": "#/components/schemas/brandingFileUploadBodySchema" }, "encoding": { "file": { "contentType": "application/octet-stream" } } } }, "description": "Multipart form data with a single `file` field containing the logo image." }, "responses": { "200": { "description": "Returns the public URL for the generated logo.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/uploadBrandingLogoResponseSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "brandingFileUploadBodySchema": { "type": "object", "properties": { "file": { "type": "string", "contentMediaType": "application/octet-stream", "description": "PNG, JPEG, or WebP image file" } }, "required": [ "file" ] }, "organizationIdSchema": { "type": "string", "format": "uuid" }, "uploadBrandingLogoResponseSchema": { "type": "object", "properties": { "logoUrl": { "type": "string", "format": "uri" } }, "required": [ "logoUrl" ] } } ``` # Remove the organization logo. (/docs/openapi/removeOrganizationLogo) Remove the organization logo from storage. ## DELETE /v1/organizations/{organizationId}/branding/logo ```json { "operationId": "removeOrganizationLogo", "summary": "Remove the organization logo.", "description": "Remove the organization logo from storage.", "tags": [ "Organizations" ], "parameters": [ { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "responses": { "200": { "description": "Returns a success payload when the logo is removed.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/organizationDeleteResponseSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "organizationDeleteResponseSchema": { "type": "object", "properties": { "purgeAfter": { "type": "string", "format": "date-time" }, "success": { "type": "boolean", "enum": [ true ] } }, "required": [ "purgeAfter", "success" ] }, "organizationIdSchema": { "type": "string", "format": "uuid" } } ``` # Remove a brand image from the organization gallery. (/docs/openapi/removeOrganizationBrandImage) Delete a stored brand image. ## DELETE /v1/organizations/{organizationId}/branding/brand-images/{brandImageId} ```json { "operationId": "removeOrganizationBrandImage", "summary": "Remove a brand image from the organization gallery.", "description": "Delete a stored brand image.", "tags": [ "Organizations" ], "parameters": [ { "in": "path", "name": "brandImageId", "required": true, "schema": { "$ref": "#/components/schemas/organizationBrandImageIdSchema" }, "example": "123" }, { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "responses": { "200": { "description": "Returns a success payload when the brand image is removed.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/organizationDeleteResponseSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "organizationBrandImageIdSchema": { "type": "string", "format": "uuid" }, "organizationDeleteResponseSchema": { "type": "object", "properties": { "purgeAfter": { "type": "string", "format": "date-time" }, "success": { "type": "boolean", "enum": [ true ] } }, "required": [ "purgeAfter", "success" ] }, "organizationIdSchema": { "type": "string", "format": "uuid" } } ``` # List my pending approvals. (/docs/openapi/listPendingApprovals) Return time-off requests, timesheets, and expense claims waiting for the caller's decision, including delegated ones. ## GET /v1/organizations/{organizationId}/approvals ```json { "operationId": "listPendingApprovals", "summary": "List my pending approvals.", "description": "Return time-off requests, timesheets, and expense claims waiting for the caller's decision, including delegated ones.", "tags": [ "People" ], "parameters": [ { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "responses": { "200": { "description": "Returns pending approvals.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/approvalsResponseSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "approvalsResponseSchema": { "type": "array", "items": { "$ref": "#/components/schemas/hrApprovalSchema" } }, "hrApprovalDecisionSchema": { "type": "string", "enum": [ "pending", "approved", "rejected" ] }, "hrApprovalSchema": { "type": "object", "properties": { "approverEmployeeId": { "type": [ "string", "null" ], "format": "uuid" }, "createdAt": { "type": "string" }, "decidedAt": { "type": [ "string", "null" ] }, "decidedBy": { "type": [ "string", "null" ], "format": "uuid" }, "decision": { "$ref": "#/components/schemas/hrApprovalDecisionSchema" }, "employee": { "$ref": "#/components/schemas/personSummarySchema" }, "id": { "type": "string", "format": "uuid" }, "note": { "type": [ "string", "null" ] }, "stepNumber": { "type": "integer" }, "subjectId": { "type": "string", "format": "uuid" }, "subjectType": { "$ref": "#/components/schemas/hrApprovalSubjectSchema" }, "summary": { "type": "string" }, "weekStart": { "anyOf": [ { "$ref": "#/components/schemas/isoDateSchema" }, { "type": "null" } ] } }, "required": [ "approverEmployeeId", "createdAt", "decidedAt", "decidedBy", "decision", "employee", "id", "note", "stepNumber", "subjectId", "subjectType", "summary", "weekStart" ] }, "hrApprovalSubjectSchema": { "type": "string", "enum": [ "time_off_request", "timesheet", "expense" ] }, "isoDateSchema": { "type": "string", "format": "date" }, "organizationIdSchema": { "type": "string", "format": "uuid" }, "personSummarySchema": { "type": "object", "properties": { "avatarUrl": { "type": [ "string", "null" ] }, "displayName": { "type": "string" }, "employeeId": { "type": "string", "format": "uuid" }, "jobTitle": { "type": [ "string", "null" ] }, "userId": { "type": [ "string", "null" ], "format": "uuid" } }, "required": [ "avatarUrl", "displayName", "employeeId", "jobTitle", "userId" ] } } ``` # Decide approvals. (/docs/openapi/decideApprovals) Approve or reject up to 100 pending approvals at once. Each approval succeeds or fails on its own. ## POST /v1/organizations/{organizationId}/approvals ```json { "operationId": "decideApprovals", "summary": "Decide approvals.", "description": "Approve or reject up to 100 pending approvals at once. Each approval succeeds or fails on its own.", "tags": [ "People" ], "parameters": [ { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/hrApprovalDecideInputSchema" } } }, "description": "The approvals and the decision." }, "responses": { "200": { "description": "Returns which approvals were decided and which failed.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/hrApprovalDecideResultSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "hrApprovalDecideInputSchema": { "type": "object", "properties": { "approvalIds": { "type": "array", "items": { "type": "string", "format": "uuid" }, "minItems": 1, "maxItems": 100 }, "decision": { "type": "string", "enum": [ "approved", "rejected" ] }, "note": { "type": [ "string", "null" ], "maxLength": 2000 } }, "required": [ "approvalIds", "decision", "note" ] }, "hrApprovalDecideResultSchema": { "type": "object", "properties": { "decided": { "type": "array", "items": { "type": "string", "format": "uuid" } }, "failed": { "type": "array", "items": { "type": "object", "properties": { "approvalId": { "type": "string", "format": "uuid" }, "message": { "type": "string" } }, "required": [ "approvalId", "message" ] } } }, "required": [ "decided", "failed" ] }, "organizationIdSchema": { "type": "string", "format": "uuid" } } ``` # List employees. (/docs/openapi/listEmployees) Return employees, contractors, and interns for an organization. Private details are never included. ## GET /v1/organizations/{organizationId}/employees ```json { "operationId": "listEmployees", "summary": "List employees.", "description": "Return employees, contractors, and interns for an organization. Private details are never included.", "tags": [ "People" ], "parameters": [ { "in": "query", "name": "includeArchived", "required": false, "schema": { "type": "string", "enum": [ "true", "false" ] }, "example": "true" }, { "in": "query", "name": "kind", "required": false, "schema": { "allOf": [ { "$ref": "#/components/schemas/employeeKindSchema" } ] }, "example": "example" }, { "in": "query", "name": "limit", "required": true, "schema": { "default": 50, "type": "integer", "minimum": 1, "maximum": 100 }, "example": 1 }, { "in": "query", "name": "managerEmployeeId", "required": false, "schema": { "type": "string", "format": "uuid" }, "example": "123e4567-e89b-12d3-a456-426614174000" }, { "in": "query", "name": "offset", "required": true, "schema": { "default": 0, "type": "integer", "minimum": 0, "maximum": 9007199254740991 }, "example": 1 }, { "in": "query", "name": "q", "required": false, "schema": { "type": "string", "maxLength": 200 }, "example": "example" }, { "in": "query", "name": "status", "required": false, "schema": { "allOf": [ { "$ref": "#/components/schemas/employeeStatusSchema" } ] }, "example": "example" }, { "in": "query", "name": "teamId", "required": false, "schema": { "type": "string", "format": "uuid" }, "example": "123e4567-e89b-12d3-a456-426614174000" }, { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "responses": { "200": { "description": "Returns employees.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/employeesResponseSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "employeeKindSchema": { "type": "string", "enum": [ "employee", "contractor", "intern" ] }, "employeeSchema": { "type": "object", "properties": { "accountDisabled": { "type": "boolean" }, "archivedAt": { "type": [ "string", "null" ] }, "avatarUrl": { "type": [ "string", "null" ] }, "companyName": { "type": [ "string", "null" ] }, "createdAt": { "type": "string" }, "departmentTeamId": { "anyOf": [ { "$ref": "#/components/schemas/teamIdSchema" }, { "type": "null" } ] }, "departmentTeamName": { "type": [ "string", "null" ] }, "displayName": { "type": "string" }, "endDate": { "type": "object" }, "firstName": { "type": "string" }, "hasAppAccess": { "type": "boolean" }, "id": { "type": "string", "format": "uuid" }, "jobTitle": { "type": [ "string", "null" ] }, "kind": { "$ref": "#/components/schemas/employeeKindSchema" }, "kvk": { "type": [ "string", "null" ] }, "lastName": { "type": [ "string", "null" ] }, "manager": { "anyOf": [ { "$ref": "#/components/schemas/personSummarySchema" }, { "type": "null" } ] }, "organizationId": { "type": "string", "format": "uuid" }, "reportsToEmployeeId": { "type": [ "string", "null" ], "format": "uuid" }, "scheduleCycleAnchor": { "type": "object" }, "showBirthday": { "type": "boolean" }, "showWorkAnniversary": { "type": "boolean" }, "startDate": { "type": "object" }, "status": { "$ref": "#/components/schemas/employeeStatusSchema" }, "timezone": { "type": [ "string", "null" ] }, "updatedAt": { "type": "string" }, "userId": { "type": [ "string", "null" ], "format": "uuid" }, "vatId": { "type": [ "string", "null" ] }, "workEmail": { "type": [ "string", "null" ] } }, "required": [ "accountDisabled", "archivedAt", "avatarUrl", "companyName", "createdAt", "departmentTeamId", "departmentTeamName", "displayName", "endDate", "firstName", "hasAppAccess", "id", "jobTitle", "kind", "kvk", "lastName", "manager", "organizationId", "reportsToEmployeeId", "scheduleCycleAnchor", "showBirthday", "showWorkAnniversary", "startDate", "status", "timezone", "updatedAt", "userId", "vatId", "workEmail" ] }, "employeeStatusSchema": { "type": "string", "enum": [ "onboarding", "active", "offboarding", "ended" ] }, "employeesResponseSchema": { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/components/schemas/employeeSchema" } }, "pagination": { "$ref": "#/components/schemas/listPaginationSchema" } }, "required": [ "data", "pagination" ] }, "listPaginationSchema": { "type": "object", "properties": { "hasMore": { "type": "boolean" }, "limit": { "type": "integer", "minimum": 1, "maximum": 100 }, "offset": { "type": "integer", "minimum": 0 }, "total": { "type": "integer", "minimum": 0 }, "totalIsEstimate": { "type": "boolean" } }, "required": [ "hasMore", "limit", "offset", "total" ] }, "organizationIdSchema": { "type": "string", "format": "uuid" }, "personSummarySchema": { "type": "object", "properties": { "avatarUrl": { "type": [ "string", "null" ] }, "displayName": { "type": "string" }, "employeeId": { "type": "string", "format": "uuid" }, "jobTitle": { "type": [ "string", "null" ] }, "userId": { "type": [ "string", "null" ], "format": "uuid" } }, "required": [ "avatarUrl", "displayName", "employeeId", "jobTitle", "userId" ] }, "teamIdSchema": { "type": "string", "format": "uuid" } } ``` # Create an employee. (/docs/openapi/createEmployee) Create an employee record without a user account. Every organization member already has one. ## POST /v1/organizations/{organizationId}/employees ```json { "operationId": "createEmployee", "summary": "Create an employee.", "description": "Create an employee record without a user account. Every organization member already has one.", "tags": [ "People" ], "parameters": [ { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/employeeInputSchema" } } }, "description": "The employee payload." }, "responses": { "201": { "description": "Returns the created employee.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/employeeSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "employeeInputSchema": { "type": "object", "properties": { "companyName": { "type": [ "string", "null" ], "maxLength": 200 }, "departmentTeamId": { "anyOf": [ { "$ref": "#/components/schemas/teamIdSchema" }, { "type": "null" } ] }, "endDate": { "type": "object" }, "firstName": { "type": "string", "minLength": 1, "maxLength": 120 }, "jobTitle": { "type": [ "string", "null" ], "maxLength": 160 }, "kind": { "type": "string", "enum": [ "employee", "contractor", "intern" ], "default": "employee" }, "kvk": { "type": [ "string", "null" ], "maxLength": 32 }, "lastName": { "type": [ "string", "null" ], "maxLength": 120 }, "reportsToEmployeeId": { "type": [ "string", "null" ], "format": "uuid" }, "showBirthday": { "type": "boolean" }, "showWorkAnniversary": { "type": "boolean" }, "startDate": { "type": "object" }, "status": { "allOf": [ { "$ref": "#/components/schemas/employeeStatusSchema" } ] }, "timezone": { "type": [ "string", "null" ], "maxLength": 64 }, "vatId": { "type": [ "string", "null" ], "maxLength": 32 }, "workEmail": { "anyOf": [ { "type": [ "string", "null" ], "format": "email", "maxLength": 320 }, { "type": "string", "enum": [ "" ] } ] } }, "required": [ "companyName", "firstName", "jobTitle", "kind", "kvk", "lastName", "timezone", "vatId" ] }, "employeeKindSchema": { "type": "string", "enum": [ "employee", "contractor", "intern" ] }, "employeeSchema": { "type": "object", "properties": { "accountDisabled": { "type": "boolean" }, "archivedAt": { "type": [ "string", "null" ] }, "avatarUrl": { "type": [ "string", "null" ] }, "companyName": { "type": [ "string", "null" ] }, "createdAt": { "type": "string" }, "departmentTeamId": { "anyOf": [ { "$ref": "#/components/schemas/teamIdSchema" }, { "type": "null" } ] }, "departmentTeamName": { "type": [ "string", "null" ] }, "displayName": { "type": "string" }, "endDate": { "type": "object" }, "firstName": { "type": "string" }, "hasAppAccess": { "type": "boolean" }, "id": { "type": "string", "format": "uuid" }, "jobTitle": { "type": [ "string", "null" ] }, "kind": { "$ref": "#/components/schemas/employeeKindSchema" }, "kvk": { "type": [ "string", "null" ] }, "lastName": { "type": [ "string", "null" ] }, "manager": { "anyOf": [ { "$ref": "#/components/schemas/personSummarySchema" }, { "type": "null" } ] }, "organizationId": { "type": "string", "format": "uuid" }, "reportsToEmployeeId": { "type": [ "string", "null" ], "format": "uuid" }, "scheduleCycleAnchor": { "type": "object" }, "showBirthday": { "type": "boolean" }, "showWorkAnniversary": { "type": "boolean" }, "startDate": { "type": "object" }, "status": { "$ref": "#/components/schemas/employeeStatusSchema" }, "timezone": { "type": [ "string", "null" ] }, "updatedAt": { "type": "string" }, "userId": { "type": [ "string", "null" ], "format": "uuid" }, "vatId": { "type": [ "string", "null" ] }, "workEmail": { "type": [ "string", "null" ] } }, "required": [ "accountDisabled", "archivedAt", "avatarUrl", "companyName", "createdAt", "departmentTeamId", "departmentTeamName", "displayName", "endDate", "firstName", "hasAppAccess", "id", "jobTitle", "kind", "kvk", "lastName", "manager", "organizationId", "reportsToEmployeeId", "scheduleCycleAnchor", "showBirthday", "showWorkAnniversary", "startDate", "status", "timezone", "updatedAt", "userId", "vatId", "workEmail" ] }, "employeeStatusSchema": { "type": "string", "enum": [ "onboarding", "active", "offboarding", "ended" ] }, "organizationIdSchema": { "type": "string", "format": "uuid" }, "personSummarySchema": { "type": "object", "properties": { "avatarUrl": { "type": [ "string", "null" ] }, "displayName": { "type": "string" }, "employeeId": { "type": "string", "format": "uuid" }, "jobTitle": { "type": [ "string", "null" ] }, "userId": { "type": [ "string", "null" ], "format": "uuid" } }, "required": [ "avatarUrl", "displayName", "employeeId", "jobTitle", "userId" ] }, "teamIdSchema": { "type": "string", "format": "uuid" } } ``` # List time entries. (/docs/openapi/listTimeEntries) Return logged time for tasks, customers, and internal work. ## GET /v1/organizations/{organizationId}/time-entries ```json { "operationId": "listTimeEntries", "summary": "List time entries.", "description": "Return logged time for tasks, customers, and internal work.", "tags": [ "People" ], "parameters": [ { "in": "query", "name": "customerId", "required": false, "schema": { "type": "string", "format": "uuid" }, "example": "123e4567-e89b-12d3-a456-426614174000" }, { "in": "query", "name": "employeeId", "required": false, "schema": { "type": "string", "format": "uuid" }, "example": "123e4567-e89b-12d3-a456-426614174000" }, { "in": "query", "name": "from", "required": false, "schema": { "type": "string", "format": "date-time" }, "example": "example" }, { "in": "query", "name": "limit", "required": true, "schema": { "default": 50, "type": "integer", "minimum": 1, "maximum": 100 }, "example": 1 }, { "in": "query", "name": "offset", "required": true, "schema": { "default": 0, "type": "integer", "minimum": 0, "maximum": 9007199254740991 }, "example": 1 }, { "in": "query", "name": "taskId", "required": false, "schema": { "type": "string", "format": "uuid" }, "example": "123e4567-e89b-12d3-a456-426614174000" }, { "in": "query", "name": "to", "required": false, "schema": { "type": "string", "format": "date-time" }, "example": "example" }, { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "responses": { "200": { "description": "Returns time entries.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/timeEntriesResponseSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "customerIdSchema": { "type": "string", "format": "uuid" }, "listPaginationSchema": { "type": "object", "properties": { "hasMore": { "type": "boolean" }, "limit": { "type": "integer", "minimum": 1, "maximum": 100 }, "offset": { "type": "integer", "minimum": 0 }, "total": { "type": "integer", "minimum": 0 }, "totalIsEstimate": { "type": "boolean" } }, "required": [ "hasMore", "limit", "offset", "total" ] }, "organizationIdSchema": { "type": "string", "format": "uuid" }, "timeEntriesResponseSchema": { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/components/schemas/timeEntrySchema" } }, "pagination": { "$ref": "#/components/schemas/listPaginationSchema" } }, "required": [ "data", "pagination" ] }, "timeEntrySchema": { "type": "object", "properties": { "agendaItemId": { "type": [ "string", "null" ], "format": "uuid" }, "agendaItemTitle": { "type": [ "string", "null" ] }, "billable": { "type": "boolean" }, "createdAt": { "type": "string" }, "customerId": { "anyOf": [ { "$ref": "#/components/schemas/customerIdSchema" }, { "type": "null" } ] }, "customerName": { "type": [ "string", "null" ] }, "durationSeconds": { "type": [ "integer", "null" ] }, "employeeId": { "type": "string", "format": "uuid" }, "id": { "type": "string", "format": "uuid" }, "invoiceLineId": { "type": [ "string", "null" ], "format": "uuid" }, "locked": { "type": "boolean" }, "note": { "type": [ "string", "null" ] }, "source": { "$ref": "#/components/schemas/timeEntrySourceSchema" }, "startedAt": { "type": "string" }, "taskId": { "type": [ "string", "null" ], "format": "uuid" }, "taskTitle": { "type": [ "string", "null" ] }, "timesheetId": { "type": [ "string", "null" ], "format": "uuid" }, "userId": { "type": [ "string", "null" ], "format": "uuid" } }, "required": [ "agendaItemId", "agendaItemTitle", "billable", "createdAt", "customerId", "customerName", "durationSeconds", "employeeId", "id", "invoiceLineId", "locked", "note", "source", "startedAt", "taskId", "taskTitle", "timesheetId", "userId" ] }, "timeEntrySourceSchema": { "type": "string", "enum": [ "manual", "timer", "agenda", "planning", "suggestion" ] } } ``` # Log time. (/docs/openapi/createTimeEntry) Create a time entry. Entries in a submitted or approved timesheet week are rejected. ## POST /v1/organizations/{organizationId}/time-entries ```json { "operationId": "createTimeEntry", "summary": "Log time.", "description": "Create a time entry. Entries in a submitted or approved timesheet week are rejected.", "tags": [ "People" ], "parameters": [ { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/timeEntryInputSchema" } } }, "description": "The time entry." }, "responses": { "201": { "description": "Returns the created time entry.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/timeEntrySchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "customerIdSchema": { "type": "string", "format": "uuid" }, "organizationIdSchema": { "type": "string", "format": "uuid" }, "timeEntryInputSchema": { "type": "object", "properties": { "agendaItemId": { "type": [ "string", "null" ], "format": "uuid" }, "billable": { "type": "boolean" }, "customerId": { "anyOf": [ { "$ref": "#/components/schemas/customerIdSchema" }, { "type": "null" } ] }, "durationSeconds": { "type": "integer", "exclusiveMinimum": 0, "maximum": 2678400 }, "employeeId": { "type": "string", "format": "uuid" }, "note": { "type": [ "string", "null" ], "maxLength": 2000 }, "source": { "allOf": [ { "$ref": "#/components/schemas/timeEntrySourceSchema" } ] }, "startedAt": { "type": "string", "format": "date-time" }, "taskId": { "type": [ "string", "null" ], "format": "uuid" } }, "required": [ "durationSeconds", "note", "startedAt" ] }, "timeEntrySchema": { "type": "object", "properties": { "agendaItemId": { "type": [ "string", "null" ], "format": "uuid" }, "agendaItemTitle": { "type": [ "string", "null" ] }, "billable": { "type": "boolean" }, "createdAt": { "type": "string" }, "customerId": { "anyOf": [ { "$ref": "#/components/schemas/customerIdSchema" }, { "type": "null" } ] }, "customerName": { "type": [ "string", "null" ] }, "durationSeconds": { "type": [ "integer", "null" ] }, "employeeId": { "type": "string", "format": "uuid" }, "id": { "type": "string", "format": "uuid" }, "invoiceLineId": { "type": [ "string", "null" ], "format": "uuid" }, "locked": { "type": "boolean" }, "note": { "type": [ "string", "null" ] }, "source": { "$ref": "#/components/schemas/timeEntrySourceSchema" }, "startedAt": { "type": "string" }, "taskId": { "type": [ "string", "null" ], "format": "uuid" }, "taskTitle": { "type": [ "string", "null" ] }, "timesheetId": { "type": [ "string", "null" ], "format": "uuid" }, "userId": { "type": [ "string", "null" ], "format": "uuid" } }, "required": [ "agendaItemId", "agendaItemTitle", "billable", "createdAt", "customerId", "customerName", "durationSeconds", "employeeId", "id", "invoiceLineId", "locked", "note", "source", "startedAt", "taskId", "taskTitle", "timesheetId", "userId" ] }, "timeEntrySourceSchema": { "type": "string", "enum": [ "manual", "timer", "agenda", "planning", "suggestion" ] } } ``` # List time-off requests. (/docs/openapi/listTimeOffRequests) Return time-off requests visible to the caller: their own, their reports', or everyone's for HR. ## GET /v1/organizations/{organizationId}/time-off-requests ```json { "operationId": "listTimeOffRequests", "summary": "List time-off requests.", "description": "Return time-off requests visible to the caller: their own, their reports', or everyone's for HR.", "tags": [ "People" ], "parameters": [ { "in": "query", "name": "employeeId", "required": false, "schema": { "type": "string", "format": "uuid" }, "example": "123e4567-e89b-12d3-a456-426614174000" }, { "in": "query", "name": "from", "required": false, "schema": { "type": "object" }, "style": "deepObject", "explode": true, "example": "example" }, { "in": "query", "name": "limit", "required": true, "schema": { "default": 50, "type": "integer", "minimum": 1, "maximum": 100 }, "example": 1 }, { "in": "query", "name": "offset", "required": true, "schema": { "default": 0, "type": "integer", "minimum": 0, "maximum": 9007199254740991 }, "example": 1 }, { "in": "query", "name": "status", "required": false, "schema": { "allOf": [ { "$ref": "#/components/schemas/timeOffRequestStatusSchema" } ] }, "example": "example" }, { "in": "query", "name": "to", "required": false, "schema": { "type": "object" }, "style": "deepObject", "explode": true, "example": "example" }, { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "responses": { "200": { "description": "Returns time-off requests.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/timeOffRequestsResponseSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "isoDateSchema": { "type": "string", "format": "date" }, "listPaginationSchema": { "type": "object", "properties": { "hasMore": { "type": "boolean" }, "limit": { "type": "integer", "minimum": 1, "maximum": 100 }, "offset": { "type": "integer", "minimum": 0 }, "total": { "type": "integer", "minimum": 0 }, "totalIsEstimate": { "type": "boolean" } }, "required": [ "hasMore", "limit", "offset", "total" ] }, "organizationIdSchema": { "type": "string", "format": "uuid" }, "personSummarySchema": { "type": "object", "properties": { "avatarUrl": { "type": [ "string", "null" ] }, "displayName": { "type": "string" }, "employeeId": { "type": "string", "format": "uuid" }, "jobTitle": { "type": [ "string", "null" ] }, "userId": { "type": [ "string", "null" ], "format": "uuid" } }, "required": [ "avatarUrl", "displayName", "employeeId", "jobTitle", "userId" ] }, "timeOffCategorySchema": { "type": "string", "enum": [ "vacation", "sick", "special", "unpaid", "parental", "compensatory" ] }, "timeOffPartialDaySchema": { "type": "string", "enum": [ "full", "morning", "afternoon", "hours" ] }, "timeOffRequestSchema": { "type": "object", "properties": { "allowedNegativeHours": { "type": [ "number", "null" ] }, "balanceAfterApproval": { "type": [ "number", "null" ] }, "canDecide": { "type": "boolean" }, "canceledAt": { "type": [ "string", "null" ] }, "createdAt": { "type": "string" }, "decidedAt": { "type": [ "string", "null" ] }, "decidedBy": { "type": [ "string", "null" ], "format": "uuid" }, "decisionNote": { "type": [ "string", "null" ] }, "deductsBalance": { "type": "boolean" }, "employee": { "$ref": "#/components/schemas/personSummarySchema" }, "employeeId": { "type": "string", "format": "uuid" }, "endPart": { "$ref": "#/components/schemas/timeOffPartialDaySchema" }, "endTime": { "type": [ "string", "null" ] }, "endsOn": { "$ref": "#/components/schemas/isoDateSchema" }, "hasAttachment": { "type": "boolean" }, "hours": { "type": "number" }, "id": { "type": "string", "format": "uuid" }, "masked": { "type": "boolean" }, "needsRecalculation": { "type": "boolean" }, "note": { "type": [ "string", "null" ] }, "startPart": { "$ref": "#/components/schemas/timeOffPartialDaySchema" }, "startTime": { "type": [ "string", "null" ] }, "startsOn": { "$ref": "#/components/schemas/isoDateSchema" }, "status": { "$ref": "#/components/schemas/timeOffRequestStatusSchema" }, "timeOffTypeId": { "type": [ "string", "null" ], "format": "uuid" }, "typeCategory": { "anyOf": [ { "$ref": "#/components/schemas/timeOffCategorySchema" }, { "type": "null" } ] }, "typeColor": { "type": [ "string", "null" ] }, "typeName": { "type": "string" } }, "required": [ "allowedNegativeHours", "balanceAfterApproval", "canDecide", "canceledAt", "createdAt", "decidedAt", "decidedBy", "decisionNote", "deductsBalance", "employee", "employeeId", "endPart", "endTime", "endsOn", "hasAttachment", "hours", "id", "masked", "needsRecalculation", "note", "startPart", "startTime", "startsOn", "status", "timeOffTypeId", "typeCategory", "typeColor", "typeName" ] }, "timeOffRequestStatusSchema": { "type": "string", "enum": [ "pending", "approved", "rejected", "canceled" ] }, "timeOffRequestsResponseSchema": { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/components/schemas/timeOffRequestSchema" } }, "pagination": { "$ref": "#/components/schemas/listPaginationSchema" } }, "required": [ "data", "pagination" ] } } ``` # Request time off. (/docs/openapi/createTimeOffRequest) Create a time-off request in hours. It is routed to the employee's manager for approval. ## POST /v1/organizations/{organizationId}/time-off-requests ```json { "operationId": "createTimeOffRequest", "summary": "Request time off.", "description": "Create a time-off request in hours. It is routed to the employee's manager for approval.", "tags": [ "People" ], "parameters": [ { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/timeOffRequestInputSchema" } } }, "description": "The time-off request." }, "responses": { "201": { "description": "Returns the created request.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/timeOffRequestSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "clockTimeSchema": { "type": "string", "pattern": "^(?<group1>[01]\\d|2[0-3]):[0-5]\\d(?<group2>:[0-5]\\d)?$" }, "isoDateSchema": { "type": "string", "format": "date" }, "organizationIdSchema": { "type": "string", "format": "uuid" }, "personSummarySchema": { "type": "object", "properties": { "avatarUrl": { "type": [ "string", "null" ] }, "displayName": { "type": "string" }, "employeeId": { "type": "string", "format": "uuid" }, "jobTitle": { "type": [ "string", "null" ] }, "userId": { "type": [ "string", "null" ], "format": "uuid" } }, "required": [ "avatarUrl", "displayName", "employeeId", "jobTitle", "userId" ] }, "timeOffCategorySchema": { "type": "string", "enum": [ "vacation", "sick", "special", "unpaid", "parental", "compensatory" ] }, "timeOffPartialDaySchema": { "type": "string", "enum": [ "full", "morning", "afternoon", "hours" ] }, "timeOffRequestInputSchema": { "type": "object", "properties": { "acknowledgeWarnings": { "type": "boolean" }, "employeeId": { "type": "string", "format": "uuid" }, "endPart": { "type": "string", "enum": [ "full", "morning", "afternoon", "hours" ], "default": "full" }, "endTime": { "anyOf": [ { "$ref": "#/components/schemas/clockTimeSchema" }, { "type": "null" } ] }, "endsOn": { "$ref": "#/components/schemas/isoDateSchema" }, "note": { "type": [ "string", "null" ], "maxLength": 2000 }, "startPart": { "type": "string", "enum": [ "full", "morning", "afternoon", "hours" ], "default": "full" }, "startTime": { "anyOf": [ { "$ref": "#/components/schemas/clockTimeSchema" }, { "type": "null" } ] }, "startsOn": { "$ref": "#/components/schemas/isoDateSchema" }, "timeOffTypeId": { "type": "string", "format": "uuid" } }, "required": [ "endPart", "endsOn", "note", "startPart", "startsOn", "timeOffTypeId" ] }, "timeOffRequestSchema": { "type": "object", "properties": { "allowedNegativeHours": { "type": [ "number", "null" ] }, "balanceAfterApproval": { "type": [ "number", "null" ] }, "canDecide": { "type": "boolean" }, "canceledAt": { "type": [ "string", "null" ] }, "createdAt": { "type": "string" }, "decidedAt": { "type": [ "string", "null" ] }, "decidedBy": { "type": [ "string", "null" ], "format": "uuid" }, "decisionNote": { "type": [ "string", "null" ] }, "deductsBalance": { "type": "boolean" }, "employee": { "$ref": "#/components/schemas/personSummarySchema" }, "employeeId": { "type": "string", "format": "uuid" }, "endPart": { "$ref": "#/components/schemas/timeOffPartialDaySchema" }, "endTime": { "type": [ "string", "null" ] }, "endsOn": { "$ref": "#/components/schemas/isoDateSchema" }, "hasAttachment": { "type": "boolean" }, "hours": { "type": "number" }, "id": { "type": "string", "format": "uuid" }, "masked": { "type": "boolean" }, "needsRecalculation": { "type": "boolean" }, "note": { "type": [ "string", "null" ] }, "startPart": { "$ref": "#/components/schemas/timeOffPartialDaySchema" }, "startTime": { "type": [ "string", "null" ] }, "startsOn": { "$ref": "#/components/schemas/isoDateSchema" }, "status": { "$ref": "#/components/schemas/timeOffRequestStatusSchema" }, "timeOffTypeId": { "type": [ "string", "null" ], "format": "uuid" }, "typeCategory": { "anyOf": [ { "$ref": "#/components/schemas/timeOffCategorySchema" }, { "type": "null" } ] }, "typeColor": { "type": [ "string", "null" ] }, "typeName": { "type": "string" } }, "required": [ "allowedNegativeHours", "balanceAfterApproval", "canDecide", "canceledAt", "createdAt", "decidedAt", "decidedBy", "decisionNote", "deductsBalance", "employee", "employeeId", "endPart", "endTime", "endsOn", "hasAttachment", "hours", "id", "masked", "needsRecalculation", "note", "startPart", "startTime", "startsOn", "status", "timeOffTypeId", "typeCategory", "typeColor", "typeName" ] }, "timeOffRequestStatusSchema": { "type": "string", "enum": [ "pending", "approved", "rejected", "canceled" ] } } ``` # Get an employee. (/docs/openapi/getEmployee) Return one employee. Private details are never included. ## GET /v1/organizations/{organizationId}/employees/{employeeId} ```json { "operationId": "getEmployee", "summary": "Get an employee.", "description": "Return one employee. Private details are never included.", "tags": [ "People" ], "parameters": [ { "in": "path", "name": "employeeId", "required": true, "schema": { "type": "string", "format": "uuid" }, "example": "123e4567-e89b-12d3-a456-426614174000" }, { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "responses": { "200": { "description": "Returns the employee.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/employeeSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "employeeKindSchema": { "type": "string", "enum": [ "employee", "contractor", "intern" ] }, "employeeSchema": { "type": "object", "properties": { "accountDisabled": { "type": "boolean" }, "archivedAt": { "type": [ "string", "null" ] }, "avatarUrl": { "type": [ "string", "null" ] }, "companyName": { "type": [ "string", "null" ] }, "createdAt": { "type": "string" }, "departmentTeamId": { "anyOf": [ { "$ref": "#/components/schemas/teamIdSchema" }, { "type": "null" } ] }, "departmentTeamName": { "type": [ "string", "null" ] }, "displayName": { "type": "string" }, "endDate": { "type": "object" }, "firstName": { "type": "string" }, "hasAppAccess": { "type": "boolean" }, "id": { "type": "string", "format": "uuid" }, "jobTitle": { "type": [ "string", "null" ] }, "kind": { "$ref": "#/components/schemas/employeeKindSchema" }, "kvk": { "type": [ "string", "null" ] }, "lastName": { "type": [ "string", "null" ] }, "manager": { "anyOf": [ { "$ref": "#/components/schemas/personSummarySchema" }, { "type": "null" } ] }, "organizationId": { "type": "string", "format": "uuid" }, "reportsToEmployeeId": { "type": [ "string", "null" ], "format": "uuid" }, "scheduleCycleAnchor": { "type": "object" }, "showBirthday": { "type": "boolean" }, "showWorkAnniversary": { "type": "boolean" }, "startDate": { "type": "object" }, "status": { "$ref": "#/components/schemas/employeeStatusSchema" }, "timezone": { "type": [ "string", "null" ] }, "updatedAt": { "type": "string" }, "userId": { "type": [ "string", "null" ], "format": "uuid" }, "vatId": { "type": [ "string", "null" ] }, "workEmail": { "type": [ "string", "null" ] } }, "required": [ "accountDisabled", "archivedAt", "avatarUrl", "companyName", "createdAt", "departmentTeamId", "departmentTeamName", "displayName", "endDate", "firstName", "hasAppAccess", "id", "jobTitle", "kind", "kvk", "lastName", "manager", "organizationId", "reportsToEmployeeId", "scheduleCycleAnchor", "showBirthday", "showWorkAnniversary", "startDate", "status", "timezone", "updatedAt", "userId", "vatId", "workEmail" ] }, "employeeStatusSchema": { "type": "string", "enum": [ "onboarding", "active", "offboarding", "ended" ] }, "organizationIdSchema": { "type": "string", "format": "uuid" }, "personSummarySchema": { "type": "object", "properties": { "avatarUrl": { "type": [ "string", "null" ] }, "displayName": { "type": "string" }, "employeeId": { "type": "string", "format": "uuid" }, "jobTitle": { "type": [ "string", "null" ] }, "userId": { "type": [ "string", "null" ], "format": "uuid" } }, "required": [ "avatarUrl", "displayName", "employeeId", "jobTitle", "userId" ] }, "teamIdSchema": { "type": "string", "format": "uuid" } } ``` # Update an employee. (/docs/openapi/updateEmployee) Update employment fields such as job title, department, manager, and status. ## PATCH /v1/organizations/{organizationId}/employees/{employeeId} ```json { "operationId": "updateEmployee", "summary": "Update an employee.", "description": "Update employment fields such as job title, department, manager, and status.", "tags": [ "People" ], "parameters": [ { "in": "path", "name": "employeeId", "required": true, "schema": { "type": "string", "format": "uuid" }, "example": "123e4567-e89b-12d3-a456-426614174000" }, { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/employeeUpdateSchema" } } }, "description": "The fields to change." }, "responses": { "200": { "description": "Returns the updated employee.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/employeeSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "employeeKindSchema": { "type": "string", "enum": [ "employee", "contractor", "intern" ] }, "employeeSchema": { "type": "object", "properties": { "accountDisabled": { "type": "boolean" }, "archivedAt": { "type": [ "string", "null" ] }, "avatarUrl": { "type": [ "string", "null" ] }, "companyName": { "type": [ "string", "null" ] }, "createdAt": { "type": "string" }, "departmentTeamId": { "anyOf": [ { "$ref": "#/components/schemas/teamIdSchema" }, { "type": "null" } ] }, "departmentTeamName": { "type": [ "string", "null" ] }, "displayName": { "type": "string" }, "endDate": { "type": "object" }, "firstName": { "type": "string" }, "hasAppAccess": { "type": "boolean" }, "id": { "type": "string", "format": "uuid" }, "jobTitle": { "type": [ "string", "null" ] }, "kind": { "$ref": "#/components/schemas/employeeKindSchema" }, "kvk": { "type": [ "string", "null" ] }, "lastName": { "type": [ "string", "null" ] }, "manager": { "anyOf": [ { "$ref": "#/components/schemas/personSummarySchema" }, { "type": "null" } ] }, "organizationId": { "type": "string", "format": "uuid" }, "reportsToEmployeeId": { "type": [ "string", "null" ], "format": "uuid" }, "scheduleCycleAnchor": { "type": "object" }, "showBirthday": { "type": "boolean" }, "showWorkAnniversary": { "type": "boolean" }, "startDate": { "type": "object" }, "status": { "$ref": "#/components/schemas/employeeStatusSchema" }, "timezone": { "type": [ "string", "null" ] }, "updatedAt": { "type": "string" }, "userId": { "type": [ "string", "null" ], "format": "uuid" }, "vatId": { "type": [ "string", "null" ] }, "workEmail": { "type": [ "string", "null" ] } }, "required": [ "accountDisabled", "archivedAt", "avatarUrl", "companyName", "createdAt", "departmentTeamId", "departmentTeamName", "displayName", "endDate", "firstName", "hasAppAccess", "id", "jobTitle", "kind", "kvk", "lastName", "manager", "organizationId", "reportsToEmployeeId", "scheduleCycleAnchor", "showBirthday", "showWorkAnniversary", "startDate", "status", "timezone", "updatedAt", "userId", "vatId", "workEmail" ] }, "employeeStatusSchema": { "type": "string", "enum": [ "onboarding", "active", "offboarding", "ended" ] }, "employeeUpdateSchema": { "type": "object", "properties": { "companyName": { "type": [ "string", "null" ], "maxLength": 200 }, "departmentTeamId": { "anyOf": [ { "$ref": "#/components/schemas/teamIdSchema" }, { "type": "null" } ] }, "endDate": { "type": "object" }, "firstName": { "type": "string", "minLength": 1, "maxLength": 120 }, "jobTitle": { "type": [ "string", "null" ], "maxLength": 160 }, "kind": { "type": "string", "enum": [ "employee", "contractor", "intern" ], "default": "employee" }, "kvk": { "type": [ "string", "null" ], "maxLength": 32 }, "lastName": { "type": [ "string", "null" ], "maxLength": 120 }, "reportsToEmployeeId": { "type": [ "string", "null" ], "format": "uuid" }, "showBirthday": { "type": "boolean" }, "showWorkAnniversary": { "type": "boolean" }, "startDate": { "type": "object" }, "status": { "allOf": [ { "$ref": "#/components/schemas/employeeStatusSchema" } ] }, "timezone": { "type": [ "string", "null" ], "maxLength": 64 }, "vatId": { "type": [ "string", "null" ], "maxLength": 32 }, "workEmail": { "anyOf": [ { "type": [ "string", "null" ], "format": "email", "maxLength": 320 }, { "type": "string", "enum": [ "" ] } ] }, "archived": { "type": "boolean" } }, "required": [] }, "organizationIdSchema": { "type": "string", "format": "uuid" }, "personSummarySchema": { "type": "object", "properties": { "avatarUrl": { "type": [ "string", "null" ] }, "displayName": { "type": "string" }, "employeeId": { "type": "string", "format": "uuid" }, "jobTitle": { "type": [ "string", "null" ] }, "userId": { "type": [ "string", "null" ], "format": "uuid" } }, "required": [ "avatarUrl", "displayName", "employeeId", "jobTitle", "userId" ] }, "teamIdSchema": { "type": "string", "format": "uuid" } } ``` # Validate a promotion. (/docs/openapi/validatePromotion) Check whether a promotion or code can be used by a customer on a quote, order, or invoice, without redeeming it. Pass the subtotal and quantity to check minimums. ## POST /v1/organizations/{organizationId}/promotions/validate ```json { "operationId": "validatePromotion", "summary": "Validate a promotion.", "description": "Check whether a promotion or code can be used by a customer on a quote, order, or invoice, without redeeming it. Pass the subtotal and quantity to check minimums.", "tags": [ "Promotions" ], "parameters": [ { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/promotionEvaluationInputSchema" } } }, "description": "The promotion or code and the customer." }, "responses": { "200": { "description": "Returns whether the promotion applies, the reason when not, and the promotion.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/promotionEvaluationSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "numeric": { "type": "number" }, "organizationIdSchema": { "type": "string", "format": "uuid" }, "promotionAppliesToSchema": { "type": "string", "enum": [ "document", "lines" ] }, "promotionEvaluationInputSchema": { "type": "object", "properties": { "code": { "type": [ "string", "null" ], "minLength": 1, "maxLength": 40 }, "customerId": { "type": "string", "format": "uuid" }, "promotionId": { "type": [ "string", "null" ], "format": "uuid" }, "quantity": { "type": [ "number", "null" ] }, "subjectId": { "type": [ "string", "null" ], "format": "uuid" }, "subjectType": { "$ref": "#/components/schemas/promotionSubjectTypeSchema" }, "subtotal": { "type": [ "number", "null" ] } }, "required": [ "customerId", "subjectType" ] }, "promotionEvaluationSchema": { "type": "object", "properties": { "promotion": { "anyOf": [ { "$ref": "#/components/schemas/promotionSummarySchema" }, { "type": "null" } ] }, "reason": { "anyOf": [ { "$ref": "#/components/schemas/promotionRejectionSchema" }, { "type": "null" } ] }, "valid": { "type": "boolean" } }, "required": [ "promotion", "reason", "valid" ] }, "promotionKindSchema": { "type": "string", "enum": [ "percentage", "fixed_amount", "free_shipping" ] }, "promotionRejectionSchema": { "type": "string", "enum": [ "not_found", "inactive", "not_started", "expired", "surface", "ineligible", "min_subtotal", "min_quantity", "limit_total", "limit_customer" ] }, "promotionSubjectTypeSchema": { "type": "string", "enum": [ "quote", "order", "invoice" ] }, "promotionSummarySchema": { "type": "object", "properties": { "appliesTo": { "$ref": "#/components/schemas/promotionAppliesToSchema" }, "code": { "type": [ "string", "null" ] }, "currency": { "type": "string" }, "id": { "type": "string", "format": "uuid" }, "kind": { "$ref": "#/components/schemas/promotionKindSchema" }, "name": { "type": "string" }, "value": { "$ref": "#/components/schemas/numeric" } }, "required": [ "appliesTo", "code", "currency", "id", "kind", "name", "value" ] } } ``` # List purchase orders. (/docs/openapi/listPurchaseOrders) Return purchase orders to suppliers, newest first. ## GET /v1/organizations/{organizationId}/purchase-orders ```json { "operationId": "listPurchaseOrders", "summary": "List purchase orders.", "description": "Return purchase orders to suppliers, newest first.", "tags": [ "Purchase orders" ], "parameters": [ { "in": "query", "name": "limit", "required": true, "schema": { "default": 50, "type": "integer", "minimum": 1, "maximum": 100 }, "example": 1 }, { "in": "query", "name": "offset", "required": true, "schema": { "default": 0, "type": "integer", "minimum": 0, "maximum": 9007199254740991 }, "example": 1 }, { "in": "query", "name": "q", "required": false, "schema": { "type": "string", "maxLength": 200 }, "example": "example" }, { "in": "query", "name": "status", "required": false, "schema": { "allOf": [ { "$ref": "#/components/schemas/purchaseOrderStatusSchema" } ] }, "example": "example" }, { "in": "query", "name": "supplierId", "required": false, "schema": { "type": "string", "format": "uuid" }, "example": "123e4567-e89b-12d3-a456-426614174000" }, { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "responses": { "200": { "description": "Returns purchase orders.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/purchaseOrdersResponseSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "isoDateSchema": { "type": "string", "format": "date" }, "listPaginationSchema": { "type": "object", "properties": { "hasMore": { "type": "boolean" }, "limit": { "type": "integer", "minimum": 1, "maximum": 100 }, "offset": { "type": "integer", "minimum": 0 }, "total": { "type": "integer", "minimum": 0 }, "totalIsEstimate": { "type": "boolean" } }, "required": [ "hasMore", "limit", "offset", "total" ] }, "organizationIdSchema": { "type": "string", "format": "uuid" }, "purchaseOrderStatusSchema": { "type": "string", "enum": [ "draft", "sent", "partially_received", "received", "closed", "canceled" ] }, "purchaseOrderSummarySchema": { "type": "object", "properties": { "canceledAt": { "type": [ "string", "null" ] }, "closedAt": { "type": [ "string", "null" ] }, "createdAt": { "type": "string" }, "createdBy": { "type": [ "string", "null" ], "format": "uuid" }, "currency": { "type": "string" }, "exchangeRate": { "type": "number" }, "expectedAt": { "anyOf": [ { "$ref": "#/components/schemas/isoDateSchema" }, { "type": "null" } ] }, "formattedNumber": { "type": "string" }, "id": { "type": "string", "format": "uuid" }, "lineCount": { "type": "integer" }, "number": { "type": "integer" }, "receivedAt": { "type": [ "string", "null" ] }, "receivedRatio": { "type": "number" }, "sentAt": { "type": [ "string", "null" ] }, "shipToLocationId": { "type": [ "string", "null" ], "format": "uuid" }, "shipToLocationName": { "type": [ "string", "null" ] }, "status": { "$ref": "#/components/schemas/purchaseOrderStatusSchema" }, "subtotalExcl": { "type": "number" }, "supplier": { "$ref": "#/components/schemas/purchaseOrderSupplierSchema" }, "supplierReference": { "type": [ "string", "null" ] }, "templateId": { "type": [ "string", "null" ], "format": "uuid" }, "totalIncl": { "type": "number" }, "updatedAt": { "type": "string" }, "vatTotal": { "type": "number" } }, "required": [ "canceledAt", "closedAt", "createdAt", "createdBy", "currency", "exchangeRate", "expectedAt", "formattedNumber", "id", "lineCount", "number", "receivedAt", "receivedRatio", "sentAt", "shipToLocationId", "shipToLocationName", "status", "subtotalExcl", "supplier", "supplierReference", "templateId", "totalIncl", "updatedAt", "vatTotal" ] }, "purchaseOrderSupplierSchema": { "type": "object", "properties": { "email": { "type": [ "string", "null" ] }, "id": { "type": "string", "format": "uuid" }, "name": { "type": "string" }, "orderingEmail": { "type": [ "string", "null" ] } }, "required": [ "email", "id", "name", "orderingEmail" ] }, "purchaseOrdersResponseSchema": { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/components/schemas/purchaseOrderSummarySchema" } }, "pagination": { "$ref": "#/components/schemas/listPaginationSchema" } }, "required": [ "data", "pagination" ] } } ``` # Create a purchase order. (/docs/openapi/createPurchaseOrder) Create a draft purchase order for a supplier. Sending and emailing it happens in the app. ## POST /v1/organizations/{organizationId}/purchase-orders ```json { "operationId": "createPurchaseOrder", "summary": "Create a purchase order.", "description": "Create a draft purchase order for a supplier. Sending and emailing it happens in the app.", "tags": [ "Purchase orders" ], "parameters": [ { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/purchaseOrderInputSchema" } } }, "description": "The supplier and variant lines." }, "responses": { "201": { "description": "Returns the draft purchase order.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/purchaseOrderSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "currencySchema": { "type": "string", "pattern": "^[A-Z]{3}$" }, "isoDateSchema": { "type": "string", "format": "date" }, "organizationIdSchema": { "type": "string", "format": "uuid" }, "purchaseOrderInputSchema": { "type": "object", "properties": { "currency": { "allOf": [ { "$ref": "#/components/schemas/currencySchema" } ] }, "exchangeRate": { "type": "number", "exclusiveMinimum": 0, "maximum": 1000000 }, "expectedAt": { "anyOf": [ { "$ref": "#/components/schemas/isoDateSchema" }, { "type": "null" } ] }, "lines": { "type": "array", "items": { "$ref": "#/components/schemas/purchaseOrderLineInputSchema" }, "maxItems": 500 }, "notes": { "type": [ "string", "null" ], "maxLength": 4000 }, "shipToLocationId": { "type": [ "string", "null" ], "format": "uuid" }, "supplierId": { "type": "string", "format": "uuid" }, "supplierNotes": { "type": [ "string", "null" ], "maxLength": 4000 }, "supplierReference": { "type": [ "string", "null" ], "maxLength": 120 }, "templateId": { "type": [ "string", "null" ], "format": "uuid" } }, "required": [ "lines", "notes", "supplierId", "supplierNotes", "supplierReference" ] }, "purchaseOrderLineInputSchema": { "type": "object", "properties": { "expectedAt": { "anyOf": [ { "$ref": "#/components/schemas/isoDateSchema" }, { "type": "null" } ] }, "productSupplierLinkId": { "type": [ "string", "null" ], "format": "uuid" }, "quantityOrdered": { "type": "number", "exclusiveMinimum": 0, "maximum": 1000000 }, "supplierSku": { "type": [ "string", "null" ], "maxLength": 120 }, "title": { "type": [ "string", "null" ], "maxLength": 300 }, "unitCost": { "type": "number", "minimum": 0, "maximum": 100000000 }, "variantId": { "type": "string", "format": "uuid" }, "vatRate": { "type": "number", "minimum": 0, "maximum": 100 } }, "required": [ "quantityOrdered", "supplierSku", "title", "unitCost", "variantId", "vatRate" ] }, "purchaseOrderLineSchema": { "type": "object", "properties": { "expectedAt": { "anyOf": [ { "$ref": "#/components/schemas/isoDateSchema" }, { "type": "null" } ] }, "id": { "type": "string", "format": "uuid" }, "position": { "type": "integer" }, "productId": { "type": "string", "format": "uuid" }, "productName": { "type": "string" }, "productSupplierLinkId": { "type": [ "string", "null" ], "format": "uuid" }, "quantityCanceled": { "type": "number" }, "quantityOpen": { "type": "number" }, "quantityOrdered": { "type": "number" }, "quantityReceived": { "type": "number" }, "sku": { "type": [ "string", "null" ] }, "supplierSku": { "type": [ "string", "null" ] }, "title": { "type": "string" }, "unitCost": { "type": "number" }, "variantId": { "type": "string", "format": "uuid" }, "variantTitle": { "type": [ "string", "null" ] }, "vatRate": { "type": "number" } }, "required": [ "expectedAt", "id", "position", "productId", "productName", "productSupplierLinkId", "quantityCanceled", "quantityOpen", "quantityOrdered", "quantityReceived", "sku", "supplierSku", "title", "unitCost", "variantId", "variantTitle", "vatRate" ] }, "purchaseOrderSchema": { "type": "object", "properties": { "canceledAt": { "type": [ "string", "null" ] }, "closedAt": { "type": [ "string", "null" ] }, "createdAt": { "type": "string" }, "createdBy": { "type": [ "string", "null" ], "format": "uuid" }, "currency": { "type": "string" }, "exchangeRate": { "type": "number" }, "expectedAt": { "anyOf": [ { "$ref": "#/components/schemas/isoDateSchema" }, { "type": "null" } ] }, "formattedNumber": { "type": "string" }, "id": { "type": "string", "format": "uuid" }, "lineCount": { "type": "integer" }, "number": { "type": "integer" }, "receivedAt": { "type": [ "string", "null" ] }, "receivedRatio": { "type": "number" }, "sentAt": { "type": [ "string", "null" ] }, "shipToLocationId": { "type": [ "string", "null" ], "format": "uuid" }, "shipToLocationName": { "type": [ "string", "null" ] }, "status": { "$ref": "#/components/schemas/purchaseOrderStatusSchema" }, "subtotalExcl": { "type": "number" }, "supplier": { "$ref": "#/components/schemas/purchaseOrderSupplierSchema" }, "supplierReference": { "type": [ "string", "null" ] }, "templateId": { "type": [ "string", "null" ], "format": "uuid" }, "totalIncl": { "type": "number" }, "updatedAt": { "type": "string" }, "vatTotal": { "type": "number" }, "lines": { "type": "array", "items": { "$ref": "#/components/schemas/purchaseOrderLineSchema" } }, "notes": { "type": [ "string", "null" ] }, "supplierNotes": { "type": [ "string", "null" ] } }, "required": [ "canceledAt", "closedAt", "createdAt", "createdBy", "currency", "exchangeRate", "expectedAt", "formattedNumber", "id", "lineCount", "number", "receivedAt", "receivedRatio", "sentAt", "shipToLocationId", "shipToLocationName", "status", "subtotalExcl", "supplier", "supplierReference", "templateId", "totalIncl", "updatedAt", "vatTotal", "lines", "notes", "supplierNotes" ] }, "purchaseOrderStatusSchema": { "type": "string", "enum": [ "draft", "sent", "partially_received", "received", "closed", "canceled" ] }, "purchaseOrderSupplierSchema": { "type": "object", "properties": { "email": { "type": [ "string", "null" ] }, "id": { "type": "string", "format": "uuid" }, "name": { "type": "string" }, "orderingEmail": { "type": [ "string", "null" ] } }, "required": [ "email", "id", "name", "orderingEmail" ] } } ``` # Get a purchase order. (/docs/openapi/getPurchaseOrder) Return one purchase order with its lines and what was received so far. ## GET /v1/organizations/{organizationId}/purchase-orders/{purchaseOrderId} ```json { "operationId": "getPurchaseOrder", "summary": "Get a purchase order.", "description": "Return one purchase order with its lines and what was received so far.", "tags": [ "Purchase orders" ], "parameters": [ { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" }, { "in": "path", "name": "purchaseOrderId", "required": true, "schema": { "type": "string", "format": "uuid" }, "example": "123e4567-e89b-12d3-a456-426614174000" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "responses": { "200": { "description": "Returns the purchase order.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/purchaseOrderSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "isoDateSchema": { "type": "string", "format": "date" }, "organizationIdSchema": { "type": "string", "format": "uuid" }, "purchaseOrderLineSchema": { "type": "object", "properties": { "expectedAt": { "anyOf": [ { "$ref": "#/components/schemas/isoDateSchema" }, { "type": "null" } ] }, "id": { "type": "string", "format": "uuid" }, "position": { "type": "integer" }, "productId": { "type": "string", "format": "uuid" }, "productName": { "type": "string" }, "productSupplierLinkId": { "type": [ "string", "null" ], "format": "uuid" }, "quantityCanceled": { "type": "number" }, "quantityOpen": { "type": "number" }, "quantityOrdered": { "type": "number" }, "quantityReceived": { "type": "number" }, "sku": { "type": [ "string", "null" ] }, "supplierSku": { "type": [ "string", "null" ] }, "title": { "type": "string" }, "unitCost": { "type": "number" }, "variantId": { "type": "string", "format": "uuid" }, "variantTitle": { "type": [ "string", "null" ] }, "vatRate": { "type": "number" } }, "required": [ "expectedAt", "id", "position", "productId", "productName", "productSupplierLinkId", "quantityCanceled", "quantityOpen", "quantityOrdered", "quantityReceived", "sku", "supplierSku", "title", "unitCost", "variantId", "variantTitle", "vatRate" ] }, "purchaseOrderSchema": { "type": "object", "properties": { "canceledAt": { "type": [ "string", "null" ] }, "closedAt": { "type": [ "string", "null" ] }, "createdAt": { "type": "string" }, "createdBy": { "type": [ "string", "null" ], "format": "uuid" }, "currency": { "type": "string" }, "exchangeRate": { "type": "number" }, "expectedAt": { "anyOf": [ { "$ref": "#/components/schemas/isoDateSchema" }, { "type": "null" } ] }, "formattedNumber": { "type": "string" }, "id": { "type": "string", "format": "uuid" }, "lineCount": { "type": "integer" }, "number": { "type": "integer" }, "receivedAt": { "type": [ "string", "null" ] }, "receivedRatio": { "type": "number" }, "sentAt": { "type": [ "string", "null" ] }, "shipToLocationId": { "type": [ "string", "null" ], "format": "uuid" }, "shipToLocationName": { "type": [ "string", "null" ] }, "status": { "$ref": "#/components/schemas/purchaseOrderStatusSchema" }, "subtotalExcl": { "type": "number" }, "supplier": { "$ref": "#/components/schemas/purchaseOrderSupplierSchema" }, "supplierReference": { "type": [ "string", "null" ] }, "templateId": { "type": [ "string", "null" ], "format": "uuid" }, "totalIncl": { "type": "number" }, "updatedAt": { "type": "string" }, "vatTotal": { "type": "number" }, "lines": { "type": "array", "items": { "$ref": "#/components/schemas/purchaseOrderLineSchema" } }, "notes": { "type": [ "string", "null" ] }, "supplierNotes": { "type": [ "string", "null" ] } }, "required": [ "canceledAt", "closedAt", "createdAt", "createdBy", "currency", "exchangeRate", "expectedAt", "formattedNumber", "id", "lineCount", "number", "receivedAt", "receivedRatio", "sentAt", "shipToLocationId", "shipToLocationName", "status", "subtotalExcl", "supplier", "supplierReference", "templateId", "totalIncl", "updatedAt", "vatTotal", "lines", "notes", "supplierNotes" ] }, "purchaseOrderStatusSchema": { "type": "string", "enum": [ "draft", "sent", "partially_received", "received", "closed", "canceled" ] }, "purchaseOrderSupplierSchema": { "type": "object", "properties": { "email": { "type": [ "string", "null" ] }, "id": { "type": "string", "format": "uuid" }, "name": { "type": "string" }, "orderingEmail": { "type": [ "string", "null" ] } }, "required": [ "email", "id", "name", "orderingEmail" ] } } ``` # Receive purchase order lines. (/docs/openapi/receivePurchaseOrder) Book received quantities into stock. Average cost updates and waiting backorders are filled. Returns 409 `APPROVAL_REQUIRED` with an `approvalId` when an approval policy wants a decision first. ## POST /v1/organizations/{organizationId}/purchase-orders/{purchaseOrderId}/receive ```json { "operationId": "receivePurchaseOrder", "summary": "Receive purchase order lines.", "description": "Book received quantities into stock. Average cost updates and waiting backorders are filled. Returns 409 `APPROVAL_REQUIRED` with an `approvalId` when an approval policy wants a decision first.", "tags": [ "Purchase orders" ], "parameters": [ { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" }, { "in": "path", "name": "purchaseOrderId", "required": true, "schema": { "type": "string", "format": "uuid" }, "example": "123e4567-e89b-12d3-a456-426614174000" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/purchaseOrderReceiptInputSchema" } } }, "description": "The received quantity per line." }, "responses": { "200": { "description": "Returns the new status and what was received.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/purchaseOrderReceiptResultSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "isoDateSchema": { "type": "string", "format": "date" }, "organizationIdSchema": { "type": "string", "format": "uuid" }, "purchaseOrderReceiptInputSchema": { "type": "object", "properties": { "allowOverReceipt": { "type": "boolean" }, "lines": { "type": "array", "items": { "$ref": "#/components/schemas/purchaseOrderReceiptLineSchema" }, "minItems": 1, "maxItems": 500 }, "locationId": { "type": [ "string", "null" ], "format": "uuid" } }, "required": [ "lines" ] }, "purchaseOrderReceiptLineSchema": { "type": "object", "properties": { "binLocation": { "type": [ "string", "null" ], "maxLength": 120 }, "expiresOn": { "anyOf": [ { "$ref": "#/components/schemas/isoDateSchema" }, { "type": "null" } ] }, "lineId": { "type": "string", "format": "uuid" }, "locationId": { "type": [ "string", "null" ], "format": "uuid" }, "lotNumber": { "type": [ "string", "null" ], "maxLength": 120 }, "quantity": { "type": "number", "exclusiveMinimum": 0, "maximum": 1000000 }, "serialNumber": { "type": [ "string", "null" ], "maxLength": 120 } }, "required": [ "binLocation", "lineId", "lotNumber", "quantity", "serialNumber" ] }, "purchaseOrderReceiptResultSchema": { "type": "object", "properties": { "received": { "type": "number" }, "status": { "$ref": "#/components/schemas/purchaseOrderStatusSchema" }, "variantIds": { "type": "array", "items": { "type": "string", "format": "uuid" } } }, "required": [ "received", "status", "variantIds" ] }, "purchaseOrderStatusSchema": { "type": "string", "enum": [ "draft", "sent", "partially_received", "received", "closed", "canceled" ] } } ``` # List quotes. (/docs/openapi/listQuotes) Return quotes for an organization the authenticated user can access. ## GET /v1/organizations/{organizationId}/quotes ```json { "operationId": "listQuotes", "summary": "List quotes.", "description": "Return quotes for an organization the authenticated user can access.", "tags": [ "Quotes" ], "parameters": [ { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "responses": { "200": { "description": "Returns paginated quotes.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/quotesListResponseSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "organizationIdSchema": { "type": "string", "format": "uuid" }, "quotesListResponseSchema": { "type": "object" } } ``` # Create a quote. (/docs/openapi/createQuote) Create a quote for the specified organization. ## POST /v1/organizations/{organizationId}/quotes ```json { "operationId": "createQuote", "summary": "Create a quote.", "description": "Create a quote for the specified organization.", "tags": [ "Quotes" ], "parameters": [ { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/quoteInputSchema" } } }, "description": "The quote payload.", "required": true }, "responses": { "201": { "description": "Returns the created quote.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/quoteSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "discountDescriptionSchema": { "type": [ "string", "null" ], "maxLength": 500 }, "discountKindSchema": { "type": "string", "enum": [ "none", "percentage", "fixed" ] }, "documentVatRegimeSchema": { "type": "string", "enum": [ "standard", "reverse_charge", "exempt", "kor", "margin_scheme" ] }, "isoDateOrNull": { "type": [ "string", "null" ], "pattern": "^\\d{4}-\\d{2}-\\d{2}$" }, "optionalUrl": { "type": [ "string", "null" ], "maxLength": 2000 }, "organizationIdSchema": { "type": "string", "format": "uuid" }, "quoteCurrencyCodeSchema": { "type": "string", "pattern": "^[A-Z]{3}$" }, "quoteDepositKindSchema": { "type": "string", "enum": [ "none", "percent", "fixed" ] }, "quoteDisplayNumberSchema": { "type": "string", "minLength": 1, "maxLength": 40 }, "quoteIdSchema": { "type": "string", "format": "uuid" }, "quoteInputSchema": { "type": "object", "properties": { "customerId": { "type": "string", "format": "uuid" }, "title": { "type": "string", "minLength": 1, "maxLength": 240 }, "quoteTemplateId": { "type": [ "string", "null" ], "format": "uuid" }, "intro": { "type": [ "string", "null" ], "maxLength": 20000 }, "terms": { "type": [ "string", "null" ], "maxLength": 20000 }, "validUntil": { "allOf": [ { "$ref": "#/components/schemas/isoDateOrNull" } ] }, "currency": { "allOf": [ { "$ref": "#/components/schemas/quoteCurrencyCodeSchema" } ] }, "vatInclusive": { "type": "boolean" }, "discountKind": { "allOf": [ { "$ref": "#/components/schemas/discountKindSchema" } ] }, "discountValue": { "type": "number", "minimum": 0 }, "discountDescription": { "allOf": [ { "$ref": "#/components/schemas/discountDescriptionSchema" } ] }, "promotionId": { "type": [ "string", "null" ], "format": "uuid" }, "publicMessage": { "type": [ "string", "null" ], "maxLength": 20000 }, "issueDate": { "allOf": [ { "$ref": "#/components/schemas/isoDateOrNull" } ] }, "vatRegime": { "allOf": [ { "$ref": "#/components/schemas/documentVatRegimeSchema" } ] }, "termsPdfFileId": { "type": [ "string", "null" ], "format": "uuid" }, "termsPdfVersionId": { "type": [ "string", "null" ], "format": "uuid" }, "termsRichText": { "type": [ "string", "null" ], "maxLength": 100000 }, "termsUrl": { "allOf": [ { "$ref": "#/components/schemas/optionalUrl" } ] }, "depositKind": { "allOf": [ { "$ref": "#/components/schemas/quoteDepositKindSchema" } ] }, "depositValue": { "type": "number", "minimum": 0 }, "depositRequiredToAccept": { "type": "boolean" }, "lines": { "type": "array", "items": { "$ref": "#/components/schemas/quoteLineInputSchema" }, "maxItems": 500 }, "linkedTaskIds": { "type": "array", "items": { "type": "string", "format": "uuid" }, "maxItems": 200 } }, "required": [ "customerId", "title" ] }, "quoteLineIdSchema": { "type": "string", "format": "uuid" }, "quoteLineInputSchema": { "type": "object", "properties": { "description": { "type": [ "string", "null" ], "maxLength": 20000 }, "discountKind": { "allOf": [ { "$ref": "#/components/schemas/discountKindSchema" } ] }, "discountValue": { "type": "number", "minimum": 0 }, "id": { "allOf": [ { "$ref": "#/components/schemas/quoteLineIdSchema" } ] }, "isOptional": { "type": "boolean" }, "kind": { "allOf": [ { "$ref": "#/components/schemas/quoteLineKindSchema" } ] }, "lineType": { "anyOf": [ { "$ref": "#/components/schemas/quoteLineTypeSchema" }, { "type": "null" } ] }, "position": { "type": "number" }, "pricingMode": { "allOf": [ { "$ref": "#/components/schemas/quoteLinePricingModeSchema" } ] }, "productId": { "type": [ "string", "null" ], "format": "uuid" }, "promotionId": { "type": [ "string", "null" ], "format": "uuid" }, "quantity": { "type": "number", "minimum": 0 }, "stockMode": { "allOf": [ { "$ref": "#/components/schemas/quoteLineStockModeSchema" } ] }, "subscriptionInterval": { "anyOf": [ { "$ref": "#/components/schemas/quoteSubscriptionIntervalSchema" }, { "type": "null" } ] }, "subscriptionPeriods": { "type": [ "integer", "null" ], "exclusiveMinimum": 0 }, "taskId": { "type": [ "string", "null" ], "format": "uuid" }, "title": { "type": "string", "minLength": 1, "maxLength": 500 }, "unit": { "type": [ "string", "null" ], "maxLength": 40 }, "unitPrice": { "type": "number" }, "variantId": { "type": [ "string", "null" ], "format": "uuid" }, "vatRate": { "type": "number", "minimum": 0, "maximum": 100 } }, "required": [ "title" ] }, "quoteLineKindSchema": { "type": "string", "enum": [ "line", "section_header" ] }, "quoteLinePricingModeSchema": { "type": "string", "enum": [ "unit", "hourly", "subscription", "fixed" ] }, "quoteLineStockModeSchema": { "type": "string", "enum": [ "auto", "none" ] }, "quoteLineTypeSchema": { "type": "string", "enum": [ "product", "labor", "custom" ] }, "quoteNumberPrefixSchema": { "type": "string", "maxLength": 16, "pattern": "^[A-Z0-9-]*$" }, "quoteNumberSchema": { "type": "integer", "exclusiveMinimum": 0, "maximum": 9007199254740991 }, "quoteSchema": { "type": "object", "properties": { "id": { "$ref": "#/components/schemas/quoteIdSchema" }, "organizationId": { "type": "string", "format": "uuid" }, "customerId": { "type": "string", "format": "uuid" }, "number": { "$ref": "#/components/schemas/quoteNumberSchema" }, "numberPrefix": { "$ref": "#/components/schemas/quoteNumberPrefixSchema" }, "displayNumber": { "$ref": "#/components/schemas/quoteDisplayNumberSchema" }, "version": { "type": "integer", "minimum": 1 }, "status": { "$ref": "#/components/schemas/quoteStatusSchema" }, "quoteTemplateId": { "type": [ "string", "null" ], "format": "uuid" }, "title": { "type": "string" }, "intro": { "type": [ "string", "null" ] }, "terms": { "type": [ "string", "null" ] }, "validUntil": { "type": [ "string", "null" ] }, "currency": { "$ref": "#/components/schemas/quoteCurrencyCodeSchema" }, "vatInclusive": { "type": "boolean" }, "discountKind": { "$ref": "#/components/schemas/discountKindSchema" }, "discountValue": { "type": "number" }, "discountDescription": { "$ref": "#/components/schemas/discountDescriptionSchema" }, "promotionId": { "type": [ "string", "null" ], "format": "uuid" }, "subtotalExcl": { "type": "number" }, "vatTotal": { "type": "number" }, "totalIncl": { "type": "number" }, "publicMessage": { "type": [ "string", "null" ] }, "sentAt": { "type": [ "string", "null" ], "format": "date-time" }, "acceptedAt": { "type": [ "string", "null" ], "format": "date-time" }, "rejectedAt": { "type": [ "string", "null" ], "format": "date-time" }, "expiredAt": { "type": [ "string", "null" ], "format": "date-time" }, "withdrawnAt": { "type": [ "string", "null" ], "format": "date-time" }, "hasUnpublishedChanges": { "type": "boolean" }, "acceptedByName": { "type": [ "string", "null" ] }, "acceptedByEmail": { "type": [ "string", "null" ] }, "acceptedIp": { "type": [ "string", "null" ] }, "acceptedUserAgent": { "type": [ "string", "null" ] }, "acceptedOptionalLineIds": { "type": "array", "items": { "type": "string", "format": "uuid" } }, "archivedAt": { "type": [ "string", "null" ], "format": "date-time" }, "issueDate": { "type": [ "string", "null" ] }, "vatRegime": { "$ref": "#/components/schemas/documentVatRegimeSchema" }, "sellerName": { "type": [ "string", "null" ] }, "sellerAddressLine1": { "type": [ "string", "null" ] }, "sellerAddressLine2": { "type": [ "string", "null" ] }, "sellerAddressPostalCode": { "type": [ "string", "null" ] }, "sellerAddressCity": { "type": [ "string", "null" ] }, "sellerAddressCountry": { "type": [ "string", "null" ] }, "sellerEmail": { "type": [ "string", "null" ] }, "sellerPhone": { "type": [ "string", "null" ] }, "sellerKvk": { "type": [ "string", "null" ] }, "sellerBtw": { "type": [ "string", "null" ] }, "sellerIban": { "type": [ "string", "null" ] }, "buyerName": { "type": [ "string", "null" ] }, "buyerAddressLine1": { "type": [ "string", "null" ] }, "buyerAddressLine2": { "type": [ "string", "null" ] }, "buyerAddressPostalCode": { "type": [ "string", "null" ] }, "buyerAddressCity": { "type": [ "string", "null" ] }, "buyerAddressCountry": { "type": [ "string", "null" ] }, "buyerEmail": { "type": [ "string", "null" ] }, "buyerKvk": { "type": [ "string", "null" ] }, "buyerBtw": { "type": [ "string", "null" ] }, "termsPdfFileId": { "type": [ "string", "null" ], "format": "uuid" }, "termsPdfVersionId": { "type": [ "string", "null" ], "format": "uuid" }, "termsRichText": { "type": [ "string", "null" ], "maxLength": 100000 }, "termsUrl": { "type": [ "string", "null" ] }, "depositKind": { "$ref": "#/components/schemas/quoteDepositKindSchema" }, "depositRequiredToAccept": { "type": "boolean" }, "depositValue": { "type": "number", "minimum": 0 }, "changesRequestedAt": { "type": [ "string", "null" ], "format": "date-time" }, "createdBy": { "type": [ "string", "null" ], "format": "uuid" }, "updatedBy": { "type": [ "string", "null" ], "format": "uuid" }, "createdAt": { "type": "string", "format": "date-time" }, "updatedAt": { "type": "string", "format": "date-time" } }, "required": [ "id", "organizationId", "customerId", "number", "numberPrefix", "displayNumber", "version", "status", "quoteTemplateId", "title", "intro", "terms", "validUntil", "currency", "vatInclusive", "discountKind", "discountValue", "discountDescription", "promotionId", "subtotalExcl", "vatTotal", "totalIncl", "publicMessage", "sentAt", "acceptedAt", "rejectedAt", "expiredAt", "withdrawnAt", "hasUnpublishedChanges", "acceptedByName", "acceptedByEmail", "acceptedIp", "acceptedUserAgent", "acceptedOptionalLineIds", "archivedAt", "issueDate", "vatRegime", "sellerName", "sellerAddressLine1", "sellerAddressLine2", "sellerAddressPostalCode", "sellerAddressCity", "sellerAddressCountry", "sellerEmail", "sellerPhone", "sellerKvk", "sellerBtw", "sellerIban", "buyerName", "buyerAddressLine1", "buyerAddressLine2", "buyerAddressPostalCode", "buyerAddressCity", "buyerAddressCountry", "buyerEmail", "buyerKvk", "buyerBtw", "termsPdfFileId", "termsPdfVersionId", "termsRichText", "termsUrl", "depositKind", "depositRequiredToAccept", "depositValue", "changesRequestedAt", "createdBy", "updatedBy", "createdAt", "updatedAt" ] }, "quoteStatusSchema": { "type": "string", "enum": [ "draft", "sent", "accepted", "rejected", "expired", "withdrawn", "changes_requested" ] }, "quoteSubscriptionIntervalSchema": { "type": "string", "enum": [ "weekly", "monthly", "quarterly", "yearly" ] } } ``` # Get a quote. (/docs/openapi/getQuote) Return a quote detail record for the specified organization. ## GET /v1/organizations/{organizationId}/quotes/{quoteId} ```json { "operationId": "getQuote", "summary": "Get a quote.", "description": "Return a quote detail record for the specified organization.", "tags": [ "Quotes" ], "parameters": [ { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" }, { "in": "path", "name": "quoteId", "required": true, "schema": { "$ref": "#/components/schemas/quoteIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "responses": { "200": { "description": "Returns the requested quote.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/quoteDetailSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "discountDescriptionSchema": { "type": [ "string", "null" ], "maxLength": 500 }, "discountKindSchema": { "type": "string", "enum": [ "none", "percentage", "fixed" ] }, "documentVatRegimeSchema": { "type": "string", "enum": [ "standard", "reverse_charge", "exempt", "kor", "margin_scheme" ] }, "organizationIdSchema": { "type": "string", "format": "uuid" }, "quoteChangeRequestSchema": { "type": "object", "properties": { "comment": { "type": "string" }, "createdAt": { "type": "string", "format": "date-time" }, "id": { "type": "string", "format": "uuid" }, "requestedByEmail": { "type": "string" }, "requestedByName": { "type": "string" }, "version": { "type": "integer", "minimum": 1 } }, "required": [ "comment", "createdAt", "id", "requestedByEmail", "requestedByName", "version" ] }, "quoteCurrencyCodeSchema": { "type": "string", "pattern": "^[A-Z]{3}$" }, "quoteDepositKindSchema": { "type": "string", "enum": [ "none", "percent", "fixed" ] }, "quoteDetailSchema": { "type": "object", "properties": { "changeRequests": { "type": "array", "items": { "$ref": "#/components/schemas/quoteChangeRequestSchema" } }, "lines": { "type": "array", "items": { "$ref": "#/components/schemas/quoteLineSchema" } }, "linkedAssetIds": { "type": "array", "items": { "type": "string", "format": "uuid" } }, "linkedTaskIds": { "type": "array", "items": { "type": "string", "format": "uuid" } }, "quote": { "$ref": "#/components/schemas/quoteSchema" }, "statusHistory": { "type": "array", "items": { "$ref": "#/components/schemas/quoteStatusHistoryEntrySchema" } }, "stockSummary": { "type": "array", "items": { "$ref": "#/components/schemas/quoteStockLineSummarySchema" } }, "versions": { "type": "array", "items": { "$ref": "#/components/schemas/quoteVersionSummarySchema" } } }, "required": [ "changeRequests", "lines", "linkedAssetIds", "linkedTaskIds", "quote", "statusHistory", "stockSummary", "versions" ] }, "quoteDisplayNumberSchema": { "type": "string", "minLength": 1, "maxLength": 40 }, "quoteIdSchema": { "type": "string", "format": "uuid" }, "quoteLineIdSchema": { "type": "string", "format": "uuid" }, "quoteLineKindSchema": { "type": "string", "enum": [ "line", "section_header" ] }, "quoteLinePricingModeSchema": { "type": "string", "enum": [ "unit", "hourly", "subscription", "fixed" ] }, "quoteLineSchema": { "type": "object", "properties": { "createdAt": { "type": "string", "format": "date-time" }, "description": { "type": [ "string", "null" ] }, "discountKind": { "$ref": "#/components/schemas/discountKindSchema" }, "discountValue": { "type": "number" }, "id": { "$ref": "#/components/schemas/quoteLineIdSchema" }, "isOptional": { "type": "boolean" }, "kind": { "$ref": "#/components/schemas/quoteLineKindSchema" }, "lineType": { "anyOf": [ { "$ref": "#/components/schemas/quoteLineTypeSchema" }, { "type": "null" } ] }, "organizationId": { "type": "string", "format": "uuid" }, "position": { "type": "number" }, "pricingMode": { "$ref": "#/components/schemas/quoteLinePricingModeSchema" }, "productId": { "type": [ "string", "null" ], "format": "uuid" }, "promotionId": { "type": [ "string", "null" ], "format": "uuid" }, "quantity": { "type": "number" }, "quoteId": { "$ref": "#/components/schemas/quoteIdSchema" }, "stockMode": { "$ref": "#/components/schemas/quoteLineStockModeSchema" }, "subscriptionInterval": { "anyOf": [ { "$ref": "#/components/schemas/quoteSubscriptionIntervalSchema" }, { "type": "null" } ] }, "subscriptionPeriods": { "type": [ "integer", "null" ], "exclusiveMinimum": 0 }, "taskId": { "type": [ "string", "null" ], "format": "uuid" }, "title": { "type": "string" }, "unit": { "type": [ "string", "null" ] }, "unitPrice": { "type": "number" }, "updatedAt": { "type": "string", "format": "date-time" }, "variantId": { "type": [ "string", "null" ], "format": "uuid" }, "vatRate": { "type": "number" } }, "required": [ "createdAt", "description", "discountKind", "discountValue", "id", "isOptional", "kind", "lineType", "organizationId", "position", "pricingMode", "productId", "promotionId", "quantity", "quoteId", "stockMode", "subscriptionInterval", "subscriptionPeriods", "taskId", "title", "unit", "unitPrice", "updatedAt", "variantId", "vatRate" ] }, "quoteLineStockModeSchema": { "type": "string", "enum": [ "auto", "none" ] }, "quoteLineTypeSchema": { "type": "string", "enum": [ "product", "labor", "custom" ] }, "quoteNumberPrefixSchema": { "type": "string", "maxLength": 16, "pattern": "^[A-Z0-9-]*$" }, "quoteNumberSchema": { "type": "integer", "exclusiveMinimum": 0, "maximum": 9007199254740991 }, "quoteSchema": { "type": "object", "properties": { "id": { "$ref": "#/components/schemas/quoteIdSchema" }, "organizationId": { "type": "string", "format": "uuid" }, "customerId": { "type": "string", "format": "uuid" }, "number": { "$ref": "#/components/schemas/quoteNumberSchema" }, "numberPrefix": { "$ref": "#/components/schemas/quoteNumberPrefixSchema" }, "displayNumber": { "$ref": "#/components/schemas/quoteDisplayNumberSchema" }, "version": { "type": "integer", "minimum": 1 }, "status": { "$ref": "#/components/schemas/quoteStatusSchema" }, "quoteTemplateId": { "type": [ "string", "null" ], "format": "uuid" }, "title": { "type": "string" }, "intro": { "type": [ "string", "null" ] }, "terms": { "type": [ "string", "null" ] }, "validUntil": { "type": [ "string", "null" ] }, "currency": { "$ref": "#/components/schemas/quoteCurrencyCodeSchema" }, "vatInclusive": { "type": "boolean" }, "discountKind": { "$ref": "#/components/schemas/discountKindSchema" }, "discountValue": { "type": "number" }, "discountDescription": { "$ref": "#/components/schemas/discountDescriptionSchema" }, "promotionId": { "type": [ "string", "null" ], "format": "uuid" }, "subtotalExcl": { "type": "number" }, "vatTotal": { "type": "number" }, "totalIncl": { "type": "number" }, "publicMessage": { "type": [ "string", "null" ] }, "sentAt": { "type": [ "string", "null" ], "format": "date-time" }, "acceptedAt": { "type": [ "string", "null" ], "format": "date-time" }, "rejectedAt": { "type": [ "string", "null" ], "format": "date-time" }, "expiredAt": { "type": [ "string", "null" ], "format": "date-time" }, "withdrawnAt": { "type": [ "string", "null" ], "format": "date-time" }, "hasUnpublishedChanges": { "type": "boolean" }, "acceptedByName": { "type": [ "string", "null" ] }, "acceptedByEmail": { "type": [ "string", "null" ] }, "acceptedIp": { "type": [ "string", "null" ] }, "acceptedUserAgent": { "type": [ "string", "null" ] }, "acceptedOptionalLineIds": { "type": "array", "items": { "type": "string", "format": "uuid" } }, "archivedAt": { "type": [ "string", "null" ], "format": "date-time" }, "issueDate": { "type": [ "string", "null" ] }, "vatRegime": { "$ref": "#/components/schemas/documentVatRegimeSchema" }, "sellerName": { "type": [ "string", "null" ] }, "sellerAddressLine1": { "type": [ "string", "null" ] }, "sellerAddressLine2": { "type": [ "string", "null" ] }, "sellerAddressPostalCode": { "type": [ "string", "null" ] }, "sellerAddressCity": { "type": [ "string", "null" ] }, "sellerAddressCountry": { "type": [ "string", "null" ] }, "sellerEmail": { "type": [ "string", "null" ] }, "sellerPhone": { "type": [ "string", "null" ] }, "sellerKvk": { "type": [ "string", "null" ] }, "sellerBtw": { "type": [ "string", "null" ] }, "sellerIban": { "type": [ "string", "null" ] }, "buyerName": { "type": [ "string", "null" ] }, "buyerAddressLine1": { "type": [ "string", "null" ] }, "buyerAddressLine2": { "type": [ "string", "null" ] }, "buyerAddressPostalCode": { "type": [ "string", "null" ] }, "buyerAddressCity": { "type": [ "string", "null" ] }, "buyerAddressCountry": { "type": [ "string", "null" ] }, "buyerEmail": { "type": [ "string", "null" ] }, "buyerKvk": { "type": [ "string", "null" ] }, "buyerBtw": { "type": [ "string", "null" ] }, "termsPdfFileId": { "type": [ "string", "null" ], "format": "uuid" }, "termsPdfVersionId": { "type": [ "string", "null" ], "format": "uuid" }, "termsRichText": { "type": [ "string", "null" ], "maxLength": 100000 }, "termsUrl": { "type": [ "string", "null" ] }, "depositKind": { "$ref": "#/components/schemas/quoteDepositKindSchema" }, "depositRequiredToAccept": { "type": "boolean" }, "depositValue": { "type": "number", "minimum": 0 }, "changesRequestedAt": { "type": [ "string", "null" ], "format": "date-time" }, "createdBy": { "type": [ "string", "null" ], "format": "uuid" }, "updatedBy": { "type": [ "string", "null" ], "format": "uuid" }, "createdAt": { "type": "string", "format": "date-time" }, "updatedAt": { "type": "string", "format": "date-time" } }, "required": [ "id", "organizationId", "customerId", "number", "numberPrefix", "displayNumber", "version", "status", "quoteTemplateId", "title", "intro", "terms", "validUntil", "currency", "vatInclusive", "discountKind", "discountValue", "discountDescription", "promotionId", "subtotalExcl", "vatTotal", "totalIncl", "publicMessage", "sentAt", "acceptedAt", "rejectedAt", "expiredAt", "withdrawnAt", "hasUnpublishedChanges", "acceptedByName", "acceptedByEmail", "acceptedIp", "acceptedUserAgent", "acceptedOptionalLineIds", "archivedAt", "issueDate", "vatRegime", "sellerName", "sellerAddressLine1", "sellerAddressLine2", "sellerAddressPostalCode", "sellerAddressCity", "sellerAddressCountry", "sellerEmail", "sellerPhone", "sellerKvk", "sellerBtw", "sellerIban", "buyerName", "buyerAddressLine1", "buyerAddressLine2", "buyerAddressPostalCode", "buyerAddressCity", "buyerAddressCountry", "buyerEmail", "buyerKvk", "buyerBtw", "termsPdfFileId", "termsPdfVersionId", "termsRichText", "termsUrl", "depositKind", "depositRequiredToAccept", "depositValue", "changesRequestedAt", "createdBy", "updatedBy", "createdAt", "updatedAt" ] }, "quoteStatusHistoryEntrySchema": { "type": "object", "properties": { "actorCustomerEmail": { "type": [ "string", "null" ] }, "actorCustomerName": { "type": [ "string", "null" ] }, "actorUserId": { "type": [ "string", "null" ], "format": "uuid" }, "createdAt": { "type": "string", "format": "date-time" }, "fromStatus": { "anyOf": [ { "$ref": "#/components/schemas/quoteStatusSchema" }, { "type": "null" } ] }, "id": { "type": "string", "format": "uuid" }, "ip": { "type": [ "string", "null" ] }, "organizationId": { "type": "string", "format": "uuid" }, "quoteId": { "$ref": "#/components/schemas/quoteIdSchema" }, "reason": { "type": [ "string", "null" ] }, "toStatus": { "$ref": "#/components/schemas/quoteStatusSchema" }, "userAgent": { "type": [ "string", "null" ] } }, "required": [ "actorCustomerEmail", "actorCustomerName", "actorUserId", "createdAt", "fromStatus", "id", "ip", "organizationId", "quoteId", "reason", "toStatus", "userAgent" ] }, "quoteStatusSchema": { "type": "string", "enum": [ "draft", "sent", "accepted", "rejected", "expired", "withdrawn", "changes_requested" ] }, "quoteStockLineSummarySchema": { "type": "object", "properties": { "availableQuantity": { "type": "number", "minimum": 0 }, "handledByTask": { "type": [ "object", "null" ], "properties": { "taskId": { "type": "string", "format": "uuid" }, "taskNumber": { "type": "integer", "exclusiveMinimum": 0 } }, "required": [ "taskId", "taskNumber" ] }, "heldQuantity": { "type": "number", "minimum": 0 }, "lineId": { "type": "string", "format": "uuid" }, "productId": { "type": [ "string", "null" ], "format": "uuid" }, "reserves": { "type": "boolean" }, "shortQuantity": { "type": "number", "minimum": 0 }, "tracked": { "type": "boolean" } }, "required": [ "availableQuantity", "handledByTask", "heldQuantity", "lineId", "productId", "reserves", "shortQuantity", "tracked" ] }, "quoteSubscriptionIntervalSchema": { "type": "string", "enum": [ "weekly", "monthly", "quarterly", "yearly" ] }, "quoteVersionSummarySchema": { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "publishedAt": { "type": "string", "format": "date-time" }, "reason": { "type": [ "string", "null" ] }, "status": { "$ref": "#/components/schemas/quoteStatusSchema" }, "version": { "type": "integer", "minimum": 1 } }, "required": [ "id", "publishedAt", "reason", "status", "version" ] } } ``` # Update a quote. (/docs/openapi/updateQuote) Update a quote for the specified organization. ## PATCH /v1/organizations/{organizationId}/quotes/{quoteId} ```json { "operationId": "updateQuote", "summary": "Update a quote.", "description": "Update a quote for the specified organization.", "tags": [ "Quotes" ], "parameters": [ { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" }, { "in": "path", "name": "quoteId", "required": true, "schema": { "$ref": "#/components/schemas/quoteIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/quotePatchSchema" } } }, "description": "The quote patch payload.", "required": true }, "responses": { "200": { "description": "Returns the updated quote.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/quoteSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "discountDescriptionSchema": { "type": [ "string", "null" ], "maxLength": 500 }, "discountKindSchema": { "type": "string", "enum": [ "none", "percentage", "fixed" ] }, "documentVatRegimeSchema": { "type": "string", "enum": [ "standard", "reverse_charge", "exempt", "kor", "margin_scheme" ] }, "isoDateOrNull": { "type": [ "string", "null" ], "pattern": "^\\d{4}-\\d{2}-\\d{2}$" }, "optionalUrl": { "type": [ "string", "null" ], "maxLength": 2000 }, "organizationIdSchema": { "type": "string", "format": "uuid" }, "quoteCurrencyCodeSchema": { "type": "string", "pattern": "^[A-Z]{3}$" }, "quoteDepositKindSchema": { "type": "string", "enum": [ "none", "percent", "fixed" ] }, "quoteDisplayNumberSchema": { "type": "string", "minLength": 1, "maxLength": 40 }, "quoteIdSchema": { "type": "string", "format": "uuid" }, "quoteLineIdSchema": { "type": "string", "format": "uuid" }, "quoteLineInputSchema": { "type": "object", "properties": { "description": { "type": [ "string", "null" ], "maxLength": 20000 }, "discountKind": { "allOf": [ { "$ref": "#/components/schemas/discountKindSchema" } ] }, "discountValue": { "type": "number", "minimum": 0 }, "id": { "allOf": [ { "$ref": "#/components/schemas/quoteLineIdSchema" } ] }, "isOptional": { "type": "boolean" }, "kind": { "allOf": [ { "$ref": "#/components/schemas/quoteLineKindSchema" } ] }, "lineType": { "anyOf": [ { "$ref": "#/components/schemas/quoteLineTypeSchema" }, { "type": "null" } ] }, "position": { "type": "number" }, "pricingMode": { "allOf": [ { "$ref": "#/components/schemas/quoteLinePricingModeSchema" } ] }, "productId": { "type": [ "string", "null" ], "format": "uuid" }, "promotionId": { "type": [ "string", "null" ], "format": "uuid" }, "quantity": { "type": "number", "minimum": 0 }, "stockMode": { "allOf": [ { "$ref": "#/components/schemas/quoteLineStockModeSchema" } ] }, "subscriptionInterval": { "anyOf": [ { "$ref": "#/components/schemas/quoteSubscriptionIntervalSchema" }, { "type": "null" } ] }, "subscriptionPeriods": { "type": [ "integer", "null" ], "exclusiveMinimum": 0 }, "taskId": { "type": [ "string", "null" ], "format": "uuid" }, "title": { "type": "string", "minLength": 1, "maxLength": 500 }, "unit": { "type": [ "string", "null" ], "maxLength": 40 }, "unitPrice": { "type": "number" }, "variantId": { "type": [ "string", "null" ], "format": "uuid" }, "vatRate": { "type": "number", "minimum": 0, "maximum": 100 } }, "required": [ "title" ] }, "quoteLineKindSchema": { "type": "string", "enum": [ "line", "section_header" ] }, "quoteLinePricingModeSchema": { "type": "string", "enum": [ "unit", "hourly", "subscription", "fixed" ] }, "quoteLineStockModeSchema": { "type": "string", "enum": [ "auto", "none" ] }, "quoteLineTypeSchema": { "type": "string", "enum": [ "product", "labor", "custom" ] }, "quoteNumberPrefixSchema": { "type": "string", "maxLength": 16, "pattern": "^[A-Z0-9-]*$" }, "quoteNumberSchema": { "type": "integer", "exclusiveMinimum": 0, "maximum": 9007199254740991 }, "quotePatchSchema": { "type": "object", "properties": { "customerId": { "type": "string", "format": "uuid" }, "title": { "type": "string", "minLength": 1, "maxLength": 240 }, "quoteTemplateId": { "type": [ "string", "null" ], "format": "uuid" }, "intro": { "type": [ "string", "null" ], "maxLength": 20000 }, "terms": { "type": [ "string", "null" ], "maxLength": 20000 }, "validUntil": { "allOf": [ { "$ref": "#/components/schemas/isoDateOrNull" } ] }, "currency": { "allOf": [ { "$ref": "#/components/schemas/quoteCurrencyCodeSchema" } ] }, "vatInclusive": { "type": "boolean" }, "discountKind": { "allOf": [ { "$ref": "#/components/schemas/discountKindSchema" } ] }, "discountValue": { "type": "number", "minimum": 0 }, "discountDescription": { "allOf": [ { "$ref": "#/components/schemas/discountDescriptionSchema" } ] }, "promotionId": { "type": [ "string", "null" ], "format": "uuid" }, "publicMessage": { "type": [ "string", "null" ], "maxLength": 20000 }, "issueDate": { "allOf": [ { "$ref": "#/components/schemas/isoDateOrNull" } ] }, "vatRegime": { "allOf": [ { "$ref": "#/components/schemas/documentVatRegimeSchema" } ] }, "termsPdfFileId": { "type": [ "string", "null" ], "format": "uuid" }, "termsPdfVersionId": { "type": [ "string", "null" ], "format": "uuid" }, "termsRichText": { "type": [ "string", "null" ], "maxLength": 100000 }, "termsUrl": { "allOf": [ { "$ref": "#/components/schemas/optionalUrl" } ] }, "depositKind": { "allOf": [ { "$ref": "#/components/schemas/quoteDepositKindSchema" } ] }, "depositValue": { "type": "number", "minimum": 0 }, "depositRequiredToAccept": { "type": "boolean" }, "lines": { "type": "array", "items": { "$ref": "#/components/schemas/quoteLineInputSchema" }, "maxItems": 500 }, "linkedTaskIds": { "type": "array", "items": { "type": "string", "format": "uuid" }, "maxItems": 200 } } }, "quoteSchema": { "type": "object", "properties": { "id": { "$ref": "#/components/schemas/quoteIdSchema" }, "organizationId": { "type": "string", "format": "uuid" }, "customerId": { "type": "string", "format": "uuid" }, "number": { "$ref": "#/components/schemas/quoteNumberSchema" }, "numberPrefix": { "$ref": "#/components/schemas/quoteNumberPrefixSchema" }, "displayNumber": { "$ref": "#/components/schemas/quoteDisplayNumberSchema" }, "version": { "type": "integer", "minimum": 1 }, "status": { "$ref": "#/components/schemas/quoteStatusSchema" }, "quoteTemplateId": { "type": [ "string", "null" ], "format": "uuid" }, "title": { "type": "string" }, "intro": { "type": [ "string", "null" ] }, "terms": { "type": [ "string", "null" ] }, "validUntil": { "type": [ "string", "null" ] }, "currency": { "$ref": "#/components/schemas/quoteCurrencyCodeSchema" }, "vatInclusive": { "type": "boolean" }, "discountKind": { "$ref": "#/components/schemas/discountKindSchema" }, "discountValue": { "type": "number" }, "discountDescription": { "$ref": "#/components/schemas/discountDescriptionSchema" }, "promotionId": { "type": [ "string", "null" ], "format": "uuid" }, "subtotalExcl": { "type": "number" }, "vatTotal": { "type": "number" }, "totalIncl": { "type": "number" }, "publicMessage": { "type": [ "string", "null" ] }, "sentAt": { "type": [ "string", "null" ], "format": "date-time" }, "acceptedAt": { "type": [ "string", "null" ], "format": "date-time" }, "rejectedAt": { "type": [ "string", "null" ], "format": "date-time" }, "expiredAt": { "type": [ "string", "null" ], "format": "date-time" }, "withdrawnAt": { "type": [ "string", "null" ], "format": "date-time" }, "hasUnpublishedChanges": { "type": "boolean" }, "acceptedByName": { "type": [ "string", "null" ] }, "acceptedByEmail": { "type": [ "string", "null" ] }, "acceptedIp": { "type": [ "string", "null" ] }, "acceptedUserAgent": { "type": [ "string", "null" ] }, "acceptedOptionalLineIds": { "type": "array", "items": { "type": "string", "format": "uuid" } }, "archivedAt": { "type": [ "string", "null" ], "format": "date-time" }, "issueDate": { "type": [ "string", "null" ] }, "vatRegime": { "$ref": "#/components/schemas/documentVatRegimeSchema" }, "sellerName": { "type": [ "string", "null" ] }, "sellerAddressLine1": { "type": [ "string", "null" ] }, "sellerAddressLine2": { "type": [ "string", "null" ] }, "sellerAddressPostalCode": { "type": [ "string", "null" ] }, "sellerAddressCity": { "type": [ "string", "null" ] }, "sellerAddressCountry": { "type": [ "string", "null" ] }, "sellerEmail": { "type": [ "string", "null" ] }, "sellerPhone": { "type": [ "string", "null" ] }, "sellerKvk": { "type": [ "string", "null" ] }, "sellerBtw": { "type": [ "string", "null" ] }, "sellerIban": { "type": [ "string", "null" ] }, "buyerName": { "type": [ "string", "null" ] }, "buyerAddressLine1": { "type": [ "string", "null" ] }, "buyerAddressLine2": { "type": [ "string", "null" ] }, "buyerAddressPostalCode": { "type": [ "string", "null" ] }, "buyerAddressCity": { "type": [ "string", "null" ] }, "buyerAddressCountry": { "type": [ "string", "null" ] }, "buyerEmail": { "type": [ "string", "null" ] }, "buyerKvk": { "type": [ "string", "null" ] }, "buyerBtw": { "type": [ "string", "null" ] }, "termsPdfFileId": { "type": [ "string", "null" ], "format": "uuid" }, "termsPdfVersionId": { "type": [ "string", "null" ], "format": "uuid" }, "termsRichText": { "type": [ "string", "null" ], "maxLength": 100000 }, "termsUrl": { "type": [ "string", "null" ] }, "depositKind": { "$ref": "#/components/schemas/quoteDepositKindSchema" }, "depositRequiredToAccept": { "type": "boolean" }, "depositValue": { "type": "number", "minimum": 0 }, "changesRequestedAt": { "type": [ "string", "null" ], "format": "date-time" }, "createdBy": { "type": [ "string", "null" ], "format": "uuid" }, "updatedBy": { "type": [ "string", "null" ], "format": "uuid" }, "createdAt": { "type": "string", "format": "date-time" }, "updatedAt": { "type": "string", "format": "date-time" } }, "required": [ "id", "organizationId", "customerId", "number", "numberPrefix", "displayNumber", "version", "status", "quoteTemplateId", "title", "intro", "terms", "validUntil", "currency", "vatInclusive", "discountKind", "discountValue", "discountDescription", "promotionId", "subtotalExcl", "vatTotal", "totalIncl", "publicMessage", "sentAt", "acceptedAt", "rejectedAt", "expiredAt", "withdrawnAt", "hasUnpublishedChanges", "acceptedByName", "acceptedByEmail", "acceptedIp", "acceptedUserAgent", "acceptedOptionalLineIds", "archivedAt", "issueDate", "vatRegime", "sellerName", "sellerAddressLine1", "sellerAddressLine2", "sellerAddressPostalCode", "sellerAddressCity", "sellerAddressCountry", "sellerEmail", "sellerPhone", "sellerKvk", "sellerBtw", "sellerIban", "buyerName", "buyerAddressLine1", "buyerAddressLine2", "buyerAddressPostalCode", "buyerAddressCity", "buyerAddressCountry", "buyerEmail", "buyerKvk", "buyerBtw", "termsPdfFileId", "termsPdfVersionId", "termsRichText", "termsUrl", "depositKind", "depositRequiredToAccept", "depositValue", "changesRequestedAt", "createdBy", "updatedBy", "createdAt", "updatedAt" ] }, "quoteStatusSchema": { "type": "string", "enum": [ "draft", "sent", "accepted", "rejected", "expired", "withdrawn", "changes_requested" ] }, "quoteSubscriptionIntervalSchema": { "type": "string", "enum": [ "weekly", "monthly", "quarterly", "yearly" ] } } ``` # Delete a quote. (/docs/openapi/deleteQuote) Archive a quote for the specified organization. ## DELETE /v1/organizations/{organizationId}/quotes/{quoteId} ```json { "operationId": "deleteQuote", "summary": "Delete a quote.", "description": "Archive a quote for the specified organization.", "tags": [ "Quotes" ], "parameters": [ { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" }, { "in": "path", "name": "quoteId", "required": true, "schema": { "$ref": "#/components/schemas/quoteIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "responses": { "200": { "description": "Returns a success payload when the quote is archived.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/publicApiSuccessResponseSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "organizationIdSchema": { "type": "string", "format": "uuid" }, "publicApiSuccessResponseSchema": { "type": "object", "properties": { "success": { "type": "boolean", "enum": [ true ] } }, "required": [ "success" ] }, "quoteIdSchema": { "type": "string", "format": "uuid" } } ``` # Accept a quote. (/docs/openapi/acceptQuote) Mark a quote accepted from the authenticated organization context. ## POST /v1/organizations/{organizationId}/quotes/{quoteId}/accept ```json { "operationId": "acceptQuote", "summary": "Accept a quote.", "description": "Mark a quote accepted from the authenticated organization context.", "tags": [ "Quotes" ], "parameters": [ { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" }, { "in": "path", "name": "quoteId", "required": true, "schema": { "$ref": "#/components/schemas/quoteIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/quoteMarkAcceptedInputSchema" } } }, "description": "Acceptance metadata for the quote.", "required": true }, "responses": { "200": { "description": "Returns the accepted quote.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/quoteSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "discountDescriptionSchema": { "type": [ "string", "null" ], "maxLength": 500 }, "discountKindSchema": { "type": "string", "enum": [ "none", "percentage", "fixed" ] }, "documentVatRegimeSchema": { "type": "string", "enum": [ "standard", "reverse_charge", "exempt", "kor", "margin_scheme" ] }, "organizationIdSchema": { "type": "string", "format": "uuid" }, "quoteCurrencyCodeSchema": { "type": "string", "pattern": "^[A-Z]{3}$" }, "quoteDepositKindSchema": { "type": "string", "enum": [ "none", "percent", "fixed" ] }, "quoteDisplayNumberSchema": { "type": "string", "minLength": 1, "maxLength": 40 }, "quoteIdSchema": { "type": "string", "format": "uuid" }, "quoteMarkAcceptedInputSchema": { "type": "object", "properties": { "acceptedByEmail": { "type": [ "string", "null" ], "format": "email", "maxLength": 320 }, "acceptedByName": { "type": [ "string", "null" ], "minLength": 1, "maxLength": 240 }, "reason": { "type": [ "string", "null" ], "maxLength": 2000 }, "selectedOptionalLineIds": { "type": "array", "items": { "type": "string", "format": "uuid" }, "maxItems": 500 } } }, "quoteNumberPrefixSchema": { "type": "string", "maxLength": 16, "pattern": "^[A-Z0-9-]*$" }, "quoteNumberSchema": { "type": "integer", "exclusiveMinimum": 0, "maximum": 9007199254740991 }, "quoteSchema": { "type": "object", "properties": { "id": { "$ref": "#/components/schemas/quoteIdSchema" }, "organizationId": { "type": "string", "format": "uuid" }, "customerId": { "type": "string", "format": "uuid" }, "number": { "$ref": "#/components/schemas/quoteNumberSchema" }, "numberPrefix": { "$ref": "#/components/schemas/quoteNumberPrefixSchema" }, "displayNumber": { "$ref": "#/components/schemas/quoteDisplayNumberSchema" }, "version": { "type": "integer", "minimum": 1 }, "status": { "$ref": "#/components/schemas/quoteStatusSchema" }, "quoteTemplateId": { "type": [ "string", "null" ], "format": "uuid" }, "title": { "type": "string" }, "intro": { "type": [ "string", "null" ] }, "terms": { "type": [ "string", "null" ] }, "validUntil": { "type": [ "string", "null" ] }, "currency": { "$ref": "#/components/schemas/quoteCurrencyCodeSchema" }, "vatInclusive": { "type": "boolean" }, "discountKind": { "$ref": "#/components/schemas/discountKindSchema" }, "discountValue": { "type": "number" }, "discountDescription": { "$ref": "#/components/schemas/discountDescriptionSchema" }, "promotionId": { "type": [ "string", "null" ], "format": "uuid" }, "subtotalExcl": { "type": "number" }, "vatTotal": { "type": "number" }, "totalIncl": { "type": "number" }, "publicMessage": { "type": [ "string", "null" ] }, "sentAt": { "type": [ "string", "null" ], "format": "date-time" }, "acceptedAt": { "type": [ "string", "null" ], "format": "date-time" }, "rejectedAt": { "type": [ "string", "null" ], "format": "date-time" }, "expiredAt": { "type": [ "string", "null" ], "format": "date-time" }, "withdrawnAt": { "type": [ "string", "null" ], "format": "date-time" }, "hasUnpublishedChanges": { "type": "boolean" }, "acceptedByName": { "type": [ "string", "null" ] }, "acceptedByEmail": { "type": [ "string", "null" ] }, "acceptedIp": { "type": [ "string", "null" ] }, "acceptedUserAgent": { "type": [ "string", "null" ] }, "acceptedOptionalLineIds": { "type": "array", "items": { "type": "string", "format": "uuid" } }, "archivedAt": { "type": [ "string", "null" ], "format": "date-time" }, "issueDate": { "type": [ "string", "null" ] }, "vatRegime": { "$ref": "#/components/schemas/documentVatRegimeSchema" }, "sellerName": { "type": [ "string", "null" ] }, "sellerAddressLine1": { "type": [ "string", "null" ] }, "sellerAddressLine2": { "type": [ "string", "null" ] }, "sellerAddressPostalCode": { "type": [ "string", "null" ] }, "sellerAddressCity": { "type": [ "string", "null" ] }, "sellerAddressCountry": { "type": [ "string", "null" ] }, "sellerEmail": { "type": [ "string", "null" ] }, "sellerPhone": { "type": [ "string", "null" ] }, "sellerKvk": { "type": [ "string", "null" ] }, "sellerBtw": { "type": [ "string", "null" ] }, "sellerIban": { "type": [ "string", "null" ] }, "buyerName": { "type": [ "string", "null" ] }, "buyerAddressLine1": { "type": [ "string", "null" ] }, "buyerAddressLine2": { "type": [ "string", "null" ] }, "buyerAddressPostalCode": { "type": [ "string", "null" ] }, "buyerAddressCity": { "type": [ "string", "null" ] }, "buyerAddressCountry": { "type": [ "string", "null" ] }, "buyerEmail": { "type": [ "string", "null" ] }, "buyerKvk": { "type": [ "string", "null" ] }, "buyerBtw": { "type": [ "string", "null" ] }, "termsPdfFileId": { "type": [ "string", "null" ], "format": "uuid" }, "termsPdfVersionId": { "type": [ "string", "null" ], "format": "uuid" }, "termsRichText": { "type": [ "string", "null" ], "maxLength": 100000 }, "termsUrl": { "type": [ "string", "null" ] }, "depositKind": { "$ref": "#/components/schemas/quoteDepositKindSchema" }, "depositRequiredToAccept": { "type": "boolean" }, "depositValue": { "type": "number", "minimum": 0 }, "changesRequestedAt": { "type": [ "string", "null" ], "format": "date-time" }, "createdBy": { "type": [ "string", "null" ], "format": "uuid" }, "updatedBy": { "type": [ "string", "null" ], "format": "uuid" }, "createdAt": { "type": "string", "format": "date-time" }, "updatedAt": { "type": "string", "format": "date-time" } }, "required": [ "id", "organizationId", "customerId", "number", "numberPrefix", "displayNumber", "version", "status", "quoteTemplateId", "title", "intro", "terms", "validUntil", "currency", "vatInclusive", "discountKind", "discountValue", "discountDescription", "promotionId", "subtotalExcl", "vatTotal", "totalIncl", "publicMessage", "sentAt", "acceptedAt", "rejectedAt", "expiredAt", "withdrawnAt", "hasUnpublishedChanges", "acceptedByName", "acceptedByEmail", "acceptedIp", "acceptedUserAgent", "acceptedOptionalLineIds", "archivedAt", "issueDate", "vatRegime", "sellerName", "sellerAddressLine1", "sellerAddressLine2", "sellerAddressPostalCode", "sellerAddressCity", "sellerAddressCountry", "sellerEmail", "sellerPhone", "sellerKvk", "sellerBtw", "sellerIban", "buyerName", "buyerAddressLine1", "buyerAddressLine2", "buyerAddressPostalCode", "buyerAddressCity", "buyerAddressCountry", "buyerEmail", "buyerKvk", "buyerBtw", "termsPdfFileId", "termsPdfVersionId", "termsRichText", "termsUrl", "depositKind", "depositRequiredToAccept", "depositValue", "changesRequestedAt", "createdBy", "updatedBy", "createdAt", "updatedAt" ] }, "quoteStatusSchema": { "type": "string", "enum": [ "draft", "sent", "accepted", "rejected", "expired", "withdrawn", "changes_requested" ] } } ``` # Set quote asset links. (/docs/openapi/setQuoteAssets) Replace the customer assets linked to a quote. ## PUT /v1/organizations/{organizationId}/quotes/{quoteId}/assets ```json { "operationId": "setQuoteAssets", "summary": "Set quote asset links.", "description": "Replace the customer assets linked to a quote.", "tags": [ "Quotes" ], "parameters": [ { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" }, { "in": "path", "name": "quoteId", "required": true, "schema": { "$ref": "#/components/schemas/quoteIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/quoteAssetLinksInputSchema" } } }, "description": "Customer asset ids to link to the quote." }, "responses": { "200": { "description": "Returns the linked customer asset ids.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/quoteAssetLinksResponseSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "organizationIdSchema": { "type": "string", "format": "uuid" }, "quoteAssetLinksInputSchema": { "type": "object", "properties": { "customerAssetIds": { "type": "array", "items": { "type": "string", "format": "uuid" }, "maxItems": 200 } }, "required": [ "customerAssetIds" ] }, "quoteAssetLinksResponseSchema": { "type": "object", "properties": { "customerAssetIds": { "type": "array", "items": { "type": "string", "format": "uuid" } } }, "required": [ "customerAssetIds" ] }, "quoteIdSchema": { "type": "string", "format": "uuid" } } ``` # Duplicate a quote. (/docs/openapi/duplicateQuote) Duplicate a quote as a new editable version. ## POST /v1/organizations/{organizationId}/quotes/{quoteId}/duplicate ```json { "operationId": "duplicateQuote", "summary": "Duplicate a quote.", "description": "Duplicate a quote as a new editable version.", "tags": [ "Quotes" ], "parameters": [ { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" }, { "in": "path", "name": "quoteId", "required": true, "schema": { "$ref": "#/components/schemas/quoteIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "responses": { "201": { "description": "Returns the duplicated quote.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/quoteSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "discountDescriptionSchema": { "type": [ "string", "null" ], "maxLength": 500 }, "discountKindSchema": { "type": "string", "enum": [ "none", "percentage", "fixed" ] }, "documentVatRegimeSchema": { "type": "string", "enum": [ "standard", "reverse_charge", "exempt", "kor", "margin_scheme" ] }, "organizationIdSchema": { "type": "string", "format": "uuid" }, "quoteCurrencyCodeSchema": { "type": "string", "pattern": "^[A-Z]{3}$" }, "quoteDepositKindSchema": { "type": "string", "enum": [ "none", "percent", "fixed" ] }, "quoteDisplayNumberSchema": { "type": "string", "minLength": 1, "maxLength": 40 }, "quoteIdSchema": { "type": "string", "format": "uuid" }, "quoteNumberPrefixSchema": { "type": "string", "maxLength": 16, "pattern": "^[A-Z0-9-]*$" }, "quoteNumberSchema": { "type": "integer", "exclusiveMinimum": 0, "maximum": 9007199254740991 }, "quoteSchema": { "type": "object", "properties": { "id": { "$ref": "#/components/schemas/quoteIdSchema" }, "organizationId": { "type": "string", "format": "uuid" }, "customerId": { "type": "string", "format": "uuid" }, "number": { "$ref": "#/components/schemas/quoteNumberSchema" }, "numberPrefix": { "$ref": "#/components/schemas/quoteNumberPrefixSchema" }, "displayNumber": { "$ref": "#/components/schemas/quoteDisplayNumberSchema" }, "version": { "type": "integer", "minimum": 1 }, "status": { "$ref": "#/components/schemas/quoteStatusSchema" }, "quoteTemplateId": { "type": [ "string", "null" ], "format": "uuid" }, "title": { "type": "string" }, "intro": { "type": [ "string", "null" ] }, "terms": { "type": [ "string", "null" ] }, "validUntil": { "type": [ "string", "null" ] }, "currency": { "$ref": "#/components/schemas/quoteCurrencyCodeSchema" }, "vatInclusive": { "type": "boolean" }, "discountKind": { "$ref": "#/components/schemas/discountKindSchema" }, "discountValue": { "type": "number" }, "discountDescription": { "$ref": "#/components/schemas/discountDescriptionSchema" }, "promotionId": { "type": [ "string", "null" ], "format": "uuid" }, "subtotalExcl": { "type": "number" }, "vatTotal": { "type": "number" }, "totalIncl": { "type": "number" }, "publicMessage": { "type": [ "string", "null" ] }, "sentAt": { "type": [ "string", "null" ], "format": "date-time" }, "acceptedAt": { "type": [ "string", "null" ], "format": "date-time" }, "rejectedAt": { "type": [ "string", "null" ], "format": "date-time" }, "expiredAt": { "type": [ "string", "null" ], "format": "date-time" }, "withdrawnAt": { "type": [ "string", "null" ], "format": "date-time" }, "hasUnpublishedChanges": { "type": "boolean" }, "acceptedByName": { "type": [ "string", "null" ] }, "acceptedByEmail": { "type": [ "string", "null" ] }, "acceptedIp": { "type": [ "string", "null" ] }, "acceptedUserAgent": { "type": [ "string", "null" ] }, "acceptedOptionalLineIds": { "type": "array", "items": { "type": "string", "format": "uuid" } }, "archivedAt": { "type": [ "string", "null" ], "format": "date-time" }, "issueDate": { "type": [ "string", "null" ] }, "vatRegime": { "$ref": "#/components/schemas/documentVatRegimeSchema" }, "sellerName": { "type": [ "string", "null" ] }, "sellerAddressLine1": { "type": [ "string", "null" ] }, "sellerAddressLine2": { "type": [ "string", "null" ] }, "sellerAddressPostalCode": { "type": [ "string", "null" ] }, "sellerAddressCity": { "type": [ "string", "null" ] }, "sellerAddressCountry": { "type": [ "string", "null" ] }, "sellerEmail": { "type": [ "string", "null" ] }, "sellerPhone": { "type": [ "string", "null" ] }, "sellerKvk": { "type": [ "string", "null" ] }, "sellerBtw": { "type": [ "string", "null" ] }, "sellerIban": { "type": [ "string", "null" ] }, "buyerName": { "type": [ "string", "null" ] }, "buyerAddressLine1": { "type": [ "string", "null" ] }, "buyerAddressLine2": { "type": [ "string", "null" ] }, "buyerAddressPostalCode": { "type": [ "string", "null" ] }, "buyerAddressCity": { "type": [ "string", "null" ] }, "buyerAddressCountry": { "type": [ "string", "null" ] }, "buyerEmail": { "type": [ "string", "null" ] }, "buyerKvk": { "type": [ "string", "null" ] }, "buyerBtw": { "type": [ "string", "null" ] }, "termsPdfFileId": { "type": [ "string", "null" ], "format": "uuid" }, "termsPdfVersionId": { "type": [ "string", "null" ], "format": "uuid" }, "termsRichText": { "type": [ "string", "null" ], "maxLength": 100000 }, "termsUrl": { "type": [ "string", "null" ] }, "depositKind": { "$ref": "#/components/schemas/quoteDepositKindSchema" }, "depositRequiredToAccept": { "type": "boolean" }, "depositValue": { "type": "number", "minimum": 0 }, "changesRequestedAt": { "type": [ "string", "null" ], "format": "date-time" }, "createdBy": { "type": [ "string", "null" ], "format": "uuid" }, "updatedBy": { "type": [ "string", "null" ], "format": "uuid" }, "createdAt": { "type": "string", "format": "date-time" }, "updatedAt": { "type": "string", "format": "date-time" } }, "required": [ "id", "organizationId", "customerId", "number", "numberPrefix", "displayNumber", "version", "status", "quoteTemplateId", "title", "intro", "terms", "validUntil", "currency", "vatInclusive", "discountKind", "discountValue", "discountDescription", "promotionId", "subtotalExcl", "vatTotal", "totalIncl", "publicMessage", "sentAt", "acceptedAt", "rejectedAt", "expiredAt", "withdrawnAt", "hasUnpublishedChanges", "acceptedByName", "acceptedByEmail", "acceptedIp", "acceptedUserAgent", "acceptedOptionalLineIds", "archivedAt", "issueDate", "vatRegime", "sellerName", "sellerAddressLine1", "sellerAddressLine2", "sellerAddressPostalCode", "sellerAddressCity", "sellerAddressCountry", "sellerEmail", "sellerPhone", "sellerKvk", "sellerBtw", "sellerIban", "buyerName", "buyerAddressLine1", "buyerAddressLine2", "buyerAddressPostalCode", "buyerAddressCity", "buyerAddressCountry", "buyerEmail", "buyerKvk", "buyerBtw", "termsPdfFileId", "termsPdfVersionId", "termsRichText", "termsUrl", "depositKind", "depositRequiredToAccept", "depositValue", "changesRequestedAt", "createdBy", "updatedBy", "createdAt", "updatedAt" ] }, "quoteStatusSchema": { "type": "string", "enum": [ "draft", "sent", "accepted", "rejected", "expired", "withdrawn", "changes_requested" ] } } ``` # Read a quote lifecycle. (/docs/openapi/getQuoteLifecycle) The quote's lifecycle: the pinned workflow version, the current stage, the stage history, and what the lifecycle waits on now. `lifecycle` is null when no lifecycle run started for the quote. ## GET /v1/organizations/{organizationId}/quotes/{quoteId}/lifecycle ```json { "operationId": "getQuoteLifecycle", "summary": "Read a quote lifecycle.", "description": "The quote's lifecycle: the pinned workflow version, the current stage, the stage history, and what the lifecycle waits on now. `lifecycle` is null when no lifecycle run started for the quote.", "tags": [ "Quotes" ], "parameters": [ { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" }, { "in": "path", "name": "quoteId", "required": true, "schema": { "$ref": "#/components/schemas/quoteIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "responses": { "200": { "description": "Returns the quote's lifecycle.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/recordLifecycleSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "dateTimeSchema": { "type": "string" }, "organizationIdSchema": { "type": "string", "format": "uuid" }, "quoteIdSchema": { "type": "string", "format": "uuid" }, "recordLifecycleSchema": { "type": "object", "properties": { "definition": { "type": [ "object", "null" ], "properties": { "id": { "type": "string", "format": "uuid" }, "key": { "type": "string" }, "name": { "type": "string" }, "source": { "type": "string", "enum": [ "organization", "record", "system", "template" ] }, "version": { "type": "integer", "exclusiveMinimum": 0 } }, "required": [ "id", "key", "name", "source", "version" ] }, "events": { "type": "array", "items": { "$ref": "#/components/schemas/workflowLifecycleEventSchema" } }, "lifecycle": { "anyOf": [ { "$ref": "#/components/schemas/workflowLifecycleSchema" }, { "type": "null" } ] }, "waiting": { "type": [ "object", "null" ], "properties": { "at": { "anyOf": [ { "$ref": "#/components/schemas/dateTimeSchema" }, { "type": "null" } ] }, "events": { "type": "array", "items": { "type": "string" } }, "kind": { "type": "string", "enum": [ "approval", "delay", "event", "manual" ] }, "nextStageKey": { "type": [ "string", "null" ] }, "nextStageLabel": { "type": [ "string", "null" ] } }, "required": [ "at", "events", "kind", "nextStageKey", "nextStageLabel" ] } }, "required": [ "definition", "events", "lifecycle", "waiting" ] }, "workflowLifecycleEventSchema": { "type": "object", "properties": { "actorCustomerContactId": { "type": [ "string", "null" ], "format": "uuid" }, "actorUserId": { "type": [ "string", "null" ], "format": "uuid" }, "clearedAt": { "anyOf": [ { "$ref": "#/components/schemas/dateTimeSchema" }, { "type": "null" } ] }, "createdAt": { "$ref": "#/components/schemas/dateTimeSchema" }, "display": { "type": "string", "enum": [ "main", "sub", "flag", "hidden" ] }, "id": { "type": "string", "format": "uuid" }, "label": { "type": "string", "minLength": 1 }, "lifecycleId": { "type": "string", "format": "uuid" }, "organizationId": { "type": "string", "format": "uuid" }, "outcome": { "type": "string", "enum": [ "progress", "success", "failure", "neutral" ] }, "reachedAt": { "$ref": "#/components/schemas/dateTimeSchema" }, "runStepId": { "type": [ "string", "null" ], "format": "uuid" }, "sourceRunId": { "type": [ "string", "null" ], "format": "uuid" }, "stageKey": { "type": "string", "minLength": 1 } }, "required": [ "actorCustomerContactId", "actorUserId", "clearedAt", "createdAt", "display", "id", "label", "lifecycleId", "organizationId", "outcome", "reachedAt", "runStepId", "sourceRunId", "stageKey" ] }, "workflowLifecycleSchema": { "type": "object", "properties": { "createdAt": { "$ref": "#/components/schemas/dateTimeSchema" }, "currentStageKey": { "type": [ "string", "null" ] }, "definitionId": { "type": "string", "format": "uuid" }, "definitionVersion": { "type": "integer", "exclusiveMinimum": 0 }, "endedAt": { "anyOf": [ { "$ref": "#/components/schemas/dateTimeSchema" }, { "type": "null" } ] }, "id": { "type": "string", "format": "uuid" }, "organizationId": { "type": "string", "format": "uuid" }, "pauseReason": { "type": [ "string", "null" ] }, "pausedAt": { "anyOf": [ { "$ref": "#/components/schemas/dateTimeSchema" }, { "type": "null" } ] }, "pausedBy": { "type": [ "string", "null" ], "format": "uuid" }, "pausedUntil": { "anyOf": [ { "$ref": "#/components/schemas/dateTimeSchema" }, { "type": "null" } ] }, "runId": { "type": [ "string", "null" ], "format": "uuid" }, "startedAt": { "$ref": "#/components/schemas/dateTimeSchema" }, "subjectId": { "type": "string", "format": "uuid" }, "subjectType": { "$ref": "#/components/schemas/workflowSubjectTypeSchema" }, "updatedAt": { "$ref": "#/components/schemas/dateTimeSchema" } }, "required": [ "createdAt", "currentStageKey", "definitionId", "definitionVersion", "endedAt", "id", "organizationId", "pauseReason", "pausedAt", "pausedBy", "pausedUntil", "runId", "startedAt", "subjectId", "subjectType", "updatedAt" ] }, "workflowSubjectTypeSchema": { "type": "string", "enum": [ "invoice", "order", "purchase_order", "quote", "shipment", "task" ] } } ``` # List quote notes. (/docs/openapi/listQuoteNotes) Return internal notes for a quote. ## GET /v1/organizations/{organizationId}/quotes/{quoteId}/notes ```json { "operationId": "listQuoteNotes", "summary": "List quote notes.", "description": "Return internal notes for a quote.", "tags": [ "Quotes" ], "parameters": [ { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" }, { "in": "path", "name": "quoteId", "required": true, "schema": { "$ref": "#/components/schemas/quoteIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "responses": { "200": { "description": "Returns quote notes.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/quoteNotesResponseSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "collaborationBlockSchema": { "type": "object", "discriminator": { "propertyName": "type" }, "oneOf": [ { "type": "object", "properties": { "content": { "$ref": "#/components/schemas/collaborationInlineContentSchema" }, "type": { "type": "string", "enum": [ "paragraph" ] } }, "required": [ "content", "type" ], "additionalProperties": false }, { "type": "object", "properties": { "items": { "type": "array", "items": { "$ref": "#/components/schemas/collaborationInlineContentSchema" }, "minItems": 1, "maxItems": 500 }, "type": { "type": "string", "enum": [ "bullet-list" ] } }, "required": [ "items", "type" ], "additionalProperties": false }, { "type": "object", "properties": { "items": { "type": "array", "items": { "$ref": "#/components/schemas/collaborationInlineContentSchema" }, "minItems": 1, "maxItems": 500 }, "type": { "type": "string", "enum": [ "numbered-list" ] } }, "required": [ "items", "type" ], "additionalProperties": false } ] }, "collaborationCommentAuthorSchema": { "type": "object", "properties": { "avatarUrl": { "type": [ "string", "null" ], "format": "uri" }, "displayName": { "type": "string", "minLength": 1 }, "userId": { "type": "string", "format": "uuid" } }, "required": [ "avatarUrl", "displayName", "userId" ] }, "collaborationDocumentSchema": { "type": "object", "properties": { "blocks": { "type": "array", "items": { "$ref": "#/components/schemas/collaborationBlockSchema" }, "minItems": 1, "maxItems": 1000 }, "version": { "type": "integer", "enum": [ 1 ] } }, "required": [ "blocks", "version" ], "additionalProperties": false }, "collaborationInlineContentSchema": { "type": "array", "items": { "$ref": "#/components/schemas/collaborationInlineSchema" }, "minItems": 1, "maxItems": 1000 }, "collaborationInlineSchema": { "type": "object", "discriminator": { "propertyName": "type" }, "oneOf": [ { "type": "object", "properties": { "text": { "type": "string", "minLength": 1, "maxLength": 20000 }, "type": { "type": "string", "enum": [ "text" ] } }, "required": [ "text", "type" ], "additionalProperties": false }, { "type": "object", "properties": { "reference": { "$ref": "#/components/schemas/collaborationReferenceSchema" }, "type": { "type": "string", "enum": [ "reference" ] } }, "required": [ "reference", "type" ], "additionalProperties": false } ] }, "collaborationReferenceSchema": { "type": "object", "discriminator": { "propertyName": "kind" }, "oneOf": [ { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "kind": { "type": "string", "enum": [ "user" ] }, "label": { "type": "string", "minLength": 1, "maxLength": 500 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "path": { "type": "string", "pattern": "^/", "maxLength": 2000 } }, "required": [ "id", "kind", "label", "organizationId", "path" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "kind": { "type": "string", "enum": [ "team" ] }, "label": { "type": "string", "minLength": 1, "maxLength": 500 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "path": { "type": "string", "pattern": "^/", "maxLength": 2000 } }, "required": [ "id", "kind", "label", "organizationId", "path" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "kind": { "type": "string", "enum": [ "task" ] }, "label": { "type": "string", "minLength": 1, "maxLength": 500 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "path": { "type": "string", "pattern": "^/", "maxLength": 2000 } }, "required": [ "id", "kind", "label", "organizationId", "path" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "kind": { "type": "string", "enum": [ "customer" ] }, "label": { "type": "string", "minLength": 1, "maxLength": 500 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "path": { "type": "string", "pattern": "^/", "maxLength": 2000 } }, "required": [ "id", "kind", "label", "organizationId", "path" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "$ref": "#/components/schemas/quoteIdSchema" }, "kind": { "type": "string", "enum": [ "quote" ] }, "label": { "type": "string", "minLength": 1, "maxLength": 500 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "path": { "type": "string", "pattern": "^/", "maxLength": 2000 } }, "required": [ "id", "kind", "label", "organizationId", "path" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "$ref": "#/components/schemas/invoiceIdSchema" }, "kind": { "type": "string", "enum": [ "invoice" ] }, "label": { "type": "string", "minLength": 1, "maxLength": 500 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "path": { "type": "string", "pattern": "^/", "maxLength": 2000 } }, "required": [ "id", "kind", "label", "organizationId", "path" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "$ref": "#/components/schemas/productIdSchema" }, "kind": { "type": "string", "enum": [ "product" ] }, "label": { "type": "string", "minLength": 1, "maxLength": 500 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "path": { "type": "string", "pattern": "^/", "maxLength": 2000 } }, "required": [ "id", "kind", "label", "organizationId", "path" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "kind": { "type": "string", "enum": [ "customer-asset" ] }, "label": { "type": "string", "minLength": 1, "maxLength": 500 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "path": { "type": "string", "pattern": "^/", "maxLength": 2000 } }, "required": [ "id", "kind", "label", "organizationId", "path" ], "additionalProperties": false } ] }, "invoiceIdSchema": { "type": "string", "format": "uuid" }, "listPaginationSchema": { "type": "object", "properties": { "hasMore": { "type": "boolean" }, "limit": { "type": "integer", "minimum": 1, "maximum": 100 }, "offset": { "type": "integer", "minimum": 0 }, "total": { "type": "integer", "minimum": 0 }, "totalIsEstimate": { "type": "boolean" } }, "required": [ "hasMore", "limit", "offset", "total" ] }, "organizationIdSchema": { "type": "string", "format": "uuid" }, "productIdSchema": { "type": "string", "format": "uuid" }, "quoteIdSchema": { "type": "string", "format": "uuid" }, "quoteNoteSchema": { "type": "object", "properties": { "author": { "anyOf": [ { "$ref": "#/components/schemas/collaborationCommentAuthorSchema" }, { "type": "null" } ] }, "authorId": { "type": [ "string", "null" ], "format": "uuid" }, "contentDoc": { "$ref": "#/components/schemas/collaborationDocumentSchema" }, "contentText": { "type": "string", "minLength": 1, "maxLength": 20000 }, "createdAt": { "type": "string", "format": "date-time" }, "id": { "type": "string", "format": "uuid" }, "organizationId": { "type": "string", "format": "uuid" }, "quoteId": { "$ref": "#/components/schemas/quoteIdSchema" }, "updatedAt": { "type": "string", "format": "date-time" } }, "required": [ "author", "authorId", "contentDoc", "contentText", "createdAt", "id", "organizationId", "quoteId", "updatedAt" ] }, "quoteNotesResponseSchema": { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/components/schemas/quoteNoteSchema" } }, "pagination": { "$ref": "#/components/schemas/listPaginationSchema" } }, "required": [ "data", "pagination" ] } } ``` # Add a quote note. (/docs/openapi/addQuoteNote) Add an internal note to a quote. ## POST /v1/organizations/{organizationId}/quotes/{quoteId}/notes ```json { "operationId": "addQuoteNote", "summary": "Add a quote note.", "description": "Add an internal note to a quote.", "tags": [ "Quotes" ], "parameters": [ { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" }, { "in": "path", "name": "quoteId", "required": true, "schema": { "$ref": "#/components/schemas/quoteIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/quoteNoteInputSchema" } } }, "description": "The quote note payload." }, "responses": { "201": { "description": "Returns the created quote note.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/quoteNoteSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "collaborationBlockSchema": { "type": "object", "discriminator": { "propertyName": "type" }, "oneOf": [ { "type": "object", "properties": { "content": { "$ref": "#/components/schemas/collaborationInlineContentSchema" }, "type": { "type": "string", "enum": [ "paragraph" ] } }, "required": [ "content", "type" ], "additionalProperties": false }, { "type": "object", "properties": { "items": { "type": "array", "items": { "$ref": "#/components/schemas/collaborationInlineContentSchema" }, "minItems": 1, "maxItems": 500 }, "type": { "type": "string", "enum": [ "bullet-list" ] } }, "required": [ "items", "type" ], "additionalProperties": false }, { "type": "object", "properties": { "items": { "type": "array", "items": { "$ref": "#/components/schemas/collaborationInlineContentSchema" }, "minItems": 1, "maxItems": 500 }, "type": { "type": "string", "enum": [ "numbered-list" ] } }, "required": [ "items", "type" ], "additionalProperties": false } ] }, "collaborationCommentAuthorSchema": { "type": "object", "properties": { "avatarUrl": { "type": [ "string", "null" ], "format": "uri" }, "displayName": { "type": "string", "minLength": 1 }, "userId": { "type": "string", "format": "uuid" } }, "required": [ "avatarUrl", "displayName", "userId" ] }, "collaborationComposerInputSchema": { "type": "object", "properties": { "selections": { "type": "array", "items": { "$ref": "#/components/schemas/collaborationComposerSelectionSchema" }, "maxItems": 100 }, "text": { "type": "string", "minLength": 1, "maxLength": 20000 } }, "required": [ "selections", "text" ], "additionalProperties": false }, "collaborationComposerSelectionSchema": { "type": "object", "properties": { "end": { "type": "integer", "exclusiveMinimum": 0, "maximum": 20000 }, "reference": { "$ref": "#/components/schemas/collaborationReferenceIdSchema" }, "start": { "type": "integer", "minimum": 0, "maximum": 20000 } }, "required": [ "end", "reference", "start" ], "additionalProperties": false }, "collaborationDocumentSchema": { "type": "object", "properties": { "blocks": { "type": "array", "items": { "$ref": "#/components/schemas/collaborationBlockSchema" }, "minItems": 1, "maxItems": 1000 }, "version": { "type": "integer", "enum": [ 1 ] } }, "required": [ "blocks", "version" ], "additionalProperties": false }, "collaborationInlineContentSchema": { "type": "array", "items": { "$ref": "#/components/schemas/collaborationInlineSchema" }, "minItems": 1, "maxItems": 1000 }, "collaborationInlineSchema": { "type": "object", "discriminator": { "propertyName": "type" }, "oneOf": [ { "type": "object", "properties": { "text": { "type": "string", "minLength": 1, "maxLength": 20000 }, "type": { "type": "string", "enum": [ "text" ] } }, "required": [ "text", "type" ], "additionalProperties": false }, { "type": "object", "properties": { "reference": { "$ref": "#/components/schemas/collaborationReferenceSchema" }, "type": { "type": "string", "enum": [ "reference" ] } }, "required": [ "reference", "type" ], "additionalProperties": false } ] }, "collaborationReferenceIdSchema": { "type": "object", "discriminator": { "propertyName": "kind" }, "oneOf": [ { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "kind": { "type": "string", "enum": [ "user" ] } }, "required": [ "id", "kind" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "kind": { "type": "string", "enum": [ "team" ] } }, "required": [ "id", "kind" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "kind": { "type": "string", "enum": [ "task" ] } }, "required": [ "id", "kind" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "kind": { "type": "string", "enum": [ "customer" ] } }, "required": [ "id", "kind" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "$ref": "#/components/schemas/quoteIdSchema" }, "kind": { "type": "string", "enum": [ "quote" ] } }, "required": [ "id", "kind" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "$ref": "#/components/schemas/invoiceIdSchema" }, "kind": { "type": "string", "enum": [ "invoice" ] } }, "required": [ "id", "kind" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "$ref": "#/components/schemas/productIdSchema" }, "kind": { "type": "string", "enum": [ "product" ] } }, "required": [ "id", "kind" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "kind": { "type": "string", "enum": [ "customer-asset" ] } }, "required": [ "id", "kind" ], "additionalProperties": false } ] }, "collaborationReferenceSchema": { "type": "object", "discriminator": { "propertyName": "kind" }, "oneOf": [ { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "kind": { "type": "string", "enum": [ "user" ] }, "label": { "type": "string", "minLength": 1, "maxLength": 500 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "path": { "type": "string", "pattern": "^/", "maxLength": 2000 } }, "required": [ "id", "kind", "label", "organizationId", "path" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "kind": { "type": "string", "enum": [ "team" ] }, "label": { "type": "string", "minLength": 1, "maxLength": 500 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "path": { "type": "string", "pattern": "^/", "maxLength": 2000 } }, "required": [ "id", "kind", "label", "organizationId", "path" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "kind": { "type": "string", "enum": [ "task" ] }, "label": { "type": "string", "minLength": 1, "maxLength": 500 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "path": { "type": "string", "pattern": "^/", "maxLength": 2000 } }, "required": [ "id", "kind", "label", "organizationId", "path" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "kind": { "type": "string", "enum": [ "customer" ] }, "label": { "type": "string", "minLength": 1, "maxLength": 500 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "path": { "type": "string", "pattern": "^/", "maxLength": 2000 } }, "required": [ "id", "kind", "label", "organizationId", "path" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "$ref": "#/components/schemas/quoteIdSchema" }, "kind": { "type": "string", "enum": [ "quote" ] }, "label": { "type": "string", "minLength": 1, "maxLength": 500 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "path": { "type": "string", "pattern": "^/", "maxLength": 2000 } }, "required": [ "id", "kind", "label", "organizationId", "path" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "$ref": "#/components/schemas/invoiceIdSchema" }, "kind": { "type": "string", "enum": [ "invoice" ] }, "label": { "type": "string", "minLength": 1, "maxLength": 500 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "path": { "type": "string", "pattern": "^/", "maxLength": 2000 } }, "required": [ "id", "kind", "label", "organizationId", "path" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "$ref": "#/components/schemas/productIdSchema" }, "kind": { "type": "string", "enum": [ "product" ] }, "label": { "type": "string", "minLength": 1, "maxLength": 500 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "path": { "type": "string", "pattern": "^/", "maxLength": 2000 } }, "required": [ "id", "kind", "label", "organizationId", "path" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "kind": { "type": "string", "enum": [ "customer-asset" ] }, "label": { "type": "string", "minLength": 1, "maxLength": 500 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "path": { "type": "string", "pattern": "^/", "maxLength": 2000 } }, "required": [ "id", "kind", "label", "organizationId", "path" ], "additionalProperties": false } ] }, "invoiceIdSchema": { "type": "string", "format": "uuid" }, "organizationIdSchema": { "type": "string", "format": "uuid" }, "productIdSchema": { "type": "string", "format": "uuid" }, "quoteIdSchema": { "type": "string", "format": "uuid" }, "quoteNoteInputSchema": { "$ref": "#/components/schemas/collaborationComposerInputSchema" }, "quoteNoteSchema": { "type": "object", "properties": { "author": { "anyOf": [ { "$ref": "#/components/schemas/collaborationCommentAuthorSchema" }, { "type": "null" } ] }, "authorId": { "type": [ "string", "null" ], "format": "uuid" }, "contentDoc": { "$ref": "#/components/schemas/collaborationDocumentSchema" }, "contentText": { "type": "string", "minLength": 1, "maxLength": 20000 }, "createdAt": { "type": "string", "format": "date-time" }, "id": { "type": "string", "format": "uuid" }, "organizationId": { "type": "string", "format": "uuid" }, "quoteId": { "$ref": "#/components/schemas/quoteIdSchema" }, "updatedAt": { "type": "string", "format": "date-time" } }, "required": [ "author", "authorId", "contentDoc", "contentText", "createdAt", "id", "organizationId", "quoteId", "updatedAt" ] } } ``` # Reject a quote. (/docs/openapi/rejectQuote) Mark a quote rejected from the authenticated organization context. ## POST /v1/organizations/{organizationId}/quotes/{quoteId}/reject ```json { "operationId": "rejectQuote", "summary": "Reject a quote.", "description": "Mark a quote rejected from the authenticated organization context.", "tags": [ "Quotes" ], "parameters": [ { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" }, { "in": "path", "name": "quoteId", "required": true, "schema": { "$ref": "#/components/schemas/quoteIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/quoteMarkRejectedInputSchema" } } }, "description": "Rejection metadata for the quote.", "required": true }, "responses": { "200": { "description": "Returns the rejected quote.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/quoteSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "discountDescriptionSchema": { "type": [ "string", "null" ], "maxLength": 500 }, "discountKindSchema": { "type": "string", "enum": [ "none", "percentage", "fixed" ] }, "documentVatRegimeSchema": { "type": "string", "enum": [ "standard", "reverse_charge", "exempt", "kor", "margin_scheme" ] }, "organizationIdSchema": { "type": "string", "format": "uuid" }, "quoteCurrencyCodeSchema": { "type": "string", "pattern": "^[A-Z]{3}$" }, "quoteDepositKindSchema": { "type": "string", "enum": [ "none", "percent", "fixed" ] }, "quoteDisplayNumberSchema": { "type": "string", "minLength": 1, "maxLength": 40 }, "quoteIdSchema": { "type": "string", "format": "uuid" }, "quoteMarkRejectedInputSchema": { "type": "object", "properties": { "reason": { "type": [ "string", "null" ], "maxLength": 20000 } } }, "quoteNumberPrefixSchema": { "type": "string", "maxLength": 16, "pattern": "^[A-Z0-9-]*$" }, "quoteNumberSchema": { "type": "integer", "exclusiveMinimum": 0, "maximum": 9007199254740991 }, "quoteSchema": { "type": "object", "properties": { "id": { "$ref": "#/components/schemas/quoteIdSchema" }, "organizationId": { "type": "string", "format": "uuid" }, "customerId": { "type": "string", "format": "uuid" }, "number": { "$ref": "#/components/schemas/quoteNumberSchema" }, "numberPrefix": { "$ref": "#/components/schemas/quoteNumberPrefixSchema" }, "displayNumber": { "$ref": "#/components/schemas/quoteDisplayNumberSchema" }, "version": { "type": "integer", "minimum": 1 }, "status": { "$ref": "#/components/schemas/quoteStatusSchema" }, "quoteTemplateId": { "type": [ "string", "null" ], "format": "uuid" }, "title": { "type": "string" }, "intro": { "type": [ "string", "null" ] }, "terms": { "type": [ "string", "null" ] }, "validUntil": { "type": [ "string", "null" ] }, "currency": { "$ref": "#/components/schemas/quoteCurrencyCodeSchema" }, "vatInclusive": { "type": "boolean" }, "discountKind": { "$ref": "#/components/schemas/discountKindSchema" }, "discountValue": { "type": "number" }, "discountDescription": { "$ref": "#/components/schemas/discountDescriptionSchema" }, "promotionId": { "type": [ "string", "null" ], "format": "uuid" }, "subtotalExcl": { "type": "number" }, "vatTotal": { "type": "number" }, "totalIncl": { "type": "number" }, "publicMessage": { "type": [ "string", "null" ] }, "sentAt": { "type": [ "string", "null" ], "format": "date-time" }, "acceptedAt": { "type": [ "string", "null" ], "format": "date-time" }, "rejectedAt": { "type": [ "string", "null" ], "format": "date-time" }, "expiredAt": { "type": [ "string", "null" ], "format": "date-time" }, "withdrawnAt": { "type": [ "string", "null" ], "format": "date-time" }, "hasUnpublishedChanges": { "type": "boolean" }, "acceptedByName": { "type": [ "string", "null" ] }, "acceptedByEmail": { "type": [ "string", "null" ] }, "acceptedIp": { "type": [ "string", "null" ] }, "acceptedUserAgent": { "type": [ "string", "null" ] }, "acceptedOptionalLineIds": { "type": "array", "items": { "type": "string", "format": "uuid" } }, "archivedAt": { "type": [ "string", "null" ], "format": "date-time" }, "issueDate": { "type": [ "string", "null" ] }, "vatRegime": { "$ref": "#/components/schemas/documentVatRegimeSchema" }, "sellerName": { "type": [ "string", "null" ] }, "sellerAddressLine1": { "type": [ "string", "null" ] }, "sellerAddressLine2": { "type": [ "string", "null" ] }, "sellerAddressPostalCode": { "type": [ "string", "null" ] }, "sellerAddressCity": { "type": [ "string", "null" ] }, "sellerAddressCountry": { "type": [ "string", "null" ] }, "sellerEmail": { "type": [ "string", "null" ] }, "sellerPhone": { "type": [ "string", "null" ] }, "sellerKvk": { "type": [ "string", "null" ] }, "sellerBtw": { "type": [ "string", "null" ] }, "sellerIban": { "type": [ "string", "null" ] }, "buyerName": { "type": [ "string", "null" ] }, "buyerAddressLine1": { "type": [ "string", "null" ] }, "buyerAddressLine2": { "type": [ "string", "null" ] }, "buyerAddressPostalCode": { "type": [ "string", "null" ] }, "buyerAddressCity": { "type": [ "string", "null" ] }, "buyerAddressCountry": { "type": [ "string", "null" ] }, "buyerEmail": { "type": [ "string", "null" ] }, "buyerKvk": { "type": [ "string", "null" ] }, "buyerBtw": { "type": [ "string", "null" ] }, "termsPdfFileId": { "type": [ "string", "null" ], "format": "uuid" }, "termsPdfVersionId": { "type": [ "string", "null" ], "format": "uuid" }, "termsRichText": { "type": [ "string", "null" ], "maxLength": 100000 }, "termsUrl": { "type": [ "string", "null" ] }, "depositKind": { "$ref": "#/components/schemas/quoteDepositKindSchema" }, "depositRequiredToAccept": { "type": "boolean" }, "depositValue": { "type": "number", "minimum": 0 }, "changesRequestedAt": { "type": [ "string", "null" ], "format": "date-time" }, "createdBy": { "type": [ "string", "null" ], "format": "uuid" }, "updatedBy": { "type": [ "string", "null" ], "format": "uuid" }, "createdAt": { "type": "string", "format": "date-time" }, "updatedAt": { "type": "string", "format": "date-time" } }, "required": [ "id", "organizationId", "customerId", "number", "numberPrefix", "displayNumber", "version", "status", "quoteTemplateId", "title", "intro", "terms", "validUntil", "currency", "vatInclusive", "discountKind", "discountValue", "discountDescription", "promotionId", "subtotalExcl", "vatTotal", "totalIncl", "publicMessage", "sentAt", "acceptedAt", "rejectedAt", "expiredAt", "withdrawnAt", "hasUnpublishedChanges", "acceptedByName", "acceptedByEmail", "acceptedIp", "acceptedUserAgent", "acceptedOptionalLineIds", "archivedAt", "issueDate", "vatRegime", "sellerName", "sellerAddressLine1", "sellerAddressLine2", "sellerAddressPostalCode", "sellerAddressCity", "sellerAddressCountry", "sellerEmail", "sellerPhone", "sellerKvk", "sellerBtw", "sellerIban", "buyerName", "buyerAddressLine1", "buyerAddressLine2", "buyerAddressPostalCode", "buyerAddressCity", "buyerAddressCountry", "buyerEmail", "buyerKvk", "buyerBtw", "termsPdfFileId", "termsPdfVersionId", "termsRichText", "termsUrl", "depositKind", "depositRequiredToAccept", "depositValue", "changesRequestedAt", "createdBy", "updatedBy", "createdAt", "updatedAt" ] }, "quoteStatusSchema": { "type": "string", "enum": [ "draft", "sent", "accepted", "rejected", "expired", "withdrawn", "changes_requested" ] } } ``` # Send a quote. (/docs/openapi/sendQuote) Transition a draft quote to sent. ## POST /v1/organizations/{organizationId}/quotes/{quoteId}/send ```json { "operationId": "sendQuote", "summary": "Send a quote.", "description": "Transition a draft quote to sent.", "tags": [ "Quotes" ], "parameters": [ { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" }, { "in": "path", "name": "quoteId", "required": true, "schema": { "$ref": "#/components/schemas/quoteIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/quoteSendInputSchema" } } }, "description": "Send options for the quote.", "required": true }, "responses": { "200": { "description": "Returns the sent quote.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/quoteSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "discountDescriptionSchema": { "type": [ "string", "null" ], "maxLength": 500 }, "discountKindSchema": { "type": "string", "enum": [ "none", "percentage", "fixed" ] }, "documentVatRegimeSchema": { "type": "string", "enum": [ "standard", "reverse_charge", "exempt", "kor", "margin_scheme" ] }, "isoDateOrNull": { "type": [ "string", "null" ], "pattern": "^\\d{4}-\\d{2}-\\d{2}$" }, "organizationIdSchema": { "type": "string", "format": "uuid" }, "quoteCurrencyCodeSchema": { "type": "string", "pattern": "^[A-Z]{3}$" }, "quoteDepositKindSchema": { "type": "string", "enum": [ "none", "percent", "fixed" ] }, "quoteDisplayNumberSchema": { "type": "string", "minLength": 1, "maxLength": 40 }, "quoteIdSchema": { "type": "string", "format": "uuid" }, "quoteNumberPrefixSchema": { "type": "string", "maxLength": 16, "pattern": "^[A-Z0-9-]*$" }, "quoteNumberSchema": { "type": "integer", "exclusiveMinimum": 0, "maximum": 9007199254740991 }, "quoteSchema": { "type": "object", "properties": { "id": { "$ref": "#/components/schemas/quoteIdSchema" }, "organizationId": { "type": "string", "format": "uuid" }, "customerId": { "type": "string", "format": "uuid" }, "number": { "$ref": "#/components/schemas/quoteNumberSchema" }, "numberPrefix": { "$ref": "#/components/schemas/quoteNumberPrefixSchema" }, "displayNumber": { "$ref": "#/components/schemas/quoteDisplayNumberSchema" }, "version": { "type": "integer", "minimum": 1 }, "status": { "$ref": "#/components/schemas/quoteStatusSchema" }, "quoteTemplateId": { "type": [ "string", "null" ], "format": "uuid" }, "title": { "type": "string" }, "intro": { "type": [ "string", "null" ] }, "terms": { "type": [ "string", "null" ] }, "validUntil": { "type": [ "string", "null" ] }, "currency": { "$ref": "#/components/schemas/quoteCurrencyCodeSchema" }, "vatInclusive": { "type": "boolean" }, "discountKind": { "$ref": "#/components/schemas/discountKindSchema" }, "discountValue": { "type": "number" }, "discountDescription": { "$ref": "#/components/schemas/discountDescriptionSchema" }, "promotionId": { "type": [ "string", "null" ], "format": "uuid" }, "subtotalExcl": { "type": "number" }, "vatTotal": { "type": "number" }, "totalIncl": { "type": "number" }, "publicMessage": { "type": [ "string", "null" ] }, "sentAt": { "type": [ "string", "null" ], "format": "date-time" }, "acceptedAt": { "type": [ "string", "null" ], "format": "date-time" }, "rejectedAt": { "type": [ "string", "null" ], "format": "date-time" }, "expiredAt": { "type": [ "string", "null" ], "format": "date-time" }, "withdrawnAt": { "type": [ "string", "null" ], "format": "date-time" }, "hasUnpublishedChanges": { "type": "boolean" }, "acceptedByName": { "type": [ "string", "null" ] }, "acceptedByEmail": { "type": [ "string", "null" ] }, "acceptedIp": { "type": [ "string", "null" ] }, "acceptedUserAgent": { "type": [ "string", "null" ] }, "acceptedOptionalLineIds": { "type": "array", "items": { "type": "string", "format": "uuid" } }, "archivedAt": { "type": [ "string", "null" ], "format": "date-time" }, "issueDate": { "type": [ "string", "null" ] }, "vatRegime": { "$ref": "#/components/schemas/documentVatRegimeSchema" }, "sellerName": { "type": [ "string", "null" ] }, "sellerAddressLine1": { "type": [ "string", "null" ] }, "sellerAddressLine2": { "type": [ "string", "null" ] }, "sellerAddressPostalCode": { "type": [ "string", "null" ] }, "sellerAddressCity": { "type": [ "string", "null" ] }, "sellerAddressCountry": { "type": [ "string", "null" ] }, "sellerEmail": { "type": [ "string", "null" ] }, "sellerPhone": { "type": [ "string", "null" ] }, "sellerKvk": { "type": [ "string", "null" ] }, "sellerBtw": { "type": [ "string", "null" ] }, "sellerIban": { "type": [ "string", "null" ] }, "buyerName": { "type": [ "string", "null" ] }, "buyerAddressLine1": { "type": [ "string", "null" ] }, "buyerAddressLine2": { "type": [ "string", "null" ] }, "buyerAddressPostalCode": { "type": [ "string", "null" ] }, "buyerAddressCity": { "type": [ "string", "null" ] }, "buyerAddressCountry": { "type": [ "string", "null" ] }, "buyerEmail": { "type": [ "string", "null" ] }, "buyerKvk": { "type": [ "string", "null" ] }, "buyerBtw": { "type": [ "string", "null" ] }, "termsPdfFileId": { "type": [ "string", "null" ], "format": "uuid" }, "termsPdfVersionId": { "type": [ "string", "null" ], "format": "uuid" }, "termsRichText": { "type": [ "string", "null" ], "maxLength": 100000 }, "termsUrl": { "type": [ "string", "null" ] }, "depositKind": { "$ref": "#/components/schemas/quoteDepositKindSchema" }, "depositRequiredToAccept": { "type": "boolean" }, "depositValue": { "type": "number", "minimum": 0 }, "changesRequestedAt": { "type": [ "string", "null" ], "format": "date-time" }, "createdBy": { "type": [ "string", "null" ], "format": "uuid" }, "updatedBy": { "type": [ "string", "null" ], "format": "uuid" }, "createdAt": { "type": "string", "format": "date-time" }, "updatedAt": { "type": "string", "format": "date-time" } }, "required": [ "id", "organizationId", "customerId", "number", "numberPrefix", "displayNumber", "version", "status", "quoteTemplateId", "title", "intro", "terms", "validUntil", "currency", "vatInclusive", "discountKind", "discountValue", "discountDescription", "promotionId", "subtotalExcl", "vatTotal", "totalIncl", "publicMessage", "sentAt", "acceptedAt", "rejectedAt", "expiredAt", "withdrawnAt", "hasUnpublishedChanges", "acceptedByName", "acceptedByEmail", "acceptedIp", "acceptedUserAgent", "acceptedOptionalLineIds", "archivedAt", "issueDate", "vatRegime", "sellerName", "sellerAddressLine1", "sellerAddressLine2", "sellerAddressPostalCode", "sellerAddressCity", "sellerAddressCountry", "sellerEmail", "sellerPhone", "sellerKvk", "sellerBtw", "sellerIban", "buyerName", "buyerAddressLine1", "buyerAddressLine2", "buyerAddressPostalCode", "buyerAddressCity", "buyerAddressCountry", "buyerEmail", "buyerKvk", "buyerBtw", "termsPdfFileId", "termsPdfVersionId", "termsRichText", "termsUrl", "depositKind", "depositRequiredToAccept", "depositValue", "changesRequestedAt", "createdBy", "updatedBy", "createdAt", "updatedAt" ] }, "quoteSendInputSchema": { "type": "object", "properties": { "publicMessage": { "type": [ "string", "null" ], "maxLength": 20000 }, "validUntil": { "allOf": [ { "$ref": "#/components/schemas/isoDateOrNull" } ] } } }, "quoteStatusSchema": { "type": "string", "enum": [ "draft", "sent", "accepted", "rejected", "expired", "withdrawn", "changes_requested" ] } } ``` # Set quote task links. (/docs/openapi/setQuoteTasks) Replace the tasks linked to a quote. ## PUT /v1/organizations/{organizationId}/quotes/{quoteId}/tasks ```json { "operationId": "setQuoteTasks", "summary": "Set quote task links.", "description": "Replace the tasks linked to a quote.", "tags": [ "Quotes" ], "parameters": [ { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" }, { "in": "path", "name": "quoteId", "required": true, "schema": { "$ref": "#/components/schemas/quoteIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/quoteTaskLinksInputSchema" } } }, "description": "Task ids to link to the quote." }, "responses": { "200": { "description": "Returns the linked task ids.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/quoteTaskLinksResponseSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "organizationIdSchema": { "type": "string", "format": "uuid" }, "quoteIdSchema": { "type": "string", "format": "uuid" }, "quoteTaskLinksInputSchema": { "type": "object", "properties": { "taskIds": { "type": "array", "items": { "type": "string", "format": "uuid" }, "maxItems": 200 } }, "required": [ "taskIds" ] }, "quoteTaskLinksResponseSchema": { "type": "object", "properties": { "taskIds": { "type": "array", "items": { "type": "string", "format": "uuid" } } }, "required": [ "taskIds" ] } } ``` # Advance a quote lifecycle stage. (/docs/openapi/advanceQuoteLifecycle) Moves the quote to a manual milestone of its lifecycle. Only stages the lifecycle marks as manually advanceable are accepted. ## POST /v1/organizations/{organizationId}/quotes/{quoteId}/lifecycle/advance ```json { "operationId": "advanceQuoteLifecycle", "summary": "Advance a quote lifecycle stage.", "description": "Moves the quote to a manual milestone of its lifecycle. Only stages the lifecycle marks as manually advanceable are accepted.", "tags": [ "Quotes" ], "parameters": [ { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" }, { "in": "path", "name": "quoteId", "required": true, "schema": { "$ref": "#/components/schemas/quoteIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/lifecycleAdvanceInputSchema" } } }, "description": "The stage key to move to.", "required": true }, "responses": { "200": { "description": "The stage was recorded.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/publicApiSuccessResponseSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "lifecycleAdvanceInputSchema": { "type": "object", "properties": { "stageKey": { "type": "string", "minLength": 1, "maxLength": 80 } }, "required": [ "stageKey" ] }, "organizationIdSchema": { "type": "string", "format": "uuid" }, "publicApiSuccessResponseSchema": { "type": "object", "properties": { "success": { "type": "boolean", "enum": [ true ] } }, "required": [ "success" ] }, "quoteIdSchema": { "type": "string", "format": "uuid" } } ``` # Update a quote note. (/docs/openapi/updateQuoteNote) Update an internal note on a quote. ## PATCH /v1/organizations/{organizationId}/quotes/{quoteId}/notes/{noteId} ```json { "operationId": "updateQuoteNote", "summary": "Update a quote note.", "description": "Update an internal note on a quote.", "tags": [ "Quotes" ], "parameters": [ { "in": "path", "name": "noteId", "required": true, "schema": { "type": "string", "format": "uuid" }, "example": "123e4567-e89b-12d3-a456-426614174000" }, { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" }, { "in": "path", "name": "quoteId", "required": true, "schema": { "$ref": "#/components/schemas/quoteIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/quoteNoteInputSchema" } } }, "description": "The quote note payload." }, "responses": { "200": { "description": "Returns the updated quote note.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/quoteNoteSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "collaborationBlockSchema": { "type": "object", "discriminator": { "propertyName": "type" }, "oneOf": [ { "type": "object", "properties": { "content": { "$ref": "#/components/schemas/collaborationInlineContentSchema" }, "type": { "type": "string", "enum": [ "paragraph" ] } }, "required": [ "content", "type" ], "additionalProperties": false }, { "type": "object", "properties": { "items": { "type": "array", "items": { "$ref": "#/components/schemas/collaborationInlineContentSchema" }, "minItems": 1, "maxItems": 500 }, "type": { "type": "string", "enum": [ "bullet-list" ] } }, "required": [ "items", "type" ], "additionalProperties": false }, { "type": "object", "properties": { "items": { "type": "array", "items": { "$ref": "#/components/schemas/collaborationInlineContentSchema" }, "minItems": 1, "maxItems": 500 }, "type": { "type": "string", "enum": [ "numbered-list" ] } }, "required": [ "items", "type" ], "additionalProperties": false } ] }, "collaborationCommentAuthorSchema": { "type": "object", "properties": { "avatarUrl": { "type": [ "string", "null" ], "format": "uri" }, "displayName": { "type": "string", "minLength": 1 }, "userId": { "type": "string", "format": "uuid" } }, "required": [ "avatarUrl", "displayName", "userId" ] }, "collaborationComposerInputSchema": { "type": "object", "properties": { "selections": { "type": "array", "items": { "$ref": "#/components/schemas/collaborationComposerSelectionSchema" }, "maxItems": 100 }, "text": { "type": "string", "minLength": 1, "maxLength": 20000 } }, "required": [ "selections", "text" ], "additionalProperties": false }, "collaborationComposerSelectionSchema": { "type": "object", "properties": { "end": { "type": "integer", "exclusiveMinimum": 0, "maximum": 20000 }, "reference": { "$ref": "#/components/schemas/collaborationReferenceIdSchema" }, "start": { "type": "integer", "minimum": 0, "maximum": 20000 } }, "required": [ "end", "reference", "start" ], "additionalProperties": false }, "collaborationDocumentSchema": { "type": "object", "properties": { "blocks": { "type": "array", "items": { "$ref": "#/components/schemas/collaborationBlockSchema" }, "minItems": 1, "maxItems": 1000 }, "version": { "type": "integer", "enum": [ 1 ] } }, "required": [ "blocks", "version" ], "additionalProperties": false }, "collaborationInlineContentSchema": { "type": "array", "items": { "$ref": "#/components/schemas/collaborationInlineSchema" }, "minItems": 1, "maxItems": 1000 }, "collaborationInlineSchema": { "type": "object", "discriminator": { "propertyName": "type" }, "oneOf": [ { "type": "object", "properties": { "text": { "type": "string", "minLength": 1, "maxLength": 20000 }, "type": { "type": "string", "enum": [ "text" ] } }, "required": [ "text", "type" ], "additionalProperties": false }, { "type": "object", "properties": { "reference": { "$ref": "#/components/schemas/collaborationReferenceSchema" }, "type": { "type": "string", "enum": [ "reference" ] } }, "required": [ "reference", "type" ], "additionalProperties": false } ] }, "collaborationReferenceIdSchema": { "type": "object", "discriminator": { "propertyName": "kind" }, "oneOf": [ { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "kind": { "type": "string", "enum": [ "user" ] } }, "required": [ "id", "kind" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "kind": { "type": "string", "enum": [ "team" ] } }, "required": [ "id", "kind" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "kind": { "type": "string", "enum": [ "task" ] } }, "required": [ "id", "kind" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "kind": { "type": "string", "enum": [ "customer" ] } }, "required": [ "id", "kind" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "$ref": "#/components/schemas/quoteIdSchema" }, "kind": { "type": "string", "enum": [ "quote" ] } }, "required": [ "id", "kind" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "$ref": "#/components/schemas/invoiceIdSchema" }, "kind": { "type": "string", "enum": [ "invoice" ] } }, "required": [ "id", "kind" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "$ref": "#/components/schemas/productIdSchema" }, "kind": { "type": "string", "enum": [ "product" ] } }, "required": [ "id", "kind" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "kind": { "type": "string", "enum": [ "customer-asset" ] } }, "required": [ "id", "kind" ], "additionalProperties": false } ] }, "collaborationReferenceSchema": { "type": "object", "discriminator": { "propertyName": "kind" }, "oneOf": [ { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "kind": { "type": "string", "enum": [ "user" ] }, "label": { "type": "string", "minLength": 1, "maxLength": 500 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "path": { "type": "string", "pattern": "^/", "maxLength": 2000 } }, "required": [ "id", "kind", "label", "organizationId", "path" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "kind": { "type": "string", "enum": [ "team" ] }, "label": { "type": "string", "minLength": 1, "maxLength": 500 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "path": { "type": "string", "pattern": "^/", "maxLength": 2000 } }, "required": [ "id", "kind", "label", "organizationId", "path" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "kind": { "type": "string", "enum": [ "task" ] }, "label": { "type": "string", "minLength": 1, "maxLength": 500 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "path": { "type": "string", "pattern": "^/", "maxLength": 2000 } }, "required": [ "id", "kind", "label", "organizationId", "path" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "kind": { "type": "string", "enum": [ "customer" ] }, "label": { "type": "string", "minLength": 1, "maxLength": 500 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "path": { "type": "string", "pattern": "^/", "maxLength": 2000 } }, "required": [ "id", "kind", "label", "organizationId", "path" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "$ref": "#/components/schemas/quoteIdSchema" }, "kind": { "type": "string", "enum": [ "quote" ] }, "label": { "type": "string", "minLength": 1, "maxLength": 500 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "path": { "type": "string", "pattern": "^/", "maxLength": 2000 } }, "required": [ "id", "kind", "label", "organizationId", "path" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "$ref": "#/components/schemas/invoiceIdSchema" }, "kind": { "type": "string", "enum": [ "invoice" ] }, "label": { "type": "string", "minLength": 1, "maxLength": 500 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "path": { "type": "string", "pattern": "^/", "maxLength": 2000 } }, "required": [ "id", "kind", "label", "organizationId", "path" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "$ref": "#/components/schemas/productIdSchema" }, "kind": { "type": "string", "enum": [ "product" ] }, "label": { "type": "string", "minLength": 1, "maxLength": 500 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "path": { "type": "string", "pattern": "^/", "maxLength": 2000 } }, "required": [ "id", "kind", "label", "organizationId", "path" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "kind": { "type": "string", "enum": [ "customer-asset" ] }, "label": { "type": "string", "minLength": 1, "maxLength": 500 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "path": { "type": "string", "pattern": "^/", "maxLength": 2000 } }, "required": [ "id", "kind", "label", "organizationId", "path" ], "additionalProperties": false } ] }, "invoiceIdSchema": { "type": "string", "format": "uuid" }, "organizationIdSchema": { "type": "string", "format": "uuid" }, "productIdSchema": { "type": "string", "format": "uuid" }, "quoteIdSchema": { "type": "string", "format": "uuid" }, "quoteNoteInputSchema": { "$ref": "#/components/schemas/collaborationComposerInputSchema" }, "quoteNoteSchema": { "type": "object", "properties": { "author": { "anyOf": [ { "$ref": "#/components/schemas/collaborationCommentAuthorSchema" }, { "type": "null" } ] }, "authorId": { "type": [ "string", "null" ], "format": "uuid" }, "contentDoc": { "$ref": "#/components/schemas/collaborationDocumentSchema" }, "contentText": { "type": "string", "minLength": 1, "maxLength": 20000 }, "createdAt": { "type": "string", "format": "date-time" }, "id": { "type": "string", "format": "uuid" }, "organizationId": { "type": "string", "format": "uuid" }, "quoteId": { "$ref": "#/components/schemas/quoteIdSchema" }, "updatedAt": { "type": "string", "format": "date-time" } }, "required": [ "author", "authorId", "contentDoc", "contentText", "createdAt", "id", "organizationId", "quoteId", "updatedAt" ] } } ``` # Delete a quote note. (/docs/openapi/deleteQuoteNote) Delete an internal note from a quote. ## DELETE /v1/organizations/{organizationId}/quotes/{quoteId}/notes/{noteId} ```json { "operationId": "deleteQuoteNote", "summary": "Delete a quote note.", "description": "Delete an internal note from a quote.", "tags": [ "Quotes" ], "parameters": [ { "in": "path", "name": "noteId", "required": true, "schema": { "type": "string", "format": "uuid" }, "example": "123e4567-e89b-12d3-a456-426614174000" }, { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" }, { "in": "path", "name": "quoteId", "required": true, "schema": { "$ref": "#/components/schemas/quoteIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "responses": { "200": { "description": "Returns a success payload when the note is deleted.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/publicApiSuccessResponseSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "organizationIdSchema": { "type": "string", "format": "uuid" }, "publicApiSuccessResponseSchema": { "type": "object", "properties": { "success": { "type": "boolean", "enum": [ true ] } }, "required": [ "success" ] }, "quoteIdSchema": { "type": "string", "format": "uuid" } } ``` # Request changes to a quote. (/docs/openapi/requestPortalQuoteChanges) The customer asks for changes to the sent quote from the portal page. The quote moves to Changes requested, its reminders stop, and the team is notified. Guests can call this; the quote must be published and still open. ## POST /v1/public/portal/{orgSlug}/quotes/{quoteId}/request-changes ```json { "operationId": "requestPortalQuoteChanges", "summary": "Request changes to a quote.", "description": "The customer asks for changes to the sent quote from the portal page. The quote moves to Changes requested, its reminders stop, and the team is notified. Guests can call this; the quote must be published and still open.", "tags": [ "Quotes" ], "parameters": [ { "in": "path", "name": "orgSlug", "required": true, "schema": { "$ref": "#/components/schemas/organizationSlugSchema" }, "example": "example" }, { "in": "path", "name": "quoteId", "required": true, "schema": { "$ref": "#/components/schemas/quoteIdSchema" }, "example": "123" } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/quoteRequestChangesInputSchema" } } }, "description": "What the customer wants changed.", "required": true }, "responses": { "200": { "description": "The change request was recorded.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/portalQuoteRequestChangesResponseSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "organizationSlugSchema": { "type": "string", "minLength": 2, "maxLength": 64, "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$" }, "portalQuoteRequestChangesResponseSchema": { "type": "object", "properties": { "quoteId": { "type": "string", "format": "uuid" } }, "required": [ "quoteId" ] }, "quoteIdSchema": { "type": "string", "format": "uuid" }, "quoteRequestChangesInputSchema": { "type": "object", "properties": { "comment": { "type": "string", "minLength": 1, "maxLength": 20000 }, "email": { "type": "string", "format": "email", "maxLength": 320 }, "name": { "type": "string", "minLength": 1, "maxLength": 240 } }, "required": [ "comment", "email", "name" ], "examples": [ { "comment": "Could you split the installation into two visits?", "email": "alex@example.com", "name": "Alex Customer" } ] } } ``` # List shipments. (/docs/openapi/listShipments) Return outbound deliveries, inbound receipts, and returns, filtered by order or purchase order. ## GET /v1/organizations/{organizationId}/shipments ```json { "operationId": "listShipments", "summary": "List shipments.", "description": "Return outbound deliveries, inbound receipts, and returns, filtered by order or purchase order.", "tags": [ "Shipments" ], "parameters": [ { "in": "query", "name": "direction", "required": false, "schema": { "allOf": [ { "$ref": "#/components/schemas/shipmentDirectionSchema" } ] }, "example": "example" }, { "in": "query", "name": "limit", "required": true, "schema": { "default": 50, "type": "integer", "minimum": 1, "maximum": 100 }, "example": 1 }, { "in": "query", "name": "offset", "required": true, "schema": { "default": 0, "type": "integer", "minimum": 0, "maximum": 9007199254740991 }, "example": 1 }, { "in": "query", "name": "orderId", "required": false, "schema": { "type": "string", "format": "uuid" }, "example": "123e4567-e89b-12d3-a456-426614174000" }, { "in": "query", "name": "purchaseOrderId", "required": false, "schema": { "type": "string", "format": "uuid" }, "example": "123e4567-e89b-12d3-a456-426614174000" }, { "in": "query", "name": "q", "required": false, "schema": { "type": "string", "maxLength": 200 }, "example": "example" }, { "in": "query", "name": "status", "required": false, "schema": { "allOf": [ { "$ref": "#/components/schemas/shipmentStatusSchema" } ] }, "example": "example" }, { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "responses": { "200": { "description": "Returns shipments.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/shipmentsResponseSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "listPaginationSchema": { "type": "object", "properties": { "hasMore": { "type": "boolean" }, "limit": { "type": "integer", "minimum": 1, "maximum": 100 }, "offset": { "type": "integer", "minimum": 0 }, "total": { "type": "integer", "minimum": 0 }, "totalIsEstimate": { "type": "boolean" } }, "required": [ "hasMore", "limit", "offset", "total" ] }, "orderDeliveryMethodSchema": { "type": "string", "enum": [ "pickup", "own_delivery", "carrier", "digital", "none" ] }, "organizationIdSchema": { "type": "string", "format": "uuid" }, "shipmentDirectionSchema": { "type": "string", "enum": [ "outbound", "inbound", "return" ] }, "shipmentStatusSchema": { "type": "string", "enum": [ "draft", "packed", "ready_for_pickup", "in_transit", "delivered", "picked_up", "exception", "canceled" ] }, "shipmentSummarySchema": { "type": "object", "properties": { "carrier": { "type": [ "string", "null" ] }, "createdAt": { "type": "string" }, "deliveredAt": { "type": [ "string", "null" ] }, "deliveryMethod": { "$ref": "#/components/schemas/orderDeliveryMethodSchema" }, "direction": { "$ref": "#/components/schemas/shipmentDirectionSchema" }, "formattedNumber": { "type": "string" }, "id": { "type": "string", "format": "uuid" }, "number": { "type": "integer" }, "orderId": { "type": [ "string", "null" ], "format": "uuid" }, "orderNumber": { "type": [ "string", "null" ] }, "packageCount": { "type": "integer" }, "partyName": { "type": [ "string", "null" ] }, "purchaseOrderId": { "type": [ "string", "null" ], "format": "uuid" }, "purchaseOrderNumber": { "type": [ "string", "null" ] }, "shippedAt": { "type": [ "string", "null" ] }, "status": { "$ref": "#/components/schemas/shipmentStatusSchema" }, "trackingNumber": { "type": [ "string", "null" ] }, "trackingUrl": { "type": [ "string", "null" ] }, "updatedAt": { "type": "string" } }, "required": [ "carrier", "createdAt", "deliveredAt", "deliveryMethod", "direction", "formattedNumber", "id", "number", "orderId", "orderNumber", "packageCount", "partyName", "purchaseOrderId", "purchaseOrderNumber", "shippedAt", "status", "trackingNumber", "trackingUrl", "updatedAt" ] }, "shipmentsResponseSchema": { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/components/schemas/shipmentSummarySchema" } }, "pagination": { "$ref": "#/components/schemas/listPaginationSchema" } }, "required": [ "data", "pagination" ] } } ``` # Get a shipment. (/docs/openapi/getShipment) Return one shipment with its lines, carrier, and tracking details. ## GET /v1/organizations/{organizationId}/shipments/{shipmentId} ```json { "operationId": "getShipment", "summary": "Get a shipment.", "description": "Return one shipment with its lines, carrier, and tracking details.", "tags": [ "Shipments" ], "parameters": [ { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" }, { "in": "path", "name": "shipmentId", "required": true, "schema": { "type": "string", "format": "uuid" }, "example": "123e4567-e89b-12d3-a456-426614174000" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "responses": { "200": { "description": "Returns the shipment.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/shipmentSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "orderDeliveryMethodSchema": { "type": "string", "enum": [ "pickup", "own_delivery", "carrier", "digital", "none" ] }, "organizationIdSchema": { "type": "string", "format": "uuid" }, "shipmentDirectionSchema": { "type": "string", "enum": [ "outbound", "inbound", "return" ] }, "shipmentLineSchema": { "type": "object", "properties": { "expiresOn": { "type": [ "string", "null" ] }, "id": { "type": "string", "format": "uuid" }, "lotNumber": { "type": [ "string", "null" ] }, "orderLineId": { "type": [ "string", "null" ], "format": "uuid" }, "position": { "type": "number" }, "productId": { "type": [ "string", "null" ], "format": "uuid" }, "purchaseOrderLineId": { "type": [ "string", "null" ], "format": "uuid" }, "quantity": { "type": "number" }, "returnDisposition": { "anyOf": [ { "$ref": "#/components/schemas/shipmentReturnDispositionSchema" }, { "type": "null" } ] }, "serialNumber": { "type": [ "string", "null" ] }, "sku": { "type": [ "string", "null" ] }, "title": { "type": "string" }, "variantId": { "type": [ "string", "null" ], "format": "uuid" } }, "required": [ "expiresOn", "id", "lotNumber", "orderLineId", "position", "productId", "purchaseOrderLineId", "quantity", "returnDisposition", "serialNumber", "sku", "title", "variantId" ] }, "shipmentReturnDispositionSchema": { "type": "string", "enum": [ "restock", "quarantine", "write_off" ] }, "shipmentSchema": { "type": "object", "properties": { "carrier": { "type": [ "string", "null" ] }, "createdAt": { "type": "string" }, "deliveredAt": { "type": [ "string", "null" ] }, "deliveryMethod": { "$ref": "#/components/schemas/orderDeliveryMethodSchema" }, "direction": { "$ref": "#/components/schemas/shipmentDirectionSchema" }, "formattedNumber": { "type": "string" }, "id": { "type": "string", "format": "uuid" }, "number": { "type": "integer" }, "orderId": { "type": [ "string", "null" ], "format": "uuid" }, "orderNumber": { "type": [ "string", "null" ] }, "packageCount": { "type": "integer" }, "partyName": { "type": [ "string", "null" ] }, "purchaseOrderId": { "type": [ "string", "null" ], "format": "uuid" }, "purchaseOrderNumber": { "type": [ "string", "null" ] }, "shippedAt": { "type": [ "string", "null" ] }, "status": { "$ref": "#/components/schemas/shipmentStatusSchema" }, "trackingNumber": { "type": [ "string", "null" ] }, "trackingUrl": { "type": [ "string", "null" ] }, "updatedAt": { "type": "string" }, "agendaItemId": { "type": [ "string", "null" ], "format": "uuid" }, "lines": { "type": "array", "items": { "$ref": "#/components/schemas/shipmentLineSchema" } }, "locationId": { "type": [ "string", "null" ], "format": "uuid" }, "locationName": { "type": [ "string", "null" ] }, "notes": { "type": [ "string", "null" ] }, "service": { "type": [ "string", "null" ] }, "shipTo": { "type": "object", "properties": { "city": { "type": [ "string", "null" ] }, "country": { "type": [ "string", "null" ] }, "line1": { "type": [ "string", "null" ] }, "line2": { "type": [ "string", "null" ] }, "name": { "type": [ "string", "null" ] }, "postalCode": { "type": [ "string", "null" ] } }, "required": [ "city", "country", "line1", "line2", "name", "postalCode" ] }, "statusOccurredAt": { "type": [ "string", "null" ] }, "weightGrams": { "type": [ "integer", "null" ] } }, "required": [ "carrier", "createdAt", "deliveredAt", "deliveryMethod", "direction", "formattedNumber", "id", "number", "orderId", "orderNumber", "packageCount", "partyName", "purchaseOrderId", "purchaseOrderNumber", "shippedAt", "status", "trackingNumber", "trackingUrl", "updatedAt", "agendaItemId", "lines", "locationId", "locationName", "notes", "service", "shipTo", "statusOccurredAt", "weightGrams" ] }, "shipmentStatusSchema": { "type": "string", "enum": [ "draft", "packed", "ready_for_pickup", "in_transit", "delivered", "picked_up", "exception", "canceled" ] } } ``` # Ship a shipment. (/docs/openapi/shipShipment) Mark an outbound shipment as shipped: stock leaves the warehouse and the order's fulfillment progresses. Returns 409 `APPROVAL_REQUIRED` with an `approvalId` when an approval policy wants a decision first. ## POST /v1/organizations/{organizationId}/shipments/{shipmentId}/ship ```json { "operationId": "shipShipment", "summary": "Ship a shipment.", "description": "Mark an outbound shipment as shipped: stock leaves the warehouse and the order's fulfillment progresses. Returns 409 `APPROVAL_REQUIRED` with an `approvalId` when an approval policy wants a decision first.", "tags": [ "Shipments" ], "parameters": [ { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" }, { "in": "path", "name": "shipmentId", "required": true, "schema": { "type": "string", "format": "uuid" }, "example": "123e4567-e89b-12d3-a456-426614174000" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "responses": { "200": { "description": "Returns the shipped shipment.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/shipmentSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "orderDeliveryMethodSchema": { "type": "string", "enum": [ "pickup", "own_delivery", "carrier", "digital", "none" ] }, "organizationIdSchema": { "type": "string", "format": "uuid" }, "shipmentDirectionSchema": { "type": "string", "enum": [ "outbound", "inbound", "return" ] }, "shipmentLineSchema": { "type": "object", "properties": { "expiresOn": { "type": [ "string", "null" ] }, "id": { "type": "string", "format": "uuid" }, "lotNumber": { "type": [ "string", "null" ] }, "orderLineId": { "type": [ "string", "null" ], "format": "uuid" }, "position": { "type": "number" }, "productId": { "type": [ "string", "null" ], "format": "uuid" }, "purchaseOrderLineId": { "type": [ "string", "null" ], "format": "uuid" }, "quantity": { "type": "number" }, "returnDisposition": { "anyOf": [ { "$ref": "#/components/schemas/shipmentReturnDispositionSchema" }, { "type": "null" } ] }, "serialNumber": { "type": [ "string", "null" ] }, "sku": { "type": [ "string", "null" ] }, "title": { "type": "string" }, "variantId": { "type": [ "string", "null" ], "format": "uuid" } }, "required": [ "expiresOn", "id", "lotNumber", "orderLineId", "position", "productId", "purchaseOrderLineId", "quantity", "returnDisposition", "serialNumber", "sku", "title", "variantId" ] }, "shipmentReturnDispositionSchema": { "type": "string", "enum": [ "restock", "quarantine", "write_off" ] }, "shipmentSchema": { "type": "object", "properties": { "carrier": { "type": [ "string", "null" ] }, "createdAt": { "type": "string" }, "deliveredAt": { "type": [ "string", "null" ] }, "deliveryMethod": { "$ref": "#/components/schemas/orderDeliveryMethodSchema" }, "direction": { "$ref": "#/components/schemas/shipmentDirectionSchema" }, "formattedNumber": { "type": "string" }, "id": { "type": "string", "format": "uuid" }, "number": { "type": "integer" }, "orderId": { "type": [ "string", "null" ], "format": "uuid" }, "orderNumber": { "type": [ "string", "null" ] }, "packageCount": { "type": "integer" }, "partyName": { "type": [ "string", "null" ] }, "purchaseOrderId": { "type": [ "string", "null" ], "format": "uuid" }, "purchaseOrderNumber": { "type": [ "string", "null" ] }, "shippedAt": { "type": [ "string", "null" ] }, "status": { "$ref": "#/components/schemas/shipmentStatusSchema" }, "trackingNumber": { "type": [ "string", "null" ] }, "trackingUrl": { "type": [ "string", "null" ] }, "updatedAt": { "type": "string" }, "agendaItemId": { "type": [ "string", "null" ], "format": "uuid" }, "lines": { "type": "array", "items": { "$ref": "#/components/schemas/shipmentLineSchema" } }, "locationId": { "type": [ "string", "null" ], "format": "uuid" }, "locationName": { "type": [ "string", "null" ] }, "notes": { "type": [ "string", "null" ] }, "service": { "type": [ "string", "null" ] }, "shipTo": { "type": "object", "properties": { "city": { "type": [ "string", "null" ] }, "country": { "type": [ "string", "null" ] }, "line1": { "type": [ "string", "null" ] }, "line2": { "type": [ "string", "null" ] }, "name": { "type": [ "string", "null" ] }, "postalCode": { "type": [ "string", "null" ] } }, "required": [ "city", "country", "line1", "line2", "name", "postalCode" ] }, "statusOccurredAt": { "type": [ "string", "null" ] }, "weightGrams": { "type": [ "integer", "null" ] } }, "required": [ "carrier", "createdAt", "deliveredAt", "deliveryMethod", "direction", "formattedNumber", "id", "number", "orderId", "orderNumber", "packageCount", "partyName", "purchaseOrderId", "purchaseOrderNumber", "shippedAt", "status", "trackingNumber", "trackingUrl", "updatedAt", "agendaItemId", "lines", "locationId", "locationName", "notes", "service", "shipTo", "statusOccurredAt", "weightGrams" ] }, "shipmentStatusSchema": { "type": "string", "enum": [ "draft", "packed", "ready_for_pickup", "in_transit", "delivered", "picked_up", "exception", "canceled" ] } } ``` # List tags. (/docs/openapi/listTags) Return tags for an organization, optionally filtered by kind. ## GET /v1/organizations/{organizationId}/tags ```json { "operationId": "listTags", "summary": "List tags.", "description": "Return tags for an organization, optionally filtered by kind.", "tags": [ "Tags" ], "parameters": [ { "in": "query", "name": "kind", "required": false, "schema": { "allOf": [ { "$ref": "#/components/schemas/tagKindSchema" } ] }, "example": "example" }, { "in": "query", "name": "limit", "required": false, "schema": { "type": "string" }, "example": "example" }, { "in": "query", "name": "offset", "required": false, "schema": { "type": "string" }, "example": "example" }, { "in": "query", "name": "q", "required": false, "schema": { "type": "string" }, "example": "example" }, { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "responses": { "200": { "description": "Returns paginated tags.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/tagsListResponseSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "organizationIdSchema": { "type": "string", "format": "uuid" }, "tagKindSchema": { "type": "string", "enum": [ "task", "customer", "product" ] }, "tagsListResponseSchema": { "type": "object" } } ``` # Create a tag. (/docs/openapi/createTag) Create a tag for the specified organization. ## POST /v1/organizations/{organizationId}/tags ```json { "operationId": "createTag", "summary": "Create a tag.", "description": "Create a tag for the specified organization.", "tags": [ "Tags" ], "parameters": [ { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/tagCreateInputSchema" } } }, "description": "The tag payload.", "required": true }, "responses": { "201": { "description": "Returns the created tag.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/organizationTagSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "organizationIdSchema": { "type": "string", "format": "uuid" }, "organizationTagSchema": { "type": "object", "properties": { "color": { "anyOf": [ { "$ref": "#/components/schemas/tagColorSchema" }, { "type": "null" } ] }, "createdAt": { "type": "string", "format": "date-time" }, "description": { "type": [ "string", "null" ] }, "id": { "$ref": "#/components/schemas/tagIdSchema" }, "kind": { "$ref": "#/components/schemas/tagKindSchema" }, "name": { "type": "string", "minLength": 1, "maxLength": 80 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "position": { "type": "integer", "minimum": 0 }, "slug": { "$ref": "#/components/schemas/tagSlugSchema" }, "updatedAt": { "type": "string", "format": "date-time" } }, "required": [ "color", "createdAt", "description", "id", "kind", "name", "organizationId", "position", "slug", "updatedAt" ] }, "tagColorSchema": { "type": "string", "pattern": "^#(?:[0-9a-f]{3}|[0-9a-f]{6})$" }, "tagCreateInputSchema": { "type": "object", "properties": { "color": { "type": [ "string", "null" ], "pattern": "^#(?:[0-9a-f]{3}|[0-9a-f]{6})$" }, "description": { "type": [ "string", "null" ], "maxLength": 4000 }, "kind": { "$ref": "#/components/schemas/tagKindSchema" }, "name": { "type": "string", "minLength": 1, "maxLength": 80 }, "slug": { "allOf": [ { "$ref": "#/components/schemas/tagSlugSchema" } ] } }, "required": [ "kind", "name" ] }, "tagIdSchema": { "type": "string", "format": "uuid" }, "tagKindSchema": { "type": "string", "enum": [ "task", "customer", "product" ] }, "tagSlugSchema": { "type": "string", "minLength": 1, "maxLength": 160, "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$" } } ``` # Get a tag. (/docs/openapi/getTag) Return a tag for the specified organization. ## GET /v1/organizations/{organizationId}/tags/{tagId} ```json { "operationId": "getTag", "summary": "Get a tag.", "description": "Return a tag for the specified organization.", "tags": [ "Tags" ], "parameters": [ { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" }, { "in": "path", "name": "tagId", "required": true, "schema": { "$ref": "#/components/schemas/tagIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "responses": { "200": { "description": "Returns the requested tag.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/organizationTagSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "organizationIdSchema": { "type": "string", "format": "uuid" }, "organizationTagSchema": { "type": "object", "properties": { "color": { "anyOf": [ { "$ref": "#/components/schemas/tagColorSchema" }, { "type": "null" } ] }, "createdAt": { "type": "string", "format": "date-time" }, "description": { "type": [ "string", "null" ] }, "id": { "$ref": "#/components/schemas/tagIdSchema" }, "kind": { "$ref": "#/components/schemas/tagKindSchema" }, "name": { "type": "string", "minLength": 1, "maxLength": 80 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "position": { "type": "integer", "minimum": 0 }, "slug": { "$ref": "#/components/schemas/tagSlugSchema" }, "updatedAt": { "type": "string", "format": "date-time" } }, "required": [ "color", "createdAt", "description", "id", "kind", "name", "organizationId", "position", "slug", "updatedAt" ] }, "tagColorSchema": { "type": "string", "pattern": "^#(?:[0-9a-f]{3}|[0-9a-f]{6})$" }, "tagIdSchema": { "type": "string", "format": "uuid" }, "tagKindSchema": { "type": "string", "enum": [ "task", "customer", "product" ] }, "tagSlugSchema": { "type": "string", "minLength": 1, "maxLength": 160, "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$" } } ``` # Update a tag. (/docs/openapi/updateTag) Update a tag for the specified organization. ## PATCH /v1/organizations/{organizationId}/tags/{tagId} ```json { "operationId": "updateTag", "summary": "Update a tag.", "description": "Update a tag for the specified organization.", "tags": [ "Tags" ], "parameters": [ { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" }, { "in": "path", "name": "tagId", "required": true, "schema": { "$ref": "#/components/schemas/tagIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/tagUpdateInputSchema" } } }, "description": "The tag patch payload.", "required": true }, "responses": { "200": { "description": "Returns the updated tag.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/organizationTagSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "organizationIdSchema": { "type": "string", "format": "uuid" }, "organizationTagSchema": { "type": "object", "properties": { "color": { "anyOf": [ { "$ref": "#/components/schemas/tagColorSchema" }, { "type": "null" } ] }, "createdAt": { "type": "string", "format": "date-time" }, "description": { "type": [ "string", "null" ] }, "id": { "$ref": "#/components/schemas/tagIdSchema" }, "kind": { "$ref": "#/components/schemas/tagKindSchema" }, "name": { "type": "string", "minLength": 1, "maxLength": 80 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "position": { "type": "integer", "minimum": 0 }, "slug": { "$ref": "#/components/schemas/tagSlugSchema" }, "updatedAt": { "type": "string", "format": "date-time" } }, "required": [ "color", "createdAt", "description", "id", "kind", "name", "organizationId", "position", "slug", "updatedAt" ] }, "tagColorSchema": { "type": "string", "pattern": "^#(?:[0-9a-f]{3}|[0-9a-f]{6})$" }, "tagIdSchema": { "type": "string", "format": "uuid" }, "tagKindSchema": { "type": "string", "enum": [ "task", "customer", "product" ] }, "tagSlugSchema": { "type": "string", "minLength": 1, "maxLength": 160, "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$" }, "tagUpdateInputSchema": { "type": "object", "properties": { "color": { "type": [ "string", "null" ], "pattern": "^#(?:[0-9a-f]{3}|[0-9a-f]{6})$" }, "description": { "type": [ "string", "null" ], "maxLength": 4000 }, "kind": { "allOf": [ { "$ref": "#/components/schemas/tagKindSchema" } ] }, "name": { "type": "string", "minLength": 1, "maxLength": 80 }, "slug": { "allOf": [ { "$ref": "#/components/schemas/tagSlugSchema" } ] } } } } ``` # Delete a tag. (/docs/openapi/deleteTag) Delete a tag for the specified organization. ## DELETE /v1/organizations/{organizationId}/tags/{tagId} ```json { "operationId": "deleteTag", "summary": "Delete a tag.", "description": "Delete a tag for the specified organization.", "tags": [ "Tags" ], "parameters": [ { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" }, { "in": "path", "name": "tagId", "required": true, "schema": { "$ref": "#/components/schemas/tagIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "responses": { "200": { "description": "Returns a success payload when the tag is deleted.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/publicApiSuccessResponseSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "organizationIdSchema": { "type": "string", "format": "uuid" }, "publicApiSuccessResponseSchema": { "type": "object", "properties": { "success": { "type": "boolean", "enum": [ true ] } }, "required": [ "success" ] }, "tagIdSchema": { "type": "string", "format": "uuid" } } ``` # List tasks. (/docs/openapi/listTasks) Return tasks for an organization the authenticated user can access. ## GET /v1/organizations/{organizationId}/tasks ```json { "operationId": "listTasks", "summary": "List tasks.", "description": "Return tasks for an organization the authenticated user can access.", "tags": [ "Tasks" ], "parameters": [ { "in": "query", "name": "assigneeId", "required": false, "schema": { "type": "string", "format": "uuid" }, "example": "123e4567-e89b-12d3-a456-426614174000" }, { "in": "query", "name": "assigneeIds", "required": false, "schema": { "type": "array", "items": { "type": "string", "format": "uuid" }, "maxItems": 100 }, "example": "example" }, { "in": "query", "name": "customerId", "required": false, "schema": { "type": "string", "format": "uuid" }, "example": "123e4567-e89b-12d3-a456-426614174000" }, { "in": "query", "name": "customerIds", "required": false, "schema": { "type": "array", "items": { "type": "string", "format": "uuid" }, "maxItems": 100 }, "example": "example" }, { "in": "query", "name": "dueDateBuckets", "required": false, "schema": { "type": "array", "items": { "$ref": "#/components/schemas/taskDueDateBucketSchema" }, "maxItems": 5 }, "example": "example" }, { "in": "query", "name": "include", "required": false, "schema": { "type": "array", "items": { "$ref": "#/components/schemas/taskIncludeFilterSchema" }, "maxItems": 2 }, "example": "example" }, { "in": "query", "name": "noCustomer", "required": false, "schema": { "allOf": [ { "$ref": "#/components/schemas/taskListBooleanSchema" } ] }, "example": "example" }, { "in": "query", "name": "priorities", "required": false, "schema": { "type": "array", "items": { "$ref": "#/components/schemas/taskPrioritySchema" }, "maxItems": 20 }, "example": "example" }, { "in": "query", "name": "priority", "required": false, "schema": { "allOf": [ { "$ref": "#/components/schemas/taskPrioritySchema" } ] }, "example": "example" }, { "in": "query", "name": "sortBy", "required": false, "schema": { "allOf": [ { "$ref": "#/components/schemas/taskListSortSchema" } ] }, "example": "example" }, { "in": "query", "name": "sortDirection", "required": false, "schema": { "type": "string", "enum": [ "asc", "desc" ] }, "example": "asc" }, { "in": "query", "name": "status", "required": false, "schema": { "allOf": [ { "$ref": "#/components/schemas/taskStatusSchema" } ] }, "example": "example" }, { "in": "query", "name": "statuses", "required": false, "schema": { "type": "array", "items": { "$ref": "#/components/schemas/taskStatusSchema" }, "maxItems": 20 }, "example": "example" }, { "in": "query", "name": "tagId", "required": false, "schema": { "type": "string", "format": "uuid" }, "example": "123e4567-e89b-12d3-a456-426614174000" }, { "in": "query", "name": "tagIds", "required": false, "schema": { "type": "array", "items": { "type": "string", "format": "uuid" }, "maxItems": 100 }, "example": "example" }, { "in": "query", "name": "unassigned", "required": false, "schema": { "allOf": [ { "$ref": "#/components/schemas/taskListBooleanSchema" } ] }, "example": "example" }, { "in": "query", "name": "limit", "required": true, "schema": { "default": 50, "type": "integer", "minimum": 1, "maximum": 100 }, "example": 1 }, { "in": "query", "name": "offset", "required": true, "schema": { "default": 0, "type": "integer", "minimum": 0, "maximum": 9007199254740991 }, "example": 1 }, { "in": "query", "name": "q", "required": false, "schema": { "type": "string", "maxLength": 200 }, "example": "example" }, { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "responses": { "200": { "description": "Returns paginated tasks.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/tasksListResponseSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "dateFilterBucketSchema": { "type": "string", "enum": [ "overdue", "today", "this_week", "later", "none" ] }, "organizationIdSchema": { "type": "string", "format": "uuid" }, "taskDueDateBucketSchema": { "$ref": "#/components/schemas/dateFilterBucketSchema" }, "taskIncludeFilterSchema": { "type": "string", "enum": [ "done", "archived" ] }, "taskListBooleanSchema": { "anyOf": [ { "type": "boolean" }, { "type": "boolean" } ] }, "taskListSortSchema": { "type": "string", "enum": [ "title", "status", "priority", "dueAt" ] }, "taskPrioritySchema": { "type": "string", "enum": [ "low", "medium", "high", "urgent" ] }, "taskStatusSchema": { "type": "string", "enum": [ "todo", "in_progress", "on_hold", "done" ] }, "tasksListResponseSchema": { "type": "object" } } ``` # Create a task. (/docs/openapi/createTask) Create a task for the specified organization. ## POST /v1/organizations/{organizationId}/tasks ```json { "operationId": "createTask", "summary": "Create a task.", "description": "Create a task for the specified organization.", "tags": [ "Tasks" ], "parameters": [ { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/taskInputSchema" } } }, "description": "The task payload.", "required": true }, "responses": { "201": { "description": "Returns the created task.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/taskSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "isoDatetimeOrNull": { "type": [ "string", "null" ], "format": "date-time" }, "organizationIdSchema": { "type": "string", "format": "uuid" }, "organizationTagSchema": { "type": "object", "properties": { "color": { "anyOf": [ { "$ref": "#/components/schemas/tagColorSchema" }, { "type": "null" } ] }, "createdAt": { "type": "string", "format": "date-time" }, "description": { "type": [ "string", "null" ] }, "id": { "$ref": "#/components/schemas/tagIdSchema" }, "kind": { "$ref": "#/components/schemas/tagKindSchema" }, "name": { "type": "string", "minLength": 1, "maxLength": 80 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "position": { "type": "integer", "minimum": 0 }, "slug": { "$ref": "#/components/schemas/tagSlugSchema" }, "updatedAt": { "type": "string", "format": "date-time" } }, "required": [ "color", "createdAt", "description", "id", "kind", "name", "organizationId", "position", "slug", "updatedAt" ] }, "tagColorSchema": { "type": "string", "pattern": "^#(?:[0-9a-f]{3}|[0-9a-f]{6})$" }, "tagIdSchema": { "type": "string", "format": "uuid" }, "tagKindSchema": { "type": "string", "enum": [ "task", "customer", "product" ] }, "tagSlugSchema": { "type": "string", "minLength": 1, "maxLength": 160, "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$" }, "taskAssigneeSchema": { "type": "object", "properties": { "avatarUrl": { "type": [ "string", "null" ], "format": "uri" }, "disabledAt": { "type": [ "string", "null" ] }, "displayName": { "type": "string", "minLength": 1 }, "userId": { "type": "string", "format": "uuid" } }, "required": [ "avatarUrl", "disabledAt", "displayName", "userId" ] }, "taskDisplayNumberSchema": { "type": "string", "minLength": 1, "maxLength": 40 }, "taskIdSchema": { "type": "string", "format": "uuid" }, "taskInputSchema": { "type": "object", "properties": { "assigneeIds": { "type": "array", "items": { "type": "string", "format": "uuid" }, "maxItems": 50 }, "customerAssetId": { "type": [ "string", "null" ], "format": "uuid" }, "customerId": { "type": [ "string", "null" ], "format": "uuid" }, "description": { "type": [ "string", "null" ], "maxLength": 20000 }, "dueAt": { "allOf": [ { "$ref": "#/components/schemas/isoDatetimeOrNull" } ] }, "endAt": { "allOf": [ { "$ref": "#/components/schemas/isoDatetimeOrNull" } ] }, "parentTaskId": { "anyOf": [ { "$ref": "#/components/schemas/taskIdSchema" }, { "type": "null" } ] }, "priority": { "allOf": [ { "$ref": "#/components/schemas/taskPrioritySchema" } ] }, "startAt": { "allOf": [ { "$ref": "#/components/schemas/isoDatetimeOrNull" } ] }, "status": { "allOf": [ { "$ref": "#/components/schemas/taskStatusSchema" } ] }, "tagIds": { "type": "array", "items": { "type": "string", "format": "uuid" }, "maxItems": 50 }, "taskTemplateId": { "type": [ "string", "null" ], "format": "uuid" }, "title": { "type": "string", "minLength": 1, "maxLength": 240 } }, "required": [ "title" ] }, "taskNumberSchema": { "type": "integer", "exclusiveMinimum": 0, "maximum": 9007199254740991 }, "taskPrioritySchema": { "type": "string", "enum": [ "low", "medium", "high", "urgent" ] }, "taskSchema": { "type": "object", "properties": { "archivedAt": { "type": [ "string", "null" ], "format": "date-time" }, "assignees": { "type": "array", "items": { "$ref": "#/components/schemas/taskAssigneeSchema" } }, "createdAt": { "type": "string", "format": "date-time" }, "createdBy": { "type": [ "string", "null" ], "format": "uuid" }, "customerAssetId": { "type": [ "string", "null" ], "format": "uuid" }, "customerId": { "type": [ "string", "null" ], "format": "uuid" }, "description": { "type": [ "string", "null" ] }, "displayNumber": { "$ref": "#/components/schemas/taskDisplayNumberSchema" }, "dueAt": { "type": [ "string", "null" ], "format": "date-time" }, "endAt": { "type": [ "string", "null" ], "format": "date-time" }, "id": { "$ref": "#/components/schemas/taskIdSchema" }, "number": { "$ref": "#/components/schemas/taskNumberSchema" }, "organizationId": { "type": "string", "format": "uuid" }, "parentTaskId": { "anyOf": [ { "$ref": "#/components/schemas/taskIdSchema" }, { "type": "null" } ] }, "position": { "type": "number" }, "priority": { "$ref": "#/components/schemas/taskPrioritySchema" }, "startAt": { "type": [ "string", "null" ], "format": "date-time" }, "status": { "$ref": "#/components/schemas/taskStatusSchema" }, "tags": { "type": "array", "items": { "$ref": "#/components/schemas/organizationTagSchema" } }, "taskTemplateId": { "type": [ "string", "null" ], "format": "uuid" }, "title": { "type": "string" }, "updatedAt": { "type": "string", "format": "date-time" }, "updatedBy": { "type": [ "string", "null" ], "format": "uuid" } }, "required": [ "archivedAt", "assignees", "createdAt", "createdBy", "customerAssetId", "customerId", "description", "displayNumber", "dueAt", "endAt", "id", "number", "organizationId", "parentTaskId", "position", "priority", "startAt", "status", "tags", "taskTemplateId", "title", "updatedAt", "updatedBy" ] }, "taskStatusSchema": { "type": "string", "enum": [ "todo", "in_progress", "on_hold", "done" ] } } ``` # Get a task. (/docs/openapi/getTask) Return task detail for the specified organization. ## GET /v1/organizations/{organizationId}/tasks/{taskId} ```json { "operationId": "getTask", "summary": "Get a task.", "description": "Return task detail for the specified organization.", "tags": [ "Tasks" ], "parameters": [ { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" }, { "in": "path", "name": "taskId", "required": true, "schema": { "$ref": "#/components/schemas/taskIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "responses": { "200": { "description": "Returns the requested task.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/taskDetailSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "collaborationBlockSchema": { "type": "object", "discriminator": { "propertyName": "type" }, "oneOf": [ { "type": "object", "properties": { "content": { "$ref": "#/components/schemas/collaborationInlineContentSchema" }, "type": { "type": "string", "enum": [ "paragraph" ] } }, "required": [ "content", "type" ], "additionalProperties": false }, { "type": "object", "properties": { "items": { "type": "array", "items": { "$ref": "#/components/schemas/collaborationInlineContentSchema" }, "minItems": 1, "maxItems": 500 }, "type": { "type": "string", "enum": [ "bullet-list" ] } }, "required": [ "items", "type" ], "additionalProperties": false }, { "type": "object", "properties": { "items": { "type": "array", "items": { "$ref": "#/components/schemas/collaborationInlineContentSchema" }, "minItems": 1, "maxItems": 500 }, "type": { "type": "string", "enum": [ "numbered-list" ] } }, "required": [ "items", "type" ], "additionalProperties": false } ] }, "collaborationCommentAuthorSchema": { "type": "object", "properties": { "avatarUrl": { "type": [ "string", "null" ], "format": "uri" }, "displayName": { "type": "string", "minLength": 1 }, "userId": { "type": "string", "format": "uuid" } }, "required": [ "avatarUrl", "displayName", "userId" ] }, "collaborationDocumentSchema": { "type": "object", "properties": { "blocks": { "type": "array", "items": { "$ref": "#/components/schemas/collaborationBlockSchema" }, "minItems": 1, "maxItems": 1000 }, "version": { "type": "integer", "enum": [ 1 ] } }, "required": [ "blocks", "version" ], "additionalProperties": false }, "collaborationInlineContentSchema": { "type": "array", "items": { "$ref": "#/components/schemas/collaborationInlineSchema" }, "minItems": 1, "maxItems": 1000 }, "collaborationInlineSchema": { "type": "object", "discriminator": { "propertyName": "type" }, "oneOf": [ { "type": "object", "properties": { "text": { "type": "string", "minLength": 1, "maxLength": 20000 }, "type": { "type": "string", "enum": [ "text" ] } }, "required": [ "text", "type" ], "additionalProperties": false }, { "type": "object", "properties": { "reference": { "$ref": "#/components/schemas/collaborationReferenceSchema" }, "type": { "type": "string", "enum": [ "reference" ] } }, "required": [ "reference", "type" ], "additionalProperties": false } ] }, "collaborationReferenceSchema": { "type": "object", "discriminator": { "propertyName": "kind" }, "oneOf": [ { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "kind": { "type": "string", "enum": [ "user" ] }, "label": { "type": "string", "minLength": 1, "maxLength": 500 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "path": { "type": "string", "pattern": "^/", "maxLength": 2000 } }, "required": [ "id", "kind", "label", "organizationId", "path" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "kind": { "type": "string", "enum": [ "team" ] }, "label": { "type": "string", "minLength": 1, "maxLength": 500 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "path": { "type": "string", "pattern": "^/", "maxLength": 2000 } }, "required": [ "id", "kind", "label", "organizationId", "path" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "kind": { "type": "string", "enum": [ "task" ] }, "label": { "type": "string", "minLength": 1, "maxLength": 500 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "path": { "type": "string", "pattern": "^/", "maxLength": 2000 } }, "required": [ "id", "kind", "label", "organizationId", "path" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "kind": { "type": "string", "enum": [ "customer" ] }, "label": { "type": "string", "minLength": 1, "maxLength": 500 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "path": { "type": "string", "pattern": "^/", "maxLength": 2000 } }, "required": [ "id", "kind", "label", "organizationId", "path" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "$ref": "#/components/schemas/quoteIdSchema" }, "kind": { "type": "string", "enum": [ "quote" ] }, "label": { "type": "string", "minLength": 1, "maxLength": 500 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "path": { "type": "string", "pattern": "^/", "maxLength": 2000 } }, "required": [ "id", "kind", "label", "organizationId", "path" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "$ref": "#/components/schemas/invoiceIdSchema" }, "kind": { "type": "string", "enum": [ "invoice" ] }, "label": { "type": "string", "minLength": 1, "maxLength": 500 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "path": { "type": "string", "pattern": "^/", "maxLength": 2000 } }, "required": [ "id", "kind", "label", "organizationId", "path" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "$ref": "#/components/schemas/productIdSchema" }, "kind": { "type": "string", "enum": [ "product" ] }, "label": { "type": "string", "minLength": 1, "maxLength": 500 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "path": { "type": "string", "pattern": "^/", "maxLength": 2000 } }, "required": [ "id", "kind", "label", "organizationId", "path" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "kind": { "type": "string", "enum": [ "customer-asset" ] }, "label": { "type": "string", "minLength": 1, "maxLength": 500 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "path": { "type": "string", "pattern": "^/", "maxLength": 2000 } }, "required": [ "id", "kind", "label", "organizationId", "path" ], "additionalProperties": false } ] }, "invoiceIdSchema": { "type": "string", "format": "uuid" }, "organizationIdSchema": { "type": "string", "format": "uuid" }, "organizationTagSchema": { "type": "object", "properties": { "color": { "anyOf": [ { "$ref": "#/components/schemas/tagColorSchema" }, { "type": "null" } ] }, "createdAt": { "type": "string", "format": "date-time" }, "description": { "type": [ "string", "null" ] }, "id": { "$ref": "#/components/schemas/tagIdSchema" }, "kind": { "$ref": "#/components/schemas/tagKindSchema" }, "name": { "type": "string", "minLength": 1, "maxLength": 80 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "position": { "type": "integer", "minimum": 0 }, "slug": { "$ref": "#/components/schemas/tagSlugSchema" }, "updatedAt": { "type": "string", "format": "date-time" } }, "required": [ "color", "createdAt", "description", "id", "kind", "name", "organizationId", "position", "slug", "updatedAt" ] }, "productIdSchema": { "type": "string", "format": "uuid" }, "quoteIdSchema": { "type": "string", "format": "uuid" }, "tagColorSchema": { "type": "string", "pattern": "^#(?:[0-9a-f]{3}|[0-9a-f]{6})$" }, "tagIdSchema": { "type": "string", "format": "uuid" }, "tagKindSchema": { "type": "string", "enum": [ "task", "customer", "product" ] }, "tagSlugSchema": { "type": "string", "minLength": 1, "maxLength": 160, "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$" }, "taskAssigneeSchema": { "type": "object", "properties": { "avatarUrl": { "type": [ "string", "null" ], "format": "uri" }, "disabledAt": { "type": [ "string", "null" ] }, "displayName": { "type": "string", "minLength": 1 }, "userId": { "type": "string", "format": "uuid" } }, "required": [ "avatarUrl", "disabledAt", "displayName", "userId" ] }, "taskBilledInvoiceSchema": { "type": "object", "properties": { "displayNumber": { "type": [ "string", "null" ], "minLength": 1 }, "id": { "type": "string", "format": "uuid" }, "status": { "type": "string", "minLength": 1 } }, "required": [ "displayNumber", "id", "status" ] }, "taskCommentAuthorSchema": { "$ref": "#/components/schemas/collaborationCommentAuthorSchema" }, "taskCommentSchema": { "type": "object", "properties": { "author": { "anyOf": [ { "$ref": "#/components/schemas/taskCommentAuthorSchema" }, { "type": "null" } ] }, "authorId": { "type": [ "string", "null" ], "format": "uuid" }, "contentDoc": { "$ref": "#/components/schemas/collaborationDocumentSchema" }, "contentText": { "type": "string", "minLength": 1, "maxLength": 20000 }, "createdAt": { "type": "string", "format": "date-time" }, "id": { "type": "string", "format": "uuid" }, "organizationId": { "type": "string", "format": "uuid" }, "taskId": { "type": "string", "format": "uuid" }, "updatedAt": { "type": "string", "format": "date-time" } }, "required": [ "author", "authorId", "contentDoc", "contentText", "createdAt", "id", "organizationId", "taskId", "updatedAt" ] }, "taskDetailSchema": { "type": "object", "properties": { "comments": { "type": "array", "items": { "$ref": "#/components/schemas/taskCommentSchema" } }, "linkedInvoiceIds": { "type": "array", "items": { "type": "string", "format": "uuid" } }, "linkedQuoteIds": { "type": "array", "items": { "type": "string", "format": "uuid" } }, "materials": { "type": "array", "items": { "$ref": "#/components/schemas/taskMaterialSchema" } }, "subtasks": { "type": "array", "items": { "$ref": "#/components/schemas/taskListItemSchema" } }, "task": { "$ref": "#/components/schemas/taskSchema" } }, "required": [ "comments", "linkedInvoiceIds", "linkedQuoteIds", "materials", "subtasks", "task" ] }, "taskDisplayNumberSchema": { "type": "string", "minLength": 1, "maxLength": 40 }, "taskIdSchema": { "type": "string", "format": "uuid" }, "taskListItemSchema": { "type": "object", "properties": { "assignees": { "type": "array", "items": { "$ref": "#/components/schemas/taskAssigneeSchema" } }, "customerAssetId": { "type": [ "string", "null" ], "format": "uuid" }, "customerId": { "type": [ "string", "null" ], "format": "uuid" }, "displayNumber": { "$ref": "#/components/schemas/taskDisplayNumberSchema" }, "dueAt": { "type": [ "string", "null" ], "format": "date-time" }, "endAt": { "type": [ "string", "null" ], "format": "date-time" }, "id": { "$ref": "#/components/schemas/taskIdSchema" }, "number": { "$ref": "#/components/schemas/taskNumberSchema" }, "organizationId": { "type": "string", "format": "uuid" }, "parentTaskId": { "anyOf": [ { "$ref": "#/components/schemas/taskIdSchema" }, { "type": "null" } ] }, "position": { "type": "number" }, "priority": { "$ref": "#/components/schemas/taskPrioritySchema" }, "startAt": { "type": [ "string", "null" ], "format": "date-time" }, "status": { "$ref": "#/components/schemas/taskStatusSchema" }, "tags": { "type": "array", "items": { "$ref": "#/components/schemas/organizationTagSchema" } }, "title": { "type": "string" }, "updatedAt": { "type": "string", "format": "date-time" }, "commentCount": { "type": "integer", "minimum": 0 }, "subtaskDone": { "type": "integer", "minimum": 0 }, "subtaskTotal": { "type": "integer", "minimum": 0 } }, "required": [ "assignees", "customerAssetId", "customerId", "displayNumber", "dueAt", "endAt", "id", "number", "organizationId", "parentTaskId", "position", "priority", "startAt", "status", "tags", "title", "updatedAt", "commentCount", "subtaskDone", "subtaskTotal" ] }, "taskMaterialSchema": { "type": "object", "properties": { "billable": { "type": "boolean" }, "billedInvoice": { "anyOf": [ { "$ref": "#/components/schemas/taskBilledInvoiceSchema" }, { "type": "null" } ] }, "consumedAt": { "type": [ "string", "null" ], "format": "date-time" }, "createdAt": { "type": "string", "format": "date-time" }, "createdBy": { "type": [ "string", "null" ], "format": "uuid" }, "currency": { "type": [ "string", "null" ], "pattern": "^[A-Z]{3}$" }, "id": { "type": "string", "format": "uuid" }, "notes": { "type": [ "string", "null" ] }, "organizationId": { "type": "string", "format": "uuid" }, "position": { "type": "number", "minimum": 0 }, "productId": { "type": "string", "format": "uuid" }, "productName": { "type": "string", "minLength": 1 }, "productSku": { "type": [ "string", "null" ] }, "productSlug": { "type": [ "string", "null" ], "minLength": 1 }, "productUnitOfMeasure": { "type": [ "string", "null" ] }, "quantity": { "type": "number", "exclusiveMinimum": 0 }, "quoteId": { "type": [ "string", "null" ], "format": "uuid" }, "stockState": { "$ref": "#/components/schemas/taskMaterialStockStateSchema" }, "taskId": { "$ref": "#/components/schemas/taskIdSchema" }, "unit": { "type": [ "string", "null" ] }, "unitPrice": { "type": "number", "minimum": 0 }, "updatedAt": { "type": "string", "format": "date-time" }, "updatedBy": { "type": [ "string", "null" ], "format": "uuid" }, "variantId": { "type": "string", "format": "uuid" }, "variantTitle": { "type": [ "string", "null" ] }, "vatRate": { "type": "number", "minimum": 0, "maximum": 100 } }, "required": [ "billable", "billedInvoice", "consumedAt", "createdAt", "createdBy", "currency", "id", "notes", "organizationId", "position", "productId", "productName", "productSku", "productSlug", "productUnitOfMeasure", "quantity", "quoteId", "stockState", "taskId", "unit", "unitPrice", "updatedAt", "updatedBy", "variantId", "variantTitle", "vatRate" ] }, "taskMaterialStockStateSchema": { "type": "string", "enum": [ "untracked", "reserved", "consumed" ] }, "taskNumberSchema": { "type": "integer", "exclusiveMinimum": 0, "maximum": 9007199254740991 }, "taskPrioritySchema": { "type": "string", "enum": [ "low", "medium", "high", "urgent" ] }, "taskSchema": { "type": "object", "properties": { "archivedAt": { "type": [ "string", "null" ], "format": "date-time" }, "assignees": { "type": "array", "items": { "$ref": "#/components/schemas/taskAssigneeSchema" } }, "createdAt": { "type": "string", "format": "date-time" }, "createdBy": { "type": [ "string", "null" ], "format": "uuid" }, "customerAssetId": { "type": [ "string", "null" ], "format": "uuid" }, "customerId": { "type": [ "string", "null" ], "format": "uuid" }, "description": { "type": [ "string", "null" ] }, "displayNumber": { "$ref": "#/components/schemas/taskDisplayNumberSchema" }, "dueAt": { "type": [ "string", "null" ], "format": "date-time" }, "endAt": { "type": [ "string", "null" ], "format": "date-time" }, "id": { "$ref": "#/components/schemas/taskIdSchema" }, "number": { "$ref": "#/components/schemas/taskNumberSchema" }, "organizationId": { "type": "string", "format": "uuid" }, "parentTaskId": { "anyOf": [ { "$ref": "#/components/schemas/taskIdSchema" }, { "type": "null" } ] }, "position": { "type": "number" }, "priority": { "$ref": "#/components/schemas/taskPrioritySchema" }, "startAt": { "type": [ "string", "null" ], "format": "date-time" }, "status": { "$ref": "#/components/schemas/taskStatusSchema" }, "tags": { "type": "array", "items": { "$ref": "#/components/schemas/organizationTagSchema" } }, "taskTemplateId": { "type": [ "string", "null" ], "format": "uuid" }, "title": { "type": "string" }, "updatedAt": { "type": "string", "format": "date-time" }, "updatedBy": { "type": [ "string", "null" ], "format": "uuid" } }, "required": [ "archivedAt", "assignees", "createdAt", "createdBy", "customerAssetId", "customerId", "description", "displayNumber", "dueAt", "endAt", "id", "number", "organizationId", "parentTaskId", "position", "priority", "startAt", "status", "tags", "taskTemplateId", "title", "updatedAt", "updatedBy" ] }, "taskStatusSchema": { "type": "string", "enum": [ "todo", "in_progress", "on_hold", "done" ] } } ``` # Update a task. (/docs/openapi/updateTask) Update a task for the specified organization. ## PATCH /v1/organizations/{organizationId}/tasks/{taskId} ```json { "operationId": "updateTask", "summary": "Update a task.", "description": "Update a task for the specified organization.", "tags": [ "Tasks" ], "parameters": [ { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" }, { "in": "path", "name": "taskId", "required": true, "schema": { "$ref": "#/components/schemas/taskIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/taskInputSchema" } } }, "description": "The task payload.", "required": true }, "responses": { "200": { "description": "Returns the updated task.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/taskSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "isoDatetimeOrNull": { "type": [ "string", "null" ], "format": "date-time" }, "organizationIdSchema": { "type": "string", "format": "uuid" }, "organizationTagSchema": { "type": "object", "properties": { "color": { "anyOf": [ { "$ref": "#/components/schemas/tagColorSchema" }, { "type": "null" } ] }, "createdAt": { "type": "string", "format": "date-time" }, "description": { "type": [ "string", "null" ] }, "id": { "$ref": "#/components/schemas/tagIdSchema" }, "kind": { "$ref": "#/components/schemas/tagKindSchema" }, "name": { "type": "string", "minLength": 1, "maxLength": 80 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "position": { "type": "integer", "minimum": 0 }, "slug": { "$ref": "#/components/schemas/tagSlugSchema" }, "updatedAt": { "type": "string", "format": "date-time" } }, "required": [ "color", "createdAt", "description", "id", "kind", "name", "organizationId", "position", "slug", "updatedAt" ] }, "tagColorSchema": { "type": "string", "pattern": "^#(?:[0-9a-f]{3}|[0-9a-f]{6})$" }, "tagIdSchema": { "type": "string", "format": "uuid" }, "tagKindSchema": { "type": "string", "enum": [ "task", "customer", "product" ] }, "tagSlugSchema": { "type": "string", "minLength": 1, "maxLength": 160, "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$" }, "taskAssigneeSchema": { "type": "object", "properties": { "avatarUrl": { "type": [ "string", "null" ], "format": "uri" }, "disabledAt": { "type": [ "string", "null" ] }, "displayName": { "type": "string", "minLength": 1 }, "userId": { "type": "string", "format": "uuid" } }, "required": [ "avatarUrl", "disabledAt", "displayName", "userId" ] }, "taskDisplayNumberSchema": { "type": "string", "minLength": 1, "maxLength": 40 }, "taskIdSchema": { "type": "string", "format": "uuid" }, "taskInputSchema": { "type": "object", "properties": { "assigneeIds": { "type": "array", "items": { "type": "string", "format": "uuid" }, "maxItems": 50 }, "customerAssetId": { "type": [ "string", "null" ], "format": "uuid" }, "customerId": { "type": [ "string", "null" ], "format": "uuid" }, "description": { "type": [ "string", "null" ], "maxLength": 20000 }, "dueAt": { "allOf": [ { "$ref": "#/components/schemas/isoDatetimeOrNull" } ] }, "endAt": { "allOf": [ { "$ref": "#/components/schemas/isoDatetimeOrNull" } ] }, "parentTaskId": { "anyOf": [ { "$ref": "#/components/schemas/taskIdSchema" }, { "type": "null" } ] }, "priority": { "allOf": [ { "$ref": "#/components/schemas/taskPrioritySchema" } ] }, "startAt": { "allOf": [ { "$ref": "#/components/schemas/isoDatetimeOrNull" } ] }, "status": { "allOf": [ { "$ref": "#/components/schemas/taskStatusSchema" } ] }, "tagIds": { "type": "array", "items": { "type": "string", "format": "uuid" }, "maxItems": 50 }, "taskTemplateId": { "type": [ "string", "null" ], "format": "uuid" }, "title": { "type": "string", "minLength": 1, "maxLength": 240 } }, "required": [ "title" ] }, "taskNumberSchema": { "type": "integer", "exclusiveMinimum": 0, "maximum": 9007199254740991 }, "taskPrioritySchema": { "type": "string", "enum": [ "low", "medium", "high", "urgent" ] }, "taskSchema": { "type": "object", "properties": { "archivedAt": { "type": [ "string", "null" ], "format": "date-time" }, "assignees": { "type": "array", "items": { "$ref": "#/components/schemas/taskAssigneeSchema" } }, "createdAt": { "type": "string", "format": "date-time" }, "createdBy": { "type": [ "string", "null" ], "format": "uuid" }, "customerAssetId": { "type": [ "string", "null" ], "format": "uuid" }, "customerId": { "type": [ "string", "null" ], "format": "uuid" }, "description": { "type": [ "string", "null" ] }, "displayNumber": { "$ref": "#/components/schemas/taskDisplayNumberSchema" }, "dueAt": { "type": [ "string", "null" ], "format": "date-time" }, "endAt": { "type": [ "string", "null" ], "format": "date-time" }, "id": { "$ref": "#/components/schemas/taskIdSchema" }, "number": { "$ref": "#/components/schemas/taskNumberSchema" }, "organizationId": { "type": "string", "format": "uuid" }, "parentTaskId": { "anyOf": [ { "$ref": "#/components/schemas/taskIdSchema" }, { "type": "null" } ] }, "position": { "type": "number" }, "priority": { "$ref": "#/components/schemas/taskPrioritySchema" }, "startAt": { "type": [ "string", "null" ], "format": "date-time" }, "status": { "$ref": "#/components/schemas/taskStatusSchema" }, "tags": { "type": "array", "items": { "$ref": "#/components/schemas/organizationTagSchema" } }, "taskTemplateId": { "type": [ "string", "null" ], "format": "uuid" }, "title": { "type": "string" }, "updatedAt": { "type": "string", "format": "date-time" }, "updatedBy": { "type": [ "string", "null" ], "format": "uuid" } }, "required": [ "archivedAt", "assignees", "createdAt", "createdBy", "customerAssetId", "customerId", "description", "displayNumber", "dueAt", "endAt", "id", "number", "organizationId", "parentTaskId", "position", "priority", "startAt", "status", "tags", "taskTemplateId", "title", "updatedAt", "updatedBy" ] }, "taskStatusSchema": { "type": "string", "enum": [ "todo", "in_progress", "on_hold", "done" ] } } ``` # Delete a task. (/docs/openapi/deleteTask) Delete a task for the specified organization. ## DELETE /v1/organizations/{organizationId}/tasks/{taskId} ```json { "operationId": "deleteTask", "summary": "Delete a task.", "description": "Delete a task for the specified organization.", "tags": [ "Tasks" ], "parameters": [ { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" }, { "in": "path", "name": "taskId", "required": true, "schema": { "$ref": "#/components/schemas/taskIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "responses": { "200": { "description": "Returns a success payload when the task is deleted.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/publicApiSuccessResponseSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "organizationIdSchema": { "type": "string", "format": "uuid" }, "publicApiSuccessResponseSchema": { "type": "object", "properties": { "success": { "type": "boolean", "enum": [ true ] } }, "required": [ "success" ] }, "taskIdSchema": { "type": "string", "format": "uuid" } } ``` # Reorder tasks. (/docs/openapi/reorderTasks) Reorder task board items for the specified organization. ## POST /v1/organizations/{organizationId}/tasks/reorder ```json { "operationId": "reorderTasks", "summary": "Reorder tasks.", "description": "Reorder task board items for the specified organization.", "tags": [ "Tasks" ], "parameters": [ { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/taskReorderInputSchema" } } }, "description": "The task ordering payload." }, "responses": { "200": { "description": "Returns a success payload when tasks are reordered.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/publicApiSuccessResponseSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "organizationIdSchema": { "type": "string", "format": "uuid" }, "publicApiSuccessResponseSchema": { "type": "object", "properties": { "success": { "type": "boolean", "enum": [ true ] } }, "required": [ "success" ] }, "taskReorderInputSchema": { "type": "object", "properties": { "items": { "type": "array", "items": { "$ref": "#/components/schemas/taskReorderItemSchema" }, "minItems": 1, "maxItems": 500 } }, "required": [ "items" ] }, "taskReorderItemSchema": { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "position": { "type": "number" }, "status": { "$ref": "#/components/schemas/taskStatusSchema" } }, "required": [ "id", "position", "status" ] }, "taskStatusSchema": { "type": "string", "enum": [ "todo", "in_progress", "on_hold", "done" ] } } ``` # Set task assignees. (/docs/openapi/setTaskAssignees) Replace the users assigned to a task. ## PUT /v1/organizations/{organizationId}/tasks/{taskId}/assignees ```json { "operationId": "setTaskAssignees", "summary": "Set task assignees.", "description": "Replace the users assigned to a task.", "tags": [ "Tasks" ], "parameters": [ { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" }, { "in": "path", "name": "taskId", "required": true, "schema": { "$ref": "#/components/schemas/taskIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/taskAssigneesInputSchema" } } }, "description": "The assignee payload.", "required": true }, "responses": { "200": { "description": "Returns a success payload when assignees are updated.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/publicApiSuccessResponseSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "organizationIdSchema": { "type": "string", "format": "uuid" }, "publicApiSuccessResponseSchema": { "type": "object", "properties": { "success": { "type": "boolean", "enum": [ true ] } }, "required": [ "success" ] }, "taskAssigneesInputSchema": { "type": "object", "properties": { "userIds": { "type": "array", "items": { "type": "string", "format": "uuid" }, "maxItems": 50 } }, "required": [ "userIds" ] }, "taskIdSchema": { "type": "string", "format": "uuid" } } ``` # List task comments. (/docs/openapi/listTaskComments) Return comments for a task. ## GET /v1/organizations/{organizationId}/tasks/{taskId}/comments ```json { "operationId": "listTaskComments", "summary": "List task comments.", "description": "Return comments for a task.", "tags": [ "Tasks" ], "parameters": [ { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" }, { "in": "path", "name": "taskId", "required": true, "schema": { "$ref": "#/components/schemas/taskIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "responses": { "200": { "description": "Returns task comments.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/taskCommentsResponseSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "collaborationBlockSchema": { "type": "object", "discriminator": { "propertyName": "type" }, "oneOf": [ { "type": "object", "properties": { "content": { "$ref": "#/components/schemas/collaborationInlineContentSchema" }, "type": { "type": "string", "enum": [ "paragraph" ] } }, "required": [ "content", "type" ], "additionalProperties": false }, { "type": "object", "properties": { "items": { "type": "array", "items": { "$ref": "#/components/schemas/collaborationInlineContentSchema" }, "minItems": 1, "maxItems": 500 }, "type": { "type": "string", "enum": [ "bullet-list" ] } }, "required": [ "items", "type" ], "additionalProperties": false }, { "type": "object", "properties": { "items": { "type": "array", "items": { "$ref": "#/components/schemas/collaborationInlineContentSchema" }, "minItems": 1, "maxItems": 500 }, "type": { "type": "string", "enum": [ "numbered-list" ] } }, "required": [ "items", "type" ], "additionalProperties": false } ] }, "collaborationCommentAuthorSchema": { "type": "object", "properties": { "avatarUrl": { "type": [ "string", "null" ], "format": "uri" }, "displayName": { "type": "string", "minLength": 1 }, "userId": { "type": "string", "format": "uuid" } }, "required": [ "avatarUrl", "displayName", "userId" ] }, "collaborationDocumentSchema": { "type": "object", "properties": { "blocks": { "type": "array", "items": { "$ref": "#/components/schemas/collaborationBlockSchema" }, "minItems": 1, "maxItems": 1000 }, "version": { "type": "integer", "enum": [ 1 ] } }, "required": [ "blocks", "version" ], "additionalProperties": false }, "collaborationInlineContentSchema": { "type": "array", "items": { "$ref": "#/components/schemas/collaborationInlineSchema" }, "minItems": 1, "maxItems": 1000 }, "collaborationInlineSchema": { "type": "object", "discriminator": { "propertyName": "type" }, "oneOf": [ { "type": "object", "properties": { "text": { "type": "string", "minLength": 1, "maxLength": 20000 }, "type": { "type": "string", "enum": [ "text" ] } }, "required": [ "text", "type" ], "additionalProperties": false }, { "type": "object", "properties": { "reference": { "$ref": "#/components/schemas/collaborationReferenceSchema" }, "type": { "type": "string", "enum": [ "reference" ] } }, "required": [ "reference", "type" ], "additionalProperties": false } ] }, "collaborationReferenceSchema": { "type": "object", "discriminator": { "propertyName": "kind" }, "oneOf": [ { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "kind": { "type": "string", "enum": [ "user" ] }, "label": { "type": "string", "minLength": 1, "maxLength": 500 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "path": { "type": "string", "pattern": "^/", "maxLength": 2000 } }, "required": [ "id", "kind", "label", "organizationId", "path" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "kind": { "type": "string", "enum": [ "team" ] }, "label": { "type": "string", "minLength": 1, "maxLength": 500 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "path": { "type": "string", "pattern": "^/", "maxLength": 2000 } }, "required": [ "id", "kind", "label", "organizationId", "path" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "kind": { "type": "string", "enum": [ "task" ] }, "label": { "type": "string", "minLength": 1, "maxLength": 500 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "path": { "type": "string", "pattern": "^/", "maxLength": 2000 } }, "required": [ "id", "kind", "label", "organizationId", "path" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "kind": { "type": "string", "enum": [ "customer" ] }, "label": { "type": "string", "minLength": 1, "maxLength": 500 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "path": { "type": "string", "pattern": "^/", "maxLength": 2000 } }, "required": [ "id", "kind", "label", "organizationId", "path" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "$ref": "#/components/schemas/quoteIdSchema" }, "kind": { "type": "string", "enum": [ "quote" ] }, "label": { "type": "string", "minLength": 1, "maxLength": 500 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "path": { "type": "string", "pattern": "^/", "maxLength": 2000 } }, "required": [ "id", "kind", "label", "organizationId", "path" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "$ref": "#/components/schemas/invoiceIdSchema" }, "kind": { "type": "string", "enum": [ "invoice" ] }, "label": { "type": "string", "minLength": 1, "maxLength": 500 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "path": { "type": "string", "pattern": "^/", "maxLength": 2000 } }, "required": [ "id", "kind", "label", "organizationId", "path" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "$ref": "#/components/schemas/productIdSchema" }, "kind": { "type": "string", "enum": [ "product" ] }, "label": { "type": "string", "minLength": 1, "maxLength": 500 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "path": { "type": "string", "pattern": "^/", "maxLength": 2000 } }, "required": [ "id", "kind", "label", "organizationId", "path" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "kind": { "type": "string", "enum": [ "customer-asset" ] }, "label": { "type": "string", "minLength": 1, "maxLength": 500 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "path": { "type": "string", "pattern": "^/", "maxLength": 2000 } }, "required": [ "id", "kind", "label", "organizationId", "path" ], "additionalProperties": false } ] }, "invoiceIdSchema": { "type": "string", "format": "uuid" }, "listPaginationSchema": { "type": "object", "properties": { "hasMore": { "type": "boolean" }, "limit": { "type": "integer", "minimum": 1, "maximum": 100 }, "offset": { "type": "integer", "minimum": 0 }, "total": { "type": "integer", "minimum": 0 }, "totalIsEstimate": { "type": "boolean" } }, "required": [ "hasMore", "limit", "offset", "total" ] }, "organizationIdSchema": { "type": "string", "format": "uuid" }, "productIdSchema": { "type": "string", "format": "uuid" }, "quoteIdSchema": { "type": "string", "format": "uuid" }, "taskCommentAuthorSchema": { "$ref": "#/components/schemas/collaborationCommentAuthorSchema" }, "taskCommentSchema": { "type": "object", "properties": { "author": { "anyOf": [ { "$ref": "#/components/schemas/taskCommentAuthorSchema" }, { "type": "null" } ] }, "authorId": { "type": [ "string", "null" ], "format": "uuid" }, "contentDoc": { "$ref": "#/components/schemas/collaborationDocumentSchema" }, "contentText": { "type": "string", "minLength": 1, "maxLength": 20000 }, "createdAt": { "type": "string", "format": "date-time" }, "id": { "type": "string", "format": "uuid" }, "organizationId": { "type": "string", "format": "uuid" }, "taskId": { "type": "string", "format": "uuid" }, "updatedAt": { "type": "string", "format": "date-time" } }, "required": [ "author", "authorId", "contentDoc", "contentText", "createdAt", "id", "organizationId", "taskId", "updatedAt" ] }, "taskCommentsResponseSchema": { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/components/schemas/taskCommentSchema" } }, "pagination": { "$ref": "#/components/schemas/listPaginationSchema" } }, "required": [ "data", "pagination" ] }, "taskIdSchema": { "type": "string", "format": "uuid" } } ``` # Add a task comment. (/docs/openapi/addTaskComment) Add a comment to a task. ## POST /v1/organizations/{organizationId}/tasks/{taskId}/comments ```json { "operationId": "addTaskComment", "summary": "Add a task comment.", "description": "Add a comment to a task.", "tags": [ "Tasks" ], "parameters": [ { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" }, { "in": "path", "name": "taskId", "required": true, "schema": { "$ref": "#/components/schemas/taskIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/taskCommentInputSchema" } } }, "description": "The task comment payload." }, "responses": { "201": { "description": "Returns the created task comment.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/taskCommentSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "collaborationBlockSchema": { "type": "object", "discriminator": { "propertyName": "type" }, "oneOf": [ { "type": "object", "properties": { "content": { "$ref": "#/components/schemas/collaborationInlineContentSchema" }, "type": { "type": "string", "enum": [ "paragraph" ] } }, "required": [ "content", "type" ], "additionalProperties": false }, { "type": "object", "properties": { "items": { "type": "array", "items": { "$ref": "#/components/schemas/collaborationInlineContentSchema" }, "minItems": 1, "maxItems": 500 }, "type": { "type": "string", "enum": [ "bullet-list" ] } }, "required": [ "items", "type" ], "additionalProperties": false }, { "type": "object", "properties": { "items": { "type": "array", "items": { "$ref": "#/components/schemas/collaborationInlineContentSchema" }, "minItems": 1, "maxItems": 500 }, "type": { "type": "string", "enum": [ "numbered-list" ] } }, "required": [ "items", "type" ], "additionalProperties": false } ] }, "collaborationCommentAuthorSchema": { "type": "object", "properties": { "avatarUrl": { "type": [ "string", "null" ], "format": "uri" }, "displayName": { "type": "string", "minLength": 1 }, "userId": { "type": "string", "format": "uuid" } }, "required": [ "avatarUrl", "displayName", "userId" ] }, "collaborationComposerInputSchema": { "type": "object", "properties": { "selections": { "type": "array", "items": { "$ref": "#/components/schemas/collaborationComposerSelectionSchema" }, "maxItems": 100 }, "text": { "type": "string", "minLength": 1, "maxLength": 20000 } }, "required": [ "selections", "text" ], "additionalProperties": false }, "collaborationComposerSelectionSchema": { "type": "object", "properties": { "end": { "type": "integer", "exclusiveMinimum": 0, "maximum": 20000 }, "reference": { "$ref": "#/components/schemas/collaborationReferenceIdSchema" }, "start": { "type": "integer", "minimum": 0, "maximum": 20000 } }, "required": [ "end", "reference", "start" ], "additionalProperties": false }, "collaborationDocumentSchema": { "type": "object", "properties": { "blocks": { "type": "array", "items": { "$ref": "#/components/schemas/collaborationBlockSchema" }, "minItems": 1, "maxItems": 1000 }, "version": { "type": "integer", "enum": [ 1 ] } }, "required": [ "blocks", "version" ], "additionalProperties": false }, "collaborationInlineContentSchema": { "type": "array", "items": { "$ref": "#/components/schemas/collaborationInlineSchema" }, "minItems": 1, "maxItems": 1000 }, "collaborationInlineSchema": { "type": "object", "discriminator": { "propertyName": "type" }, "oneOf": [ { "type": "object", "properties": { "text": { "type": "string", "minLength": 1, "maxLength": 20000 }, "type": { "type": "string", "enum": [ "text" ] } }, "required": [ "text", "type" ], "additionalProperties": false }, { "type": "object", "properties": { "reference": { "$ref": "#/components/schemas/collaborationReferenceSchema" }, "type": { "type": "string", "enum": [ "reference" ] } }, "required": [ "reference", "type" ], "additionalProperties": false } ] }, "collaborationReferenceIdSchema": { "type": "object", "discriminator": { "propertyName": "kind" }, "oneOf": [ { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "kind": { "type": "string", "enum": [ "user" ] } }, "required": [ "id", "kind" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "kind": { "type": "string", "enum": [ "team" ] } }, "required": [ "id", "kind" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "kind": { "type": "string", "enum": [ "task" ] } }, "required": [ "id", "kind" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "kind": { "type": "string", "enum": [ "customer" ] } }, "required": [ "id", "kind" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "$ref": "#/components/schemas/quoteIdSchema" }, "kind": { "type": "string", "enum": [ "quote" ] } }, "required": [ "id", "kind" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "$ref": "#/components/schemas/invoiceIdSchema" }, "kind": { "type": "string", "enum": [ "invoice" ] } }, "required": [ "id", "kind" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "$ref": "#/components/schemas/productIdSchema" }, "kind": { "type": "string", "enum": [ "product" ] } }, "required": [ "id", "kind" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "kind": { "type": "string", "enum": [ "customer-asset" ] } }, "required": [ "id", "kind" ], "additionalProperties": false } ] }, "collaborationReferenceSchema": { "type": "object", "discriminator": { "propertyName": "kind" }, "oneOf": [ { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "kind": { "type": "string", "enum": [ "user" ] }, "label": { "type": "string", "minLength": 1, "maxLength": 500 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "path": { "type": "string", "pattern": "^/", "maxLength": 2000 } }, "required": [ "id", "kind", "label", "organizationId", "path" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "kind": { "type": "string", "enum": [ "team" ] }, "label": { "type": "string", "minLength": 1, "maxLength": 500 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "path": { "type": "string", "pattern": "^/", "maxLength": 2000 } }, "required": [ "id", "kind", "label", "organizationId", "path" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "kind": { "type": "string", "enum": [ "task" ] }, "label": { "type": "string", "minLength": 1, "maxLength": 500 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "path": { "type": "string", "pattern": "^/", "maxLength": 2000 } }, "required": [ "id", "kind", "label", "organizationId", "path" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "kind": { "type": "string", "enum": [ "customer" ] }, "label": { "type": "string", "minLength": 1, "maxLength": 500 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "path": { "type": "string", "pattern": "^/", "maxLength": 2000 } }, "required": [ "id", "kind", "label", "organizationId", "path" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "$ref": "#/components/schemas/quoteIdSchema" }, "kind": { "type": "string", "enum": [ "quote" ] }, "label": { "type": "string", "minLength": 1, "maxLength": 500 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "path": { "type": "string", "pattern": "^/", "maxLength": 2000 } }, "required": [ "id", "kind", "label", "organizationId", "path" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "$ref": "#/components/schemas/invoiceIdSchema" }, "kind": { "type": "string", "enum": [ "invoice" ] }, "label": { "type": "string", "minLength": 1, "maxLength": 500 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "path": { "type": "string", "pattern": "^/", "maxLength": 2000 } }, "required": [ "id", "kind", "label", "organizationId", "path" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "$ref": "#/components/schemas/productIdSchema" }, "kind": { "type": "string", "enum": [ "product" ] }, "label": { "type": "string", "minLength": 1, "maxLength": 500 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "path": { "type": "string", "pattern": "^/", "maxLength": 2000 } }, "required": [ "id", "kind", "label", "organizationId", "path" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "kind": { "type": "string", "enum": [ "customer-asset" ] }, "label": { "type": "string", "minLength": 1, "maxLength": 500 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "path": { "type": "string", "pattern": "^/", "maxLength": 2000 } }, "required": [ "id", "kind", "label", "organizationId", "path" ], "additionalProperties": false } ] }, "invoiceIdSchema": { "type": "string", "format": "uuid" }, "organizationIdSchema": { "type": "string", "format": "uuid" }, "productIdSchema": { "type": "string", "format": "uuid" }, "quoteIdSchema": { "type": "string", "format": "uuid" }, "taskCommentAuthorSchema": { "$ref": "#/components/schemas/collaborationCommentAuthorSchema" }, "taskCommentInputSchema": { "$ref": "#/components/schemas/collaborationComposerInputSchema" }, "taskCommentSchema": { "type": "object", "properties": { "author": { "anyOf": [ { "$ref": "#/components/schemas/taskCommentAuthorSchema" }, { "type": "null" } ] }, "authorId": { "type": [ "string", "null" ], "format": "uuid" }, "contentDoc": { "$ref": "#/components/schemas/collaborationDocumentSchema" }, "contentText": { "type": "string", "minLength": 1, "maxLength": 20000 }, "createdAt": { "type": "string", "format": "date-time" }, "id": { "type": "string", "format": "uuid" }, "organizationId": { "type": "string", "format": "uuid" }, "taskId": { "type": "string", "format": "uuid" }, "updatedAt": { "type": "string", "format": "date-time" } }, "required": [ "author", "authorId", "contentDoc", "contentText", "createdAt", "id", "organizationId", "taskId", "updatedAt" ] }, "taskIdSchema": { "type": "string", "format": "uuid" } } ``` # Read a task lifecycle. (/docs/openapi/getTaskLifecycle) The task's lifecycle: the pinned workflow version, the current stage, the stage history, and what the lifecycle waits on now. `lifecycle` is null when no lifecycle run started for the task. ## GET /v1/organizations/{organizationId}/tasks/{taskId}/lifecycle ```json { "operationId": "getTaskLifecycle", "summary": "Read a task lifecycle.", "description": "The task's lifecycle: the pinned workflow version, the current stage, the stage history, and what the lifecycle waits on now. `lifecycle` is null when no lifecycle run started for the task.", "tags": [ "Tasks" ], "parameters": [ { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" }, { "in": "path", "name": "taskId", "required": true, "schema": { "$ref": "#/components/schemas/taskIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "responses": { "200": { "description": "Returns the task's lifecycle.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/recordLifecycleSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "dateTimeSchema": { "type": "string" }, "organizationIdSchema": { "type": "string", "format": "uuid" }, "recordLifecycleSchema": { "type": "object", "properties": { "definition": { "type": [ "object", "null" ], "properties": { "id": { "type": "string", "format": "uuid" }, "key": { "type": "string" }, "name": { "type": "string" }, "source": { "type": "string", "enum": [ "organization", "record", "system", "template" ] }, "version": { "type": "integer", "exclusiveMinimum": 0 } }, "required": [ "id", "key", "name", "source", "version" ] }, "events": { "type": "array", "items": { "$ref": "#/components/schemas/workflowLifecycleEventSchema" } }, "lifecycle": { "anyOf": [ { "$ref": "#/components/schemas/workflowLifecycleSchema" }, { "type": "null" } ] }, "waiting": { "type": [ "object", "null" ], "properties": { "at": { "anyOf": [ { "$ref": "#/components/schemas/dateTimeSchema" }, { "type": "null" } ] }, "events": { "type": "array", "items": { "type": "string" } }, "kind": { "type": "string", "enum": [ "approval", "delay", "event", "manual" ] }, "nextStageKey": { "type": [ "string", "null" ] }, "nextStageLabel": { "type": [ "string", "null" ] } }, "required": [ "at", "events", "kind", "nextStageKey", "nextStageLabel" ] } }, "required": [ "definition", "events", "lifecycle", "waiting" ] }, "taskIdSchema": { "type": "string", "format": "uuid" }, "workflowLifecycleEventSchema": { "type": "object", "properties": { "actorCustomerContactId": { "type": [ "string", "null" ], "format": "uuid" }, "actorUserId": { "type": [ "string", "null" ], "format": "uuid" }, "clearedAt": { "anyOf": [ { "$ref": "#/components/schemas/dateTimeSchema" }, { "type": "null" } ] }, "createdAt": { "$ref": "#/components/schemas/dateTimeSchema" }, "display": { "type": "string", "enum": [ "main", "sub", "flag", "hidden" ] }, "id": { "type": "string", "format": "uuid" }, "label": { "type": "string", "minLength": 1 }, "lifecycleId": { "type": "string", "format": "uuid" }, "organizationId": { "type": "string", "format": "uuid" }, "outcome": { "type": "string", "enum": [ "progress", "success", "failure", "neutral" ] }, "reachedAt": { "$ref": "#/components/schemas/dateTimeSchema" }, "runStepId": { "type": [ "string", "null" ], "format": "uuid" }, "sourceRunId": { "type": [ "string", "null" ], "format": "uuid" }, "stageKey": { "type": "string", "minLength": 1 } }, "required": [ "actorCustomerContactId", "actorUserId", "clearedAt", "createdAt", "display", "id", "label", "lifecycleId", "organizationId", "outcome", "reachedAt", "runStepId", "sourceRunId", "stageKey" ] }, "workflowLifecycleSchema": { "type": "object", "properties": { "createdAt": { "$ref": "#/components/schemas/dateTimeSchema" }, "currentStageKey": { "type": [ "string", "null" ] }, "definitionId": { "type": "string", "format": "uuid" }, "definitionVersion": { "type": "integer", "exclusiveMinimum": 0 }, "endedAt": { "anyOf": [ { "$ref": "#/components/schemas/dateTimeSchema" }, { "type": "null" } ] }, "id": { "type": "string", "format": "uuid" }, "organizationId": { "type": "string", "format": "uuid" }, "pauseReason": { "type": [ "string", "null" ] }, "pausedAt": { "anyOf": [ { "$ref": "#/components/schemas/dateTimeSchema" }, { "type": "null" } ] }, "pausedBy": { "type": [ "string", "null" ], "format": "uuid" }, "pausedUntil": { "anyOf": [ { "$ref": "#/components/schemas/dateTimeSchema" }, { "type": "null" } ] }, "runId": { "type": [ "string", "null" ], "format": "uuid" }, "startedAt": { "$ref": "#/components/schemas/dateTimeSchema" }, "subjectId": { "type": "string", "format": "uuid" }, "subjectType": { "$ref": "#/components/schemas/workflowSubjectTypeSchema" }, "updatedAt": { "$ref": "#/components/schemas/dateTimeSchema" } }, "required": [ "createdAt", "currentStageKey", "definitionId", "definitionVersion", "endedAt", "id", "organizationId", "pauseReason", "pausedAt", "pausedBy", "pausedUntil", "runId", "startedAt", "subjectId", "subjectType", "updatedAt" ] }, "workflowSubjectTypeSchema": { "type": "string", "enum": [ "invoice", "order", "purchase_order", "quote", "shipment", "task" ] } } ``` # Set task status. (/docs/openapi/setTaskStatus) Move a task to a status and board position. ## PATCH /v1/organizations/{organizationId}/tasks/{taskId}/status ```json { "operationId": "setTaskStatus", "summary": "Set task status.", "description": "Move a task to a status and board position.", "tags": [ "Tasks" ], "parameters": [ { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" }, { "in": "path", "name": "taskId", "required": true, "schema": { "$ref": "#/components/schemas/taskIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/taskStatusUpdateInputSchema" } } }, "description": "The status and position payload.", "required": true }, "responses": { "200": { "description": "Returns a success payload when the status is updated.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/publicApiSuccessResponseSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "organizationIdSchema": { "type": "string", "format": "uuid" }, "publicApiSuccessResponseSchema": { "type": "object", "properties": { "success": { "type": "boolean", "enum": [ true ] } }, "required": [ "success" ] }, "taskIdSchema": { "type": "string", "format": "uuid" }, "taskStatusSchema": { "type": "string", "enum": [ "todo", "in_progress", "on_hold", "done" ] }, "taskStatusUpdateInputSchema": { "type": "object", "properties": { "position": { "type": "number" }, "status": { "$ref": "#/components/schemas/taskStatusSchema" } }, "required": [ "position", "status" ] } } ``` # List task time entries. (/docs/openapi/listTaskTimeEntries) Return time entries for a task. ## GET /v1/organizations/{organizationId}/tasks/{taskId}/time-entries ```json { "operationId": "listTaskTimeEntries", "summary": "List task time entries.", "description": "Return time entries for a task.", "tags": [ "Tasks" ], "parameters": [ { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" }, { "in": "path", "name": "taskId", "required": true, "schema": { "$ref": "#/components/schemas/taskIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "responses": { "200": { "description": "Returns task time entries.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/taskTimeEntriesResponseSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "listPaginationSchema": { "type": "object", "properties": { "hasMore": { "type": "boolean" }, "limit": { "type": "integer", "minimum": 1, "maximum": 100 }, "offset": { "type": "integer", "minimum": 0 }, "total": { "type": "integer", "minimum": 0 }, "totalIsEstimate": { "type": "boolean" } }, "required": [ "hasMore", "limit", "offset", "total" ] }, "organizationIdSchema": { "type": "string", "format": "uuid" }, "taskBilledInvoiceSchema": { "type": "object", "properties": { "displayNumber": { "type": [ "string", "null" ], "minLength": 1 }, "id": { "type": "string", "format": "uuid" }, "status": { "type": "string", "minLength": 1 } }, "required": [ "displayNumber", "id", "status" ] }, "taskIdSchema": { "type": "string", "format": "uuid" }, "taskTimeEntriesResponseSchema": { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/components/schemas/taskTimeEntrySchema" } }, "pagination": { "$ref": "#/components/schemas/listPaginationSchema" } }, "required": [ "data", "pagination" ] }, "taskTimeEntryAuthorSchema": { "type": "object", "properties": { "avatarUrl": { "type": [ "string", "null" ], "format": "uri" }, "displayName": { "type": "string", "minLength": 1 }, "userId": { "type": "string", "format": "uuid" } }, "required": [ "avatarUrl", "displayName", "userId" ] }, "taskTimeEntrySchema": { "type": "object", "properties": { "billable": { "type": "boolean" }, "billedInvoice": { "anyOf": [ { "$ref": "#/components/schemas/taskBilledInvoiceSchema" }, { "type": "null" } ] }, "createdAt": { "type": "string", "format": "date-time" }, "durationSeconds": { "type": "integer", "exclusiveMinimum": 0 }, "id": { "type": "string", "format": "uuid" }, "note": { "type": [ "string", "null" ] }, "organizationId": { "type": "string", "format": "uuid" }, "startedAt": { "type": "string", "format": "date-time" }, "taskId": { "type": "string", "format": "uuid" }, "updatedAt": { "type": "string", "format": "date-time" }, "user": { "anyOf": [ { "$ref": "#/components/schemas/taskTimeEntryAuthorSchema" }, { "type": "null" } ] }, "userId": { "type": [ "string", "null" ], "format": "uuid" } }, "required": [ "billable", "billedInvoice", "createdAt", "durationSeconds", "id", "note", "organizationId", "startedAt", "taskId", "updatedAt", "user", "userId" ] } } ``` # Add a task time entry. (/docs/openapi/addTaskTimeEntry) Add a time entry to a task. ## POST /v1/organizations/{organizationId}/tasks/{taskId}/time-entries ```json { "operationId": "addTaskTimeEntry", "summary": "Add a task time entry.", "description": "Add a time entry to a task.", "tags": [ "Tasks" ], "parameters": [ { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" }, { "in": "path", "name": "taskId", "required": true, "schema": { "$ref": "#/components/schemas/taskIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/taskTimeEntryInputSchema" } } }, "description": "The task time entry payload." }, "responses": { "201": { "description": "Returns the created task time entry.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/taskTimeEntrySchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "organizationIdSchema": { "type": "string", "format": "uuid" }, "taskBilledInvoiceSchema": { "type": "object", "properties": { "displayNumber": { "type": [ "string", "null" ], "minLength": 1 }, "id": { "type": "string", "format": "uuid" }, "status": { "type": "string", "minLength": 1 } }, "required": [ "displayNumber", "id", "status" ] }, "taskIdSchema": { "type": "string", "format": "uuid" }, "taskTimeEntryAuthorSchema": { "type": "object", "properties": { "avatarUrl": { "type": [ "string", "null" ], "format": "uri" }, "displayName": { "type": "string", "minLength": 1 }, "userId": { "type": "string", "format": "uuid" } }, "required": [ "avatarUrl", "displayName", "userId" ] }, "taskTimeEntryInputSchema": { "type": "object", "properties": { "billable": { "type": "boolean" }, "durationSeconds": { "type": "integer", "exclusiveMinimum": 0, "maximum": 2678400 }, "note": { "type": [ "string", "null" ], "maxLength": 2000 }, "startedAt": { "type": "string", "format": "date-time" }, "userId": { "type": "string", "format": "uuid" } }, "required": [ "durationSeconds" ] }, "taskTimeEntrySchema": { "type": "object", "properties": { "billable": { "type": "boolean" }, "billedInvoice": { "anyOf": [ { "$ref": "#/components/schemas/taskBilledInvoiceSchema" }, { "type": "null" } ] }, "createdAt": { "type": "string", "format": "date-time" }, "durationSeconds": { "type": "integer", "exclusiveMinimum": 0 }, "id": { "type": "string", "format": "uuid" }, "note": { "type": [ "string", "null" ] }, "organizationId": { "type": "string", "format": "uuid" }, "startedAt": { "type": "string", "format": "date-time" }, "taskId": { "type": "string", "format": "uuid" }, "updatedAt": { "type": "string", "format": "date-time" }, "user": { "anyOf": [ { "$ref": "#/components/schemas/taskTimeEntryAuthorSchema" }, { "type": "null" } ] }, "userId": { "type": [ "string", "null" ], "format": "uuid" } }, "required": [ "billable", "billedInvoice", "createdAt", "durationSeconds", "id", "note", "organizationId", "startedAt", "taskId", "updatedAt", "user", "userId" ] } } ``` # Update a task comment. (/docs/openapi/updateTaskComment) Update a comment on a task. ## PATCH /v1/organizations/{organizationId}/tasks/{taskId}/comments/{commentId} ```json { "operationId": "updateTaskComment", "summary": "Update a task comment.", "description": "Update a comment on a task.", "tags": [ "Tasks" ], "parameters": [ { "in": "path", "name": "commentId", "required": true, "schema": { "type": "string", "format": "uuid" }, "example": "123e4567-e89b-12d3-a456-426614174000" }, { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" }, { "in": "path", "name": "taskId", "required": true, "schema": { "$ref": "#/components/schemas/taskIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/taskCommentInputSchema" } } }, "description": "The task comment payload." }, "responses": { "200": { "description": "Returns the updated task comment.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/taskCommentSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "collaborationBlockSchema": { "type": "object", "discriminator": { "propertyName": "type" }, "oneOf": [ { "type": "object", "properties": { "content": { "$ref": "#/components/schemas/collaborationInlineContentSchema" }, "type": { "type": "string", "enum": [ "paragraph" ] } }, "required": [ "content", "type" ], "additionalProperties": false }, { "type": "object", "properties": { "items": { "type": "array", "items": { "$ref": "#/components/schemas/collaborationInlineContentSchema" }, "minItems": 1, "maxItems": 500 }, "type": { "type": "string", "enum": [ "bullet-list" ] } }, "required": [ "items", "type" ], "additionalProperties": false }, { "type": "object", "properties": { "items": { "type": "array", "items": { "$ref": "#/components/schemas/collaborationInlineContentSchema" }, "minItems": 1, "maxItems": 500 }, "type": { "type": "string", "enum": [ "numbered-list" ] } }, "required": [ "items", "type" ], "additionalProperties": false } ] }, "collaborationCommentAuthorSchema": { "type": "object", "properties": { "avatarUrl": { "type": [ "string", "null" ], "format": "uri" }, "displayName": { "type": "string", "minLength": 1 }, "userId": { "type": "string", "format": "uuid" } }, "required": [ "avatarUrl", "displayName", "userId" ] }, "collaborationComposerInputSchema": { "type": "object", "properties": { "selections": { "type": "array", "items": { "$ref": "#/components/schemas/collaborationComposerSelectionSchema" }, "maxItems": 100 }, "text": { "type": "string", "minLength": 1, "maxLength": 20000 } }, "required": [ "selections", "text" ], "additionalProperties": false }, "collaborationComposerSelectionSchema": { "type": "object", "properties": { "end": { "type": "integer", "exclusiveMinimum": 0, "maximum": 20000 }, "reference": { "$ref": "#/components/schemas/collaborationReferenceIdSchema" }, "start": { "type": "integer", "minimum": 0, "maximum": 20000 } }, "required": [ "end", "reference", "start" ], "additionalProperties": false }, "collaborationDocumentSchema": { "type": "object", "properties": { "blocks": { "type": "array", "items": { "$ref": "#/components/schemas/collaborationBlockSchema" }, "minItems": 1, "maxItems": 1000 }, "version": { "type": "integer", "enum": [ 1 ] } }, "required": [ "blocks", "version" ], "additionalProperties": false }, "collaborationInlineContentSchema": { "type": "array", "items": { "$ref": "#/components/schemas/collaborationInlineSchema" }, "minItems": 1, "maxItems": 1000 }, "collaborationInlineSchema": { "type": "object", "discriminator": { "propertyName": "type" }, "oneOf": [ { "type": "object", "properties": { "text": { "type": "string", "minLength": 1, "maxLength": 20000 }, "type": { "type": "string", "enum": [ "text" ] } }, "required": [ "text", "type" ], "additionalProperties": false }, { "type": "object", "properties": { "reference": { "$ref": "#/components/schemas/collaborationReferenceSchema" }, "type": { "type": "string", "enum": [ "reference" ] } }, "required": [ "reference", "type" ], "additionalProperties": false } ] }, "collaborationReferenceIdSchema": { "type": "object", "discriminator": { "propertyName": "kind" }, "oneOf": [ { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "kind": { "type": "string", "enum": [ "user" ] } }, "required": [ "id", "kind" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "kind": { "type": "string", "enum": [ "team" ] } }, "required": [ "id", "kind" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "kind": { "type": "string", "enum": [ "task" ] } }, "required": [ "id", "kind" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "kind": { "type": "string", "enum": [ "customer" ] } }, "required": [ "id", "kind" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "$ref": "#/components/schemas/quoteIdSchema" }, "kind": { "type": "string", "enum": [ "quote" ] } }, "required": [ "id", "kind" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "$ref": "#/components/schemas/invoiceIdSchema" }, "kind": { "type": "string", "enum": [ "invoice" ] } }, "required": [ "id", "kind" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "$ref": "#/components/schemas/productIdSchema" }, "kind": { "type": "string", "enum": [ "product" ] } }, "required": [ "id", "kind" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "kind": { "type": "string", "enum": [ "customer-asset" ] } }, "required": [ "id", "kind" ], "additionalProperties": false } ] }, "collaborationReferenceSchema": { "type": "object", "discriminator": { "propertyName": "kind" }, "oneOf": [ { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "kind": { "type": "string", "enum": [ "user" ] }, "label": { "type": "string", "minLength": 1, "maxLength": 500 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "path": { "type": "string", "pattern": "^/", "maxLength": 2000 } }, "required": [ "id", "kind", "label", "organizationId", "path" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "kind": { "type": "string", "enum": [ "team" ] }, "label": { "type": "string", "minLength": 1, "maxLength": 500 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "path": { "type": "string", "pattern": "^/", "maxLength": 2000 } }, "required": [ "id", "kind", "label", "organizationId", "path" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "kind": { "type": "string", "enum": [ "task" ] }, "label": { "type": "string", "minLength": 1, "maxLength": 500 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "path": { "type": "string", "pattern": "^/", "maxLength": 2000 } }, "required": [ "id", "kind", "label", "organizationId", "path" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "kind": { "type": "string", "enum": [ "customer" ] }, "label": { "type": "string", "minLength": 1, "maxLength": 500 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "path": { "type": "string", "pattern": "^/", "maxLength": 2000 } }, "required": [ "id", "kind", "label", "organizationId", "path" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "$ref": "#/components/schemas/quoteIdSchema" }, "kind": { "type": "string", "enum": [ "quote" ] }, "label": { "type": "string", "minLength": 1, "maxLength": 500 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "path": { "type": "string", "pattern": "^/", "maxLength": 2000 } }, "required": [ "id", "kind", "label", "organizationId", "path" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "$ref": "#/components/schemas/invoiceIdSchema" }, "kind": { "type": "string", "enum": [ "invoice" ] }, "label": { "type": "string", "minLength": 1, "maxLength": 500 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "path": { "type": "string", "pattern": "^/", "maxLength": 2000 } }, "required": [ "id", "kind", "label", "organizationId", "path" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "$ref": "#/components/schemas/productIdSchema" }, "kind": { "type": "string", "enum": [ "product" ] }, "label": { "type": "string", "minLength": 1, "maxLength": 500 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "path": { "type": "string", "pattern": "^/", "maxLength": 2000 } }, "required": [ "id", "kind", "label", "organizationId", "path" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "kind": { "type": "string", "enum": [ "customer-asset" ] }, "label": { "type": "string", "minLength": 1, "maxLength": 500 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "path": { "type": "string", "pattern": "^/", "maxLength": 2000 } }, "required": [ "id", "kind", "label", "organizationId", "path" ], "additionalProperties": false } ] }, "invoiceIdSchema": { "type": "string", "format": "uuid" }, "organizationIdSchema": { "type": "string", "format": "uuid" }, "productIdSchema": { "type": "string", "format": "uuid" }, "quoteIdSchema": { "type": "string", "format": "uuid" }, "taskCommentAuthorSchema": { "$ref": "#/components/schemas/collaborationCommentAuthorSchema" }, "taskCommentInputSchema": { "$ref": "#/components/schemas/collaborationComposerInputSchema" }, "taskCommentSchema": { "type": "object", "properties": { "author": { "anyOf": [ { "$ref": "#/components/schemas/taskCommentAuthorSchema" }, { "type": "null" } ] }, "authorId": { "type": [ "string", "null" ], "format": "uuid" }, "contentDoc": { "$ref": "#/components/schemas/collaborationDocumentSchema" }, "contentText": { "type": "string", "minLength": 1, "maxLength": 20000 }, "createdAt": { "type": "string", "format": "date-time" }, "id": { "type": "string", "format": "uuid" }, "organizationId": { "type": "string", "format": "uuid" }, "taskId": { "type": "string", "format": "uuid" }, "updatedAt": { "type": "string", "format": "date-time" } }, "required": [ "author", "authorId", "contentDoc", "contentText", "createdAt", "id", "organizationId", "taskId", "updatedAt" ] }, "taskIdSchema": { "type": "string", "format": "uuid" } } ``` # Delete a task comment. (/docs/openapi/deleteTaskComment) Delete a comment from a task. ## DELETE /v1/organizations/{organizationId}/tasks/{taskId}/comments/{commentId} ```json { "operationId": "deleteTaskComment", "summary": "Delete a task comment.", "description": "Delete a comment from a task.", "tags": [ "Tasks" ], "parameters": [ { "in": "path", "name": "commentId", "required": true, "schema": { "type": "string", "format": "uuid" }, "example": "123e4567-e89b-12d3-a456-426614174000" }, { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" }, { "in": "path", "name": "taskId", "required": true, "schema": { "$ref": "#/components/schemas/taskIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "responses": { "200": { "description": "Returns a success payload when the comment is deleted.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/publicApiSuccessResponseSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "organizationIdSchema": { "type": "string", "format": "uuid" }, "publicApiSuccessResponseSchema": { "type": "object", "properties": { "success": { "type": "boolean", "enum": [ true ] } }, "required": [ "success" ] }, "taskIdSchema": { "type": "string", "format": "uuid" } } ``` # Advance a task lifecycle stage. (/docs/openapi/advanceTaskLifecycle) Moves the task to a manual milestone of its lifecycle. Only stages the lifecycle marks as manually advanceable are accepted. ## POST /v1/organizations/{organizationId}/tasks/{taskId}/lifecycle/advance ```json { "operationId": "advanceTaskLifecycle", "summary": "Advance a task lifecycle stage.", "description": "Moves the task to a manual milestone of its lifecycle. Only stages the lifecycle marks as manually advanceable are accepted.", "tags": [ "Tasks" ], "parameters": [ { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" }, { "in": "path", "name": "taskId", "required": true, "schema": { "$ref": "#/components/schemas/taskIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/lifecycleAdvanceInputSchema" } } }, "description": "The stage key to move to.", "required": true }, "responses": { "200": { "description": "The stage was recorded.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/publicApiSuccessResponseSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "lifecycleAdvanceInputSchema": { "type": "object", "properties": { "stageKey": { "type": "string", "minLength": 1, "maxLength": 80 } }, "required": [ "stageKey" ] }, "organizationIdSchema": { "type": "string", "format": "uuid" }, "publicApiSuccessResponseSchema": { "type": "object", "properties": { "success": { "type": "boolean", "enum": [ true ] } }, "required": [ "success" ] }, "taskIdSchema": { "type": "string", "format": "uuid" } } ``` # Delete a task time entry. (/docs/openapi/deleteTaskTimeEntry) Delete a time entry from a task. ## DELETE /v1/organizations/{organizationId}/tasks/{taskId}/time-entries/{entryId} ```json { "operationId": "deleteTaskTimeEntry", "summary": "Delete a task time entry.", "description": "Delete a time entry from a task.", "tags": [ "Tasks" ], "parameters": [ { "in": "path", "name": "entryId", "required": true, "schema": { "type": "string", "format": "uuid" }, "example": "123e4567-e89b-12d3-a456-426614174000" }, { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" }, { "in": "path", "name": "taskId", "required": true, "schema": { "$ref": "#/components/schemas/taskIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "responses": { "200": { "description": "Returns a success payload when the time entry is deleted.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/publicApiSuccessResponseSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "organizationIdSchema": { "type": "string", "format": "uuid" }, "publicApiSuccessResponseSchema": { "type": "object", "properties": { "success": { "type": "boolean", "enum": [ true ] } }, "required": [ "success" ] }, "taskIdSchema": { "type": "string", "format": "uuid" } } ``` # List teams. (/docs/openapi/listTeams) Return teams for an organization the authenticated user can access. ## GET /v1/organizations/{organizationId}/teams ```json { "operationId": "listTeams", "summary": "List teams.", "description": "Return teams for an organization the authenticated user can access.", "tags": [ "Teams" ], "parameters": [ { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "responses": { "200": { "description": "Returns paginated teams.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/teamsListResponseSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "organizationIdSchema": { "type": "string", "format": "uuid" }, "teamsListResponseSchema": { "type": "object" } } ``` # Create a team. (/docs/openapi/createTeam) Create a team for the specified organization. ## POST /v1/organizations/{organizationId}/teams ```json { "operationId": "createTeam", "summary": "Create a team.", "description": "Create a team for the specified organization.", "tags": [ "Teams" ], "parameters": [ { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/teamInputSchema" } } }, "description": "The team payload." }, "responses": { "201": { "description": "Returns the created team.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/teamSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "organizationIdSchema": { "type": "string", "format": "uuid" }, "teamDescriptionSchema": { "type": "string", "maxLength": 500 }, "teamIconNameSchema": { "type": "string", "minLength": 1, "maxLength": 120, "pattern": "^[A-Za-z0-9]+Icon$" }, "teamIdSchema": { "type": "string", "format": "uuid" }, "teamInputSchema": { "type": "object", "properties": { "description": { "anyOf": [ { "$ref": "#/components/schemas/teamDescriptionSchema" }, { "type": "null" } ] }, "iconName": { "$ref": "#/components/schemas/teamIconNameSchema" }, "name": { "type": "string", "minLength": 1, "maxLength": 120 }, "slug": { "$ref": "#/components/schemas/teamSlugSchema" } }, "required": [ "iconName", "name", "slug" ] }, "teamSchema": { "type": "object", "properties": { "createdAt": { "type": "string", "format": "date-time" }, "createdBy": { "type": [ "string", "null" ], "format": "uuid" }, "description": { "anyOf": [ { "$ref": "#/components/schemas/teamDescriptionSchema" }, { "type": "null" } ] }, "iconName": { "$ref": "#/components/schemas/teamIconNameSchema" }, "id": { "$ref": "#/components/schemas/teamIdSchema" }, "memberCount": { "type": "integer", "minimum": 0 }, "name": { "type": "string", "minLength": 1 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "slug": { "$ref": "#/components/schemas/teamSlugSchema" }, "status": { "$ref": "#/components/schemas/teamStatusSchema" }, "updatedAt": { "type": "string", "format": "date-time" }, "updatedBy": { "type": [ "string", "null" ], "format": "uuid" } }, "required": [ "createdAt", "createdBy", "description", "iconName", "id", "memberCount", "name", "organizationId", "slug", "status", "updatedAt", "updatedBy" ] }, "teamSlugSchema": { "type": "string", "minLength": 2, "maxLength": 64, "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$" }, "teamStatusSchema": { "type": "string", "enum": [ "active", "archived" ] } } ``` # Get a team. (/docs/openapi/getTeam) Return a team detail record for the specified organization. ## GET /v1/organizations/{organizationId}/teams/{teamId} ```json { "operationId": "getTeam", "summary": "Get a team.", "description": "Return a team detail record for the specified organization.", "tags": [ "Teams" ], "parameters": [ { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" }, { "in": "path", "name": "teamId", "required": true, "schema": { "$ref": "#/components/schemas/teamIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "responses": { "200": { "description": "Returns the requested team.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/teamDetailSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "organizationIdSchema": { "type": "string", "format": "uuid" }, "teamDescriptionSchema": { "type": "string", "maxLength": 500 }, "teamDetailSchema": { "type": "object", "properties": { "createdAt": { "type": "string", "format": "date-time" }, "createdBy": { "type": [ "string", "null" ], "format": "uuid" }, "description": { "anyOf": [ { "$ref": "#/components/schemas/teamDescriptionSchema" }, { "type": "null" } ] }, "iconName": { "$ref": "#/components/schemas/teamIconNameSchema" }, "id": { "$ref": "#/components/schemas/teamIdSchema" }, "memberCount": { "type": "integer", "minimum": 0 }, "name": { "type": "string", "minLength": 1 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "slug": { "$ref": "#/components/schemas/teamSlugSchema" }, "status": { "$ref": "#/components/schemas/teamStatusSchema" }, "updatedAt": { "type": "string", "format": "date-time" }, "updatedBy": { "type": [ "string", "null" ], "format": "uuid" }, "members": { "type": "array", "items": { "$ref": "#/components/schemas/teamMemberSchema" } } }, "required": [ "createdAt", "createdBy", "description", "iconName", "id", "memberCount", "name", "organizationId", "slug", "status", "updatedAt", "updatedBy", "members" ] }, "teamIconNameSchema": { "type": "string", "minLength": 1, "maxLength": 120, "pattern": "^[A-Za-z0-9]+Icon$" }, "teamIdSchema": { "type": "string", "format": "uuid" }, "teamMemberSchema": { "type": "object", "properties": { "avatarUrl": { "type": [ "string", "null" ], "format": "uri" }, "createdAt": { "type": "string", "format": "date-time" }, "email": { "type": [ "string", "null" ], "format": "email" }, "firstName": { "type": [ "string", "null" ] }, "lastName": { "type": [ "string", "null" ] }, "roleName": { "type": [ "string", "null" ] }, "updatedAt": { "type": "string", "format": "date-time" }, "userId": { "type": "string", "format": "uuid" } }, "required": [ "avatarUrl", "createdAt", "email", "firstName", "lastName", "roleName", "updatedAt", "userId" ] }, "teamSlugSchema": { "type": "string", "minLength": 2, "maxLength": 64, "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$" }, "teamStatusSchema": { "type": "string", "enum": [ "active", "archived" ] } } ``` # Update a team. (/docs/openapi/updateTeam) Update a team for the specified organization. ## PATCH /v1/organizations/{organizationId}/teams/{teamId} ```json { "operationId": "updateTeam", "summary": "Update a team.", "description": "Update a team for the specified organization.", "tags": [ "Teams" ], "parameters": [ { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" }, { "in": "path", "name": "teamId", "required": true, "schema": { "$ref": "#/components/schemas/teamIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/teamInputSchema" } } }, "description": "The team payload." }, "responses": { "200": { "description": "Returns the updated team.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/teamSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "organizationIdSchema": { "type": "string", "format": "uuid" }, "teamDescriptionSchema": { "type": "string", "maxLength": 500 }, "teamIconNameSchema": { "type": "string", "minLength": 1, "maxLength": 120, "pattern": "^[A-Za-z0-9]+Icon$" }, "teamIdSchema": { "type": "string", "format": "uuid" }, "teamInputSchema": { "type": "object", "properties": { "description": { "anyOf": [ { "$ref": "#/components/schemas/teamDescriptionSchema" }, { "type": "null" } ] }, "iconName": { "$ref": "#/components/schemas/teamIconNameSchema" }, "name": { "type": "string", "minLength": 1, "maxLength": 120 }, "slug": { "$ref": "#/components/schemas/teamSlugSchema" } }, "required": [ "iconName", "name", "slug" ] }, "teamSchema": { "type": "object", "properties": { "createdAt": { "type": "string", "format": "date-time" }, "createdBy": { "type": [ "string", "null" ], "format": "uuid" }, "description": { "anyOf": [ { "$ref": "#/components/schemas/teamDescriptionSchema" }, { "type": "null" } ] }, "iconName": { "$ref": "#/components/schemas/teamIconNameSchema" }, "id": { "$ref": "#/components/schemas/teamIdSchema" }, "memberCount": { "type": "integer", "minimum": 0 }, "name": { "type": "string", "minLength": 1 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "slug": { "$ref": "#/components/schemas/teamSlugSchema" }, "status": { "$ref": "#/components/schemas/teamStatusSchema" }, "updatedAt": { "type": "string", "format": "date-time" }, "updatedBy": { "type": [ "string", "null" ], "format": "uuid" } }, "required": [ "createdAt", "createdBy", "description", "iconName", "id", "memberCount", "name", "organizationId", "slug", "status", "updatedAt", "updatedBy" ] }, "teamSlugSchema": { "type": "string", "minLength": 2, "maxLength": 64, "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$" }, "teamStatusSchema": { "type": "string", "enum": [ "active", "archived" ] } } ``` # Delete a team. (/docs/openapi/deleteTeam) Delete a team for the specified organization. ## DELETE /v1/organizations/{organizationId}/teams/{teamId} ```json { "operationId": "deleteTeam", "summary": "Delete a team.", "description": "Delete a team for the specified organization.", "tags": [ "Teams" ], "parameters": [ { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" }, { "in": "path", "name": "teamId", "required": true, "schema": { "$ref": "#/components/schemas/teamIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "responses": { "200": { "description": "Returns a success payload when the team is deleted.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/publicApiSuccessResponseSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "organizationIdSchema": { "type": "string", "format": "uuid" }, "publicApiSuccessResponseSchema": { "type": "object", "properties": { "success": { "type": "boolean", "enum": [ true ] } }, "required": [ "success" ] }, "teamIdSchema": { "type": "string", "format": "uuid" } } ``` # Replace team members. (/docs/openapi/replaceTeamMembers) Replace all members assigned to a team. ## PUT /v1/organizations/{organizationId}/teams/{teamId}/members ```json { "operationId": "replaceTeamMembers", "summary": "Replace team members.", "description": "Replace all members assigned to a team.", "tags": [ "Teams" ], "parameters": [ { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" }, { "in": "path", "name": "teamId", "required": true, "schema": { "$ref": "#/components/schemas/teamIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/teamMembersInputSchema" } } }, "description": "The complete member assignment for the team." }, "responses": { "200": { "description": "Returns the updated team detail.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/teamDetailSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "organizationIdSchema": { "type": "string", "format": "uuid" }, "teamDescriptionSchema": { "type": "string", "maxLength": 500 }, "teamDetailSchema": { "type": "object", "properties": { "createdAt": { "type": "string", "format": "date-time" }, "createdBy": { "type": [ "string", "null" ], "format": "uuid" }, "description": { "anyOf": [ { "$ref": "#/components/schemas/teamDescriptionSchema" }, { "type": "null" } ] }, "iconName": { "$ref": "#/components/schemas/teamIconNameSchema" }, "id": { "$ref": "#/components/schemas/teamIdSchema" }, "memberCount": { "type": "integer", "minimum": 0 }, "name": { "type": "string", "minLength": 1 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "slug": { "$ref": "#/components/schemas/teamSlugSchema" }, "status": { "$ref": "#/components/schemas/teamStatusSchema" }, "updatedAt": { "type": "string", "format": "date-time" }, "updatedBy": { "type": [ "string", "null" ], "format": "uuid" }, "members": { "type": "array", "items": { "$ref": "#/components/schemas/teamMemberSchema" } } }, "required": [ "createdAt", "createdBy", "description", "iconName", "id", "memberCount", "name", "organizationId", "slug", "status", "updatedAt", "updatedBy", "members" ] }, "teamIconNameSchema": { "type": "string", "minLength": 1, "maxLength": 120, "pattern": "^[A-Za-z0-9]+Icon$" }, "teamIdSchema": { "type": "string", "format": "uuid" }, "teamMemberSchema": { "type": "object", "properties": { "avatarUrl": { "type": [ "string", "null" ], "format": "uri" }, "createdAt": { "type": "string", "format": "date-time" }, "email": { "type": [ "string", "null" ], "format": "email" }, "firstName": { "type": [ "string", "null" ] }, "lastName": { "type": [ "string", "null" ] }, "roleName": { "type": [ "string", "null" ] }, "updatedAt": { "type": "string", "format": "date-time" }, "userId": { "type": "string", "format": "uuid" } }, "required": [ "avatarUrl", "createdAt", "email", "firstName", "lastName", "roleName", "updatedAt", "userId" ] }, "teamMembersInputSchema": { "type": "object", "properties": { "userIds": { "type": "array", "items": { "type": "string", "format": "uuid" }, "maxItems": 200 } }, "required": [ "userIds" ] }, "teamSlugSchema": { "type": "string", "minLength": 2, "maxLength": 64, "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$" }, "teamStatusSchema": { "type": "string", "enum": [ "active", "archived" ] } } ``` # List templates. (/docs/openapi/listTemplates) Return quote, invoice, and task templates for an organization the authenticated user can access. ## GET /v1/organizations/{organizationId}/templates ```json { "operationId": "listTemplates", "summary": "List templates.", "description": "Return quote, invoice, and task templates for an organization the authenticated user can access.", "tags": [ "Templates" ], "parameters": [ { "in": "query", "name": "language", "required": false, "schema": { "allOf": [ { "$ref": "#/components/schemas/templateLanguageSchema" } ] }, "example": "example" }, { "in": "query", "name": "type", "required": false, "schema": { "allOf": [ { "$ref": "#/components/schemas/templateTypeSchema" } ] }, "example": "example" }, { "in": "query", "name": "limit", "required": false, "schema": { "type": "string" }, "example": "example" }, { "in": "query", "name": "offset", "required": false, "schema": { "type": "string" }, "example": "example" }, { "in": "query", "name": "q", "required": false, "schema": { "type": "string" }, "example": "example" }, { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "responses": { "200": { "description": "Returns paginated templates.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/templatesListResponseSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "appLocaleSchema": { "type": "string", "enum": [ "en", "nl", "de" ] }, "organizationIdSchema": { "type": "string", "format": "uuid" }, "templateLanguageSchema": { "$ref": "#/components/schemas/appLocaleSchema" }, "templateTypeSchema": { "type": "string", "enum": [ "invoice", "quote", "task", "order", "purchase_order" ] }, "templatesListResponseSchema": { "type": "object" } } ``` # Create a template. (/docs/openapi/createTemplate) Create a reusable quote, invoice, or task template for the specified organization. ## POST /v1/organizations/{organizationId}/templates ```json { "operationId": "createTemplate", "summary": "Create a template.", "description": "Create a reusable quote, invoice, or task template for the specified organization.", "tags": [ "Templates" ], "parameters": [ { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/templateInputSchema" } } }, "description": "The template payload." }, "responses": { "201": { "description": "Returns the created template.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/templateSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "appLocaleSchema": { "type": "string", "enum": [ "en", "nl", "de" ] }, "currencyCode": { "type": "string", "pattern": "^[A-Z]{3}$" }, "discountKindSchema": { "type": "string", "enum": [ "none", "percentage", "fixed" ] }, "documentVatRegimeSchema": { "type": "string", "enum": [ "standard", "reverse_charge", "exempt", "kor", "margin_scheme" ] }, "invoiceTemplateInputSchema": { "type": "object", "properties": { "currency": { "allOf": [ { "$ref": "#/components/schemas/currencyCode" } ] }, "defaultDueDays": { "type": [ "integer", "null" ], "minimum": 0 }, "defaultValidityDays": { "type": [ "integer", "null" ], "exclusiveMinimum": 0 }, "description": { "type": [ "string", "null" ], "maxLength": 20000 }, "discountKind": { "allOf": [ { "$ref": "#/components/schemas/discountKindSchema" } ] }, "discountValue": { "type": "number", "minimum": 0 }, "footerText": { "type": [ "string", "null" ], "maxLength": 20000 }, "internalNotes": { "type": [ "string", "null" ], "maxLength": 20000 }, "intro": { "type": [ "string", "null" ], "maxLength": 20000 }, "isDefault": { "type": "boolean" }, "language": { "allOf": [ { "$ref": "#/components/schemas/templateLanguageSchema" } ] }, "lifecycleWorkflowKey": { "allOf": [ { "$ref": "#/components/schemas/templateLifecycleWorkflowKeySchema" } ] }, "name": { "type": "string", "minLength": 1, "maxLength": 240 }, "publicMessage": { "type": [ "string", "null" ], "maxLength": 20000 }, "terms": { "type": [ "string", "null" ], "maxLength": 20000 }, "title": { "type": "string", "minLength": 1, "maxLength": 240 }, "vatInclusive": { "type": "boolean" }, "vatRegime": { "allOf": [ { "$ref": "#/components/schemas/documentVatRegimeSchema" } ] }, "termsPdfFileId": { "type": [ "string", "null" ], "format": "uuid" }, "termsPdfVersionId": { "type": [ "string", "null" ], "format": "uuid" }, "termsRichText": { "type": [ "string", "null" ], "maxLength": 100000 }, "termsUrl": { "allOf": [ { "$ref": "#/components/schemas/optionalUrl" } ] }, "lines": { "type": "array", "items": { "$ref": "#/components/schemas/invoiceTemplateLineInputSchema" }, "maxItems": 500 }, "type": { "type": "string", "enum": [ "invoice" ] } }, "required": [ "name", "title", "type" ] }, "invoiceTemplateLineInputSchema": { "type": "object", "properties": { "description": { "type": [ "string", "null" ], "maxLength": 20000 }, "discountKind": { "allOf": [ { "$ref": "#/components/schemas/discountKindSchema" } ] }, "discountValue": { "type": "number", "minimum": 0 }, "id": { "type": "string", "format": "uuid" }, "kind": { "allOf": [ { "$ref": "#/components/schemas/templateLineKindSchema" } ] }, "lineType": { "anyOf": [ { "$ref": "#/components/schemas/templateLineTypeSchema" }, { "type": "null" } ] }, "position": { "type": "number" }, "pricingMode": { "allOf": [ { "$ref": "#/components/schemas/invoiceTemplateLinePricingModeSchema" } ] }, "productId": { "type": [ "string", "null" ], "format": "uuid" }, "quantity": { "type": "number", "minimum": 0 }, "title": { "type": "string", "minLength": 1, "maxLength": 500 }, "unit": { "type": [ "string", "null" ], "maxLength": 40 }, "unitPrice": { "type": "number" }, "variantId": { "type": [ "string", "null" ], "format": "uuid" }, "vatRate": { "type": "number", "minimum": 0, "maximum": 100 } }, "required": [ "title" ], "additionalProperties": false }, "invoiceTemplateLinePricingModeSchema": { "type": "string", "enum": [ "unit", "hourly", "fixed" ] }, "optionalUrl": { "type": [ "string", "null" ], "maxLength": 2000 }, "orderDeliveryMethodSchema": { "type": "string", "enum": [ "pickup", "own_delivery", "carrier", "digital", "none" ] }, "orderInvoicingPolicySchema": { "type": "string", "enum": [ "before_fulfillment", "on_fulfillment", "manual" ] }, "orderTemplateInputSchema": { "type": "object", "properties": { "currency": { "allOf": [ { "$ref": "#/components/schemas/currencyCode" } ] }, "defaultDueDays": { "type": [ "integer", "null" ], "minimum": 0 }, "defaultValidityDays": { "type": [ "integer", "null" ], "exclusiveMinimum": 0 }, "description": { "type": [ "string", "null" ], "maxLength": 20000 }, "discountKind": { "allOf": [ { "$ref": "#/components/schemas/discountKindSchema" } ] }, "discountValue": { "type": "number", "minimum": 0 }, "footerText": { "type": [ "string", "null" ], "maxLength": 20000 }, "internalNotes": { "type": [ "string", "null" ], "maxLength": 20000 }, "intro": { "type": [ "string", "null" ], "maxLength": 20000 }, "isDefault": { "type": "boolean" }, "language": { "allOf": [ { "$ref": "#/components/schemas/templateLanguageSchema" } ] }, "lifecycleWorkflowKey": { "allOf": [ { "$ref": "#/components/schemas/templateLifecycleWorkflowKeySchema" } ] }, "name": { "type": "string", "minLength": 1, "maxLength": 240 }, "publicMessage": { "type": [ "string", "null" ], "maxLength": 20000 }, "terms": { "type": [ "string", "null" ], "maxLength": 20000 }, "title": { "type": "string", "minLength": 1, "maxLength": 240 }, "vatInclusive": { "type": "boolean" }, "vatRegime": { "allOf": [ { "$ref": "#/components/schemas/documentVatRegimeSchema" } ] }, "termsPdfFileId": { "type": [ "string", "null" ], "format": "uuid" }, "termsPdfVersionId": { "type": [ "string", "null" ], "format": "uuid" }, "termsRichText": { "type": [ "string", "null" ], "maxLength": 100000 }, "termsUrl": { "allOf": [ { "$ref": "#/components/schemas/optionalUrl" } ] }, "orderDeliveryMethod": { "anyOf": [ { "$ref": "#/components/schemas/orderDeliveryMethodSchema" }, { "type": "null" } ] }, "orderInvoicingPolicy": { "anyOf": [ { "$ref": "#/components/schemas/orderInvoicingPolicySchema" }, { "type": "null" } ] }, "orderPaymentRequired": { "type": "boolean" }, "type": { "type": "string", "enum": [ "order" ] } }, "required": [ "name", "title", "type" ] }, "organizationIdSchema": { "type": "string", "format": "uuid" }, "purchaseOrderTemplateInputSchema": { "type": "object", "properties": { "currency": { "allOf": [ { "$ref": "#/components/schemas/currencyCode" } ] }, "defaultDueDays": { "type": [ "integer", "null" ], "minimum": 0 }, "defaultValidityDays": { "type": [ "integer", "null" ], "exclusiveMinimum": 0 }, "description": { "type": [ "string", "null" ], "maxLength": 20000 }, "discountKind": { "allOf": [ { "$ref": "#/components/schemas/discountKindSchema" } ] }, "discountValue": { "type": "number", "minimum": 0 }, "footerText": { "type": [ "string", "null" ], "maxLength": 20000 }, "internalNotes": { "type": [ "string", "null" ], "maxLength": 20000 }, "intro": { "type": [ "string", "null" ], "maxLength": 20000 }, "isDefault": { "type": "boolean" }, "language": { "allOf": [ { "$ref": "#/components/schemas/templateLanguageSchema" } ] }, "lifecycleWorkflowKey": { "allOf": [ { "$ref": "#/components/schemas/templateLifecycleWorkflowKeySchema" } ] }, "name": { "type": "string", "minLength": 1, "maxLength": 240 }, "publicMessage": { "type": [ "string", "null" ], "maxLength": 20000 }, "terms": { "type": [ "string", "null" ], "maxLength": 20000 }, "title": { "type": "string", "minLength": 1, "maxLength": 240 }, "vatInclusive": { "type": "boolean" }, "vatRegime": { "allOf": [ { "$ref": "#/components/schemas/documentVatRegimeSchema" } ] }, "termsPdfFileId": { "type": [ "string", "null" ], "format": "uuid" }, "termsPdfVersionId": { "type": [ "string", "null" ], "format": "uuid" }, "termsRichText": { "type": [ "string", "null" ], "maxLength": 100000 }, "termsUrl": { "allOf": [ { "$ref": "#/components/schemas/optionalUrl" } ] }, "shipToLocationId": { "type": [ "string", "null" ], "format": "uuid" }, "type": { "type": "string", "enum": [ "purchase_order" ] } }, "required": [ "name", "title", "type" ] }, "quoteDepositKindSchema": { "type": "string", "enum": [ "none", "percent", "fixed" ] }, "quoteTemplateInputSchema": { "type": "object", "properties": { "currency": { "allOf": [ { "$ref": "#/components/schemas/currencyCode" } ] }, "defaultDueDays": { "type": [ "integer", "null" ], "minimum": 0 }, "defaultValidityDays": { "type": [ "integer", "null" ], "exclusiveMinimum": 0 }, "description": { "type": [ "string", "null" ], "maxLength": 20000 }, "discountKind": { "allOf": [ { "$ref": "#/components/schemas/discountKindSchema" } ] }, "discountValue": { "type": "number", "minimum": 0 }, "footerText": { "type": [ "string", "null" ], "maxLength": 20000 }, "internalNotes": { "type": [ "string", "null" ], "maxLength": 20000 }, "intro": { "type": [ "string", "null" ], "maxLength": 20000 }, "isDefault": { "type": "boolean" }, "language": { "allOf": [ { "$ref": "#/components/schemas/templateLanguageSchema" } ] }, "lifecycleWorkflowKey": { "allOf": [ { "$ref": "#/components/schemas/templateLifecycleWorkflowKeySchema" } ] }, "name": { "type": "string", "minLength": 1, "maxLength": 240 }, "publicMessage": { "type": [ "string", "null" ], "maxLength": 20000 }, "terms": { "type": [ "string", "null" ], "maxLength": 20000 }, "title": { "type": "string", "minLength": 1, "maxLength": 240 }, "vatInclusive": { "type": "boolean" }, "vatRegime": { "allOf": [ { "$ref": "#/components/schemas/documentVatRegimeSchema" } ] }, "termsPdfFileId": { "type": [ "string", "null" ], "format": "uuid" }, "termsPdfVersionId": { "type": [ "string", "null" ], "format": "uuid" }, "termsRichText": { "type": [ "string", "null" ], "maxLength": 100000 }, "termsUrl": { "allOf": [ { "$ref": "#/components/schemas/optionalUrl" } ] }, "createOrderOnAccept": { "type": "boolean" }, "depositKind": { "allOf": [ { "$ref": "#/components/schemas/quoteDepositKindSchema" } ] }, "depositRequiredToAccept": { "type": "boolean" }, "depositValue": { "type": "number", "minimum": 0 }, "lines": { "type": "array", "items": { "$ref": "#/components/schemas/quoteTemplateLineInputSchema" }, "maxItems": 500 }, "type": { "type": "string", "enum": [ "quote" ] } }, "required": [ "name", "title", "type" ] }, "quoteTemplateLineInputSchema": { "type": "object", "properties": { "description": { "type": [ "string", "null" ], "maxLength": 20000 }, "discountKind": { "allOf": [ { "$ref": "#/components/schemas/discountKindSchema" } ] }, "discountValue": { "type": "number", "minimum": 0 }, "id": { "type": "string", "format": "uuid" }, "isOptional": { "type": "boolean" }, "kind": { "allOf": [ { "$ref": "#/components/schemas/templateLineKindSchema" } ] }, "lineType": { "anyOf": [ { "$ref": "#/components/schemas/templateLineTypeSchema" }, { "type": "null" } ] }, "position": { "type": "number" }, "pricingMode": { "allOf": [ { "$ref": "#/components/schemas/quoteTemplateLinePricingModeSchema" } ] }, "productId": { "type": [ "string", "null" ], "format": "uuid" }, "quantity": { "type": "number", "minimum": 0 }, "subscriptionInterval": { "anyOf": [ { "$ref": "#/components/schemas/quoteTemplateSubscriptionIntervalSchema" }, { "type": "null" } ] }, "subscriptionPeriods": { "type": [ "integer", "null" ], "exclusiveMinimum": 0 }, "title": { "type": "string", "minLength": 1, "maxLength": 500 }, "unit": { "type": [ "string", "null" ], "maxLength": 40 }, "unitPrice": { "type": "number" }, "variantId": { "type": [ "string", "null" ], "format": "uuid" }, "vatRate": { "type": "number", "minimum": 0, "maximum": 100 } }, "required": [ "title" ], "additionalProperties": false }, "quoteTemplateLinePricingModeSchema": { "type": "string", "enum": [ "unit", "hourly", "subscription", "fixed" ] }, "quoteTemplateSubscriptionIntervalSchema": { "type": "string", "enum": [ "weekly", "monthly", "quarterly", "yearly" ] }, "taskPrioritySchema": { "type": "string", "enum": [ "low", "medium", "high", "urgent" ] }, "taskStatusSchema": { "type": "string", "enum": [ "todo", "in_progress", "on_hold", "done" ] }, "taskTemplateInputSchema": { "type": "object", "properties": { "currency": { "allOf": [ { "$ref": "#/components/schemas/currencyCode" } ] }, "defaultDueDays": { "type": [ "integer", "null" ], "minimum": 0 }, "defaultValidityDays": { "type": [ "integer", "null" ], "exclusiveMinimum": 0 }, "description": { "type": [ "string", "null" ], "maxLength": 20000 }, "discountKind": { "allOf": [ { "$ref": "#/components/schemas/discountKindSchema" } ] }, "discountValue": { "type": "number", "minimum": 0 }, "footerText": { "type": [ "string", "null" ], "maxLength": 20000 }, "internalNotes": { "type": [ "string", "null" ], "maxLength": 20000 }, "intro": { "type": [ "string", "null" ], "maxLength": 20000 }, "isDefault": { "type": "boolean" }, "language": { "allOf": [ { "$ref": "#/components/schemas/templateLanguageSchema" } ] }, "lifecycleWorkflowKey": { "allOf": [ { "$ref": "#/components/schemas/templateLifecycleWorkflowKeySchema" } ] }, "name": { "type": "string", "minLength": 1, "maxLength": 240 }, "publicMessage": { "type": [ "string", "null" ], "maxLength": 20000 }, "terms": { "type": [ "string", "null" ], "maxLength": 20000 }, "title": { "type": "string", "minLength": 1, "maxLength": 240 }, "vatInclusive": { "type": "boolean" }, "vatRegime": { "allOf": [ { "$ref": "#/components/schemas/documentVatRegimeSchema" } ] }, "termsPdfFileId": { "type": [ "string", "null" ], "format": "uuid" }, "termsPdfVersionId": { "type": [ "string", "null" ], "format": "uuid" }, "termsRichText": { "type": [ "string", "null" ], "maxLength": 100000 }, "termsUrl": { "allOf": [ { "$ref": "#/components/schemas/optionalUrl" } ] }, "assigneeIds": { "type": "array", "items": { "type": "string", "format": "uuid" }, "maxItems": 50 }, "subtasks": { "type": "array", "items": { "$ref": "#/components/schemas/templateTaskSubtaskInputSchema" }, "maxItems": 100 }, "tagIds": { "type": "array", "items": { "type": "string", "format": "uuid" }, "maxItems": 50 }, "task": { "$ref": "#/components/schemas/templateTaskSettingsInputSchema" }, "type": { "type": "string", "enum": [ "task" ] } }, "required": [ "name", "title", "task", "type" ] }, "templateIdSchema": { "type": "string", "format": "uuid" }, "templateInputSchema": { "type": "object", "discriminator": { "propertyName": "type" }, "oneOf": [ { "$ref": "#/components/schemas/invoiceTemplateInputSchema" }, { "$ref": "#/components/schemas/quoteTemplateInputSchema" }, { "$ref": "#/components/schemas/taskTemplateInputSchema" }, { "$ref": "#/components/schemas/orderTemplateInputSchema" }, { "$ref": "#/components/schemas/purchaseOrderTemplateInputSchema" } ] }, "templateLanguageSchema": { "$ref": "#/components/schemas/appLocaleSchema" }, "templateLifecycleWorkflowKeySchema": { "type": [ "string", "null" ], "minLength": 1, "maxLength": 120, "pattern": "^[a-z0-9][a-z0-9-]*$" }, "templateLineKindSchema": { "type": "string", "enum": [ "line", "section_header" ] }, "templateLineTypeSchema": { "type": "string", "enum": [ "product", "labor", "custom" ] }, "templateSchema": { "type": "object", "properties": { "id": { "$ref": "#/components/schemas/templateIdSchema" }, "organizationId": { "type": "string", "format": "uuid" }, "type": { "$ref": "#/components/schemas/templateTypeSchema" }, "language": { "$ref": "#/components/schemas/templateLanguageSchema" }, "isDefault": { "type": "boolean" }, "name": { "type": "string" }, "description": { "type": [ "string", "null" ] }, "title": { "type": "string" }, "intro": { "type": [ "string", "null" ] }, "terms": { "type": [ "string", "null" ] }, "internalNotes": { "type": [ "string", "null" ] }, "publicMessage": { "type": [ "string", "null" ] }, "footerText": { "type": [ "string", "null" ] }, "currency": { "$ref": "#/components/schemas/currencyCode" }, "vatInclusive": { "type": "boolean" }, "discountKind": { "$ref": "#/components/schemas/discountKindSchema" }, "discountValue": { "type": "number" }, "defaultDueDays": { "type": [ "integer", "null" ], "minimum": 0 }, "defaultValidityDays": { "type": [ "integer", "null" ], "exclusiveMinimum": 0 }, "vatRegime": { "$ref": "#/components/schemas/documentVatRegimeSchema" }, "termsPdfFileId": { "type": [ "string", "null" ], "format": "uuid" }, "termsPdfVersionId": { "type": [ "string", "null" ], "format": "uuid" }, "termsRichText": { "type": [ "string", "null" ], "maxLength": 100000 }, "termsUrl": { "type": [ "string", "null" ] }, "depositKind": { "$ref": "#/components/schemas/quoteDepositKindSchema" }, "depositRequiredToAccept": { "type": "boolean" }, "depositValue": { "type": "number", "minimum": 0 }, "lifecycleWorkflowKey": { "type": [ "string", "null" ] }, "orderDeliveryMethod": { "anyOf": [ { "$ref": "#/components/schemas/orderDeliveryMethodSchema" }, { "type": "null" } ] }, "orderInvoicingPolicy": { "anyOf": [ { "$ref": "#/components/schemas/orderInvoicingPolicySchema" }, { "type": "null" } ] }, "orderPaymentRequired": { "type": "boolean" }, "shipToLocationId": { "type": [ "string", "null" ], "format": "uuid" }, "createOrderOnAccept": { "type": "boolean" }, "archivedAt": { "type": [ "string", "null" ], "format": "date-time" }, "createdBy": { "type": [ "string", "null" ], "format": "uuid" }, "updatedBy": { "type": [ "string", "null" ], "format": "uuid" }, "createdAt": { "type": "string", "format": "date-time" }, "updatedAt": { "type": "string", "format": "date-time" } }, "required": [ "id", "organizationId", "type", "language", "isDefault", "name", "description", "title", "intro", "terms", "internalNotes", "publicMessage", "footerText", "currency", "vatInclusive", "discountKind", "discountValue", "defaultDueDays", "defaultValidityDays", "vatRegime", "termsPdfFileId", "termsPdfVersionId", "termsRichText", "termsUrl", "depositKind", "depositRequiredToAccept", "depositValue", "lifecycleWorkflowKey", "orderDeliveryMethod", "orderInvoicingPolicy", "orderPaymentRequired", "shipToLocationId", "createOrderOnAccept", "archivedAt", "createdBy", "updatedBy", "createdAt", "updatedAt" ] }, "templateTaskSettingsInputSchema": { "type": "object", "properties": { "customerAssetId": { "type": [ "string", "null" ], "format": "uuid" }, "customerId": { "type": [ "string", "null" ], "format": "uuid" }, "description": { "type": [ "string", "null" ], "maxLength": 20000 }, "priority": { "allOf": [ { "$ref": "#/components/schemas/taskPrioritySchema" } ] }, "status": { "allOf": [ { "$ref": "#/components/schemas/taskStatusSchema" } ] }, "title": { "type": "string", "minLength": 1, "maxLength": 240 } }, "required": [ "title" ] }, "templateTaskSubtaskInputSchema": { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "position": { "type": "number" }, "priority": { "allOf": [ { "$ref": "#/components/schemas/taskPrioritySchema" } ] }, "status": { "allOf": [ { "$ref": "#/components/schemas/taskStatusSchema" } ] }, "title": { "type": "string", "minLength": 1, "maxLength": 240 } }, "required": [ "title" ] }, "templateTypeSchema": { "type": "string", "enum": [ "invoice", "quote", "task", "order", "purchase_order" ] } } ``` # Get a template. (/docs/openapi/getTemplate) Return a template detail record for the specified organization. ## GET /v1/organizations/{organizationId}/templates/{templateId} ```json { "operationId": "getTemplate", "summary": "Get a template.", "description": "Return a template detail record for the specified organization.", "tags": [ "Templates" ], "parameters": [ { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" }, { "in": "path", "name": "templateId", "required": true, "schema": { "$ref": "#/components/schemas/templateIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "responses": { "200": { "description": "Returns the requested template.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/templateDetailSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "appLocaleSchema": { "type": "string", "enum": [ "en", "nl", "de" ] }, "currencyCode": { "type": "string", "pattern": "^[A-Z]{3}$" }, "discountKindSchema": { "type": "string", "enum": [ "none", "percentage", "fixed" ] }, "documentVatRegimeSchema": { "type": "string", "enum": [ "standard", "reverse_charge", "exempt", "kor", "margin_scheme" ] }, "invoiceTemplateDetailSchema": { "type": "object", "properties": { "lines": { "type": "array", "items": { "$ref": "#/components/schemas/invoiceTemplateLineSchema" } }, "template": { "$ref": "#/components/schemas/invoiceTemplateSchema" } }, "required": [ "lines", "template" ] }, "invoiceTemplateLinePricingModeSchema": { "type": "string", "enum": [ "unit", "hourly", "fixed" ] }, "invoiceTemplateLineSchema": { "type": "object", "properties": { "createdAt": { "type": "string", "format": "date-time" }, "description": { "type": [ "string", "null" ] }, "discountKind": { "$ref": "#/components/schemas/discountKindSchema" }, "discountValue": { "type": "number" }, "id": { "type": "string", "format": "uuid" }, "kind": { "$ref": "#/components/schemas/templateLineKindSchema" }, "lineType": { "anyOf": [ { "$ref": "#/components/schemas/templateLineTypeSchema" }, { "type": "null" } ] }, "organizationId": { "type": "string", "format": "uuid" }, "position": { "type": "number" }, "pricingMode": { "$ref": "#/components/schemas/invoiceTemplateLinePricingModeSchema" }, "productId": { "type": [ "string", "null" ], "format": "uuid" }, "quantity": { "type": "number" }, "templateId": { "$ref": "#/components/schemas/templateIdSchema" }, "title": { "type": "string" }, "unit": { "type": [ "string", "null" ] }, "unitPrice": { "type": "number" }, "updatedAt": { "type": "string", "format": "date-time" }, "variantId": { "type": [ "string", "null" ], "format": "uuid" }, "vatRate": { "type": "number" } }, "required": [ "createdAt", "description", "discountKind", "discountValue", "id", "kind", "lineType", "organizationId", "position", "pricingMode", "productId", "quantity", "templateId", "title", "unit", "unitPrice", "updatedAt", "variantId", "vatRate" ] }, "invoiceTemplateSchema": { "type": "object", "properties": { "id": { "$ref": "#/components/schemas/templateIdSchema" }, "organizationId": { "type": "string", "format": "uuid" }, "type": { "type": "string", "enum": [ "invoice" ] }, "language": { "$ref": "#/components/schemas/templateLanguageSchema" }, "isDefault": { "type": "boolean" }, "name": { "type": "string" }, "description": { "type": [ "string", "null" ] }, "title": { "type": "string" }, "intro": { "type": [ "string", "null" ] }, "terms": { "type": [ "string", "null" ] }, "internalNotes": { "type": [ "string", "null" ] }, "publicMessage": { "type": [ "string", "null" ] }, "footerText": { "type": [ "string", "null" ] }, "currency": { "$ref": "#/components/schemas/currencyCode" }, "vatInclusive": { "type": "boolean" }, "discountKind": { "$ref": "#/components/schemas/discountKindSchema" }, "discountValue": { "type": "number" }, "defaultDueDays": { "type": [ "integer", "null" ], "minimum": 0 }, "defaultValidityDays": { "type": [ "integer", "null" ], "exclusiveMinimum": 0 }, "vatRegime": { "$ref": "#/components/schemas/documentVatRegimeSchema" }, "termsPdfFileId": { "type": [ "string", "null" ], "format": "uuid" }, "termsPdfVersionId": { "type": [ "string", "null" ], "format": "uuid" }, "termsRichText": { "type": [ "string", "null" ], "maxLength": 100000 }, "termsUrl": { "type": [ "string", "null" ] }, "depositKind": { "$ref": "#/components/schemas/quoteDepositKindSchema" }, "depositRequiredToAccept": { "type": "boolean" }, "depositValue": { "type": "number", "minimum": 0 }, "lifecycleWorkflowKey": { "type": [ "string", "null" ] }, "orderDeliveryMethod": { "anyOf": [ { "$ref": "#/components/schemas/orderDeliveryMethodSchema" }, { "type": "null" } ] }, "orderInvoicingPolicy": { "anyOf": [ { "$ref": "#/components/schemas/orderInvoicingPolicySchema" }, { "type": "null" } ] }, "orderPaymentRequired": { "type": "boolean" }, "shipToLocationId": { "type": [ "string", "null" ], "format": "uuid" }, "createOrderOnAccept": { "type": "boolean" }, "archivedAt": { "type": [ "string", "null" ], "format": "date-time" }, "createdBy": { "type": [ "string", "null" ], "format": "uuid" }, "updatedBy": { "type": [ "string", "null" ], "format": "uuid" }, "createdAt": { "type": "string", "format": "date-time" }, "updatedAt": { "type": "string", "format": "date-time" } }, "required": [ "id", "organizationId", "type", "language", "isDefault", "name", "description", "title", "intro", "terms", "internalNotes", "publicMessage", "footerText", "currency", "vatInclusive", "discountKind", "discountValue", "defaultDueDays", "defaultValidityDays", "vatRegime", "termsPdfFileId", "termsPdfVersionId", "termsRichText", "termsUrl", "depositKind", "depositRequiredToAccept", "depositValue", "lifecycleWorkflowKey", "orderDeliveryMethod", "orderInvoicingPolicy", "orderPaymentRequired", "shipToLocationId", "createOrderOnAccept", "archivedAt", "createdBy", "updatedBy", "createdAt", "updatedAt" ] }, "orderDeliveryMethodSchema": { "type": "string", "enum": [ "pickup", "own_delivery", "carrier", "digital", "none" ] }, "orderInvoicingPolicySchema": { "type": "string", "enum": [ "before_fulfillment", "on_fulfillment", "manual" ] }, "orderTemplateDetailSchema": { "type": "object", "properties": { "lines": { "type": "array", "prefixItems": [], "items": false }, "template": { "$ref": "#/components/schemas/orderTemplateSchema" } }, "required": [ "lines", "template" ] }, "orderTemplateSchema": { "type": "object", "properties": { "id": { "$ref": "#/components/schemas/templateIdSchema" }, "organizationId": { "type": "string", "format": "uuid" }, "type": { "type": "string", "enum": [ "order" ] }, "language": { "$ref": "#/components/schemas/templateLanguageSchema" }, "isDefault": { "type": "boolean" }, "name": { "type": "string" }, "description": { "type": [ "string", "null" ] }, "title": { "type": "string" }, "intro": { "type": [ "string", "null" ] }, "terms": { "type": [ "string", "null" ] }, "internalNotes": { "type": [ "string", "null" ] }, "publicMessage": { "type": [ "string", "null" ] }, "footerText": { "type": [ "string", "null" ] }, "currency": { "$ref": "#/components/schemas/currencyCode" }, "vatInclusive": { "type": "boolean" }, "discountKind": { "$ref": "#/components/schemas/discountKindSchema" }, "discountValue": { "type": "number" }, "defaultDueDays": { "type": [ "integer", "null" ], "minimum": 0 }, "defaultValidityDays": { "type": [ "integer", "null" ], "exclusiveMinimum": 0 }, "vatRegime": { "$ref": "#/components/schemas/documentVatRegimeSchema" }, "termsPdfFileId": { "type": [ "string", "null" ], "format": "uuid" }, "termsPdfVersionId": { "type": [ "string", "null" ], "format": "uuid" }, "termsRichText": { "type": [ "string", "null" ], "maxLength": 100000 }, "termsUrl": { "type": [ "string", "null" ] }, "depositKind": { "$ref": "#/components/schemas/quoteDepositKindSchema" }, "depositRequiredToAccept": { "type": "boolean" }, "depositValue": { "type": "number", "minimum": 0 }, "lifecycleWorkflowKey": { "type": [ "string", "null" ] }, "orderDeliveryMethod": { "anyOf": [ { "$ref": "#/components/schemas/orderDeliveryMethodSchema" }, { "type": "null" } ] }, "orderInvoicingPolicy": { "anyOf": [ { "$ref": "#/components/schemas/orderInvoicingPolicySchema" }, { "type": "null" } ] }, "orderPaymentRequired": { "type": "boolean" }, "shipToLocationId": { "type": [ "string", "null" ], "format": "uuid" }, "createOrderOnAccept": { "type": "boolean" }, "archivedAt": { "type": [ "string", "null" ], "format": "date-time" }, "createdBy": { "type": [ "string", "null" ], "format": "uuid" }, "updatedBy": { "type": [ "string", "null" ], "format": "uuid" }, "createdAt": { "type": "string", "format": "date-time" }, "updatedAt": { "type": "string", "format": "date-time" } }, "required": [ "id", "organizationId", "type", "language", "isDefault", "name", "description", "title", "intro", "terms", "internalNotes", "publicMessage", "footerText", "currency", "vatInclusive", "discountKind", "discountValue", "defaultDueDays", "defaultValidityDays", "vatRegime", "termsPdfFileId", "termsPdfVersionId", "termsRichText", "termsUrl", "depositKind", "depositRequiredToAccept", "depositValue", "lifecycleWorkflowKey", "orderDeliveryMethod", "orderInvoicingPolicy", "orderPaymentRequired", "shipToLocationId", "createOrderOnAccept", "archivedAt", "createdBy", "updatedBy", "createdAt", "updatedAt" ] }, "organizationIdSchema": { "type": "string", "format": "uuid" }, "purchaseOrderTemplateDetailSchema": { "type": "object", "properties": { "lines": { "type": "array", "prefixItems": [], "items": false }, "template": { "$ref": "#/components/schemas/purchaseOrderTemplateSchema" } }, "required": [ "lines", "template" ] }, "purchaseOrderTemplateSchema": { "type": "object", "properties": { "id": { "$ref": "#/components/schemas/templateIdSchema" }, "organizationId": { "type": "string", "format": "uuid" }, "type": { "type": "string", "enum": [ "purchase_order" ] }, "language": { "$ref": "#/components/schemas/templateLanguageSchema" }, "isDefault": { "type": "boolean" }, "name": { "type": "string" }, "description": { "type": [ "string", "null" ] }, "title": { "type": "string" }, "intro": { "type": [ "string", "null" ] }, "terms": { "type": [ "string", "null" ] }, "internalNotes": { "type": [ "string", "null" ] }, "publicMessage": { "type": [ "string", "null" ] }, "footerText": { "type": [ "string", "null" ] }, "currency": { "$ref": "#/components/schemas/currencyCode" }, "vatInclusive": { "type": "boolean" }, "discountKind": { "$ref": "#/components/schemas/discountKindSchema" }, "discountValue": { "type": "number" }, "defaultDueDays": { "type": [ "integer", "null" ], "minimum": 0 }, "defaultValidityDays": { "type": [ "integer", "null" ], "exclusiveMinimum": 0 }, "vatRegime": { "$ref": "#/components/schemas/documentVatRegimeSchema" }, "termsPdfFileId": { "type": [ "string", "null" ], "format": "uuid" }, "termsPdfVersionId": { "type": [ "string", "null" ], "format": "uuid" }, "termsRichText": { "type": [ "string", "null" ], "maxLength": 100000 }, "termsUrl": { "type": [ "string", "null" ] }, "depositKind": { "$ref": "#/components/schemas/quoteDepositKindSchema" }, "depositRequiredToAccept": { "type": "boolean" }, "depositValue": { "type": "number", "minimum": 0 }, "lifecycleWorkflowKey": { "type": [ "string", "null" ] }, "orderDeliveryMethod": { "anyOf": [ { "$ref": "#/components/schemas/orderDeliveryMethodSchema" }, { "type": "null" } ] }, "orderInvoicingPolicy": { "anyOf": [ { "$ref": "#/components/schemas/orderInvoicingPolicySchema" }, { "type": "null" } ] }, "orderPaymentRequired": { "type": "boolean" }, "shipToLocationId": { "type": [ "string", "null" ], "format": "uuid" }, "createOrderOnAccept": { "type": "boolean" }, "archivedAt": { "type": [ "string", "null" ], "format": "date-time" }, "createdBy": { "type": [ "string", "null" ], "format": "uuid" }, "updatedBy": { "type": [ "string", "null" ], "format": "uuid" }, "createdAt": { "type": "string", "format": "date-time" }, "updatedAt": { "type": "string", "format": "date-time" } }, "required": [ "id", "organizationId", "type", "language", "isDefault", "name", "description", "title", "intro", "terms", "internalNotes", "publicMessage", "footerText", "currency", "vatInclusive", "discountKind", "discountValue", "defaultDueDays", "defaultValidityDays", "vatRegime", "termsPdfFileId", "termsPdfVersionId", "termsRichText", "termsUrl", "depositKind", "depositRequiredToAccept", "depositValue", "lifecycleWorkflowKey", "orderDeliveryMethod", "orderInvoicingPolicy", "orderPaymentRequired", "shipToLocationId", "createOrderOnAccept", "archivedAt", "createdBy", "updatedBy", "createdAt", "updatedAt" ] }, "quoteDepositKindSchema": { "type": "string", "enum": [ "none", "percent", "fixed" ] }, "quoteTemplateDetailSchema": { "type": "object", "properties": { "lines": { "type": "array", "items": { "$ref": "#/components/schemas/quoteTemplateLineSchema" } }, "template": { "$ref": "#/components/schemas/quoteTemplateSchema" } }, "required": [ "lines", "template" ] }, "quoteTemplateLinePricingModeSchema": { "type": "string", "enum": [ "unit", "hourly", "subscription", "fixed" ] }, "quoteTemplateLineSchema": { "type": "object", "properties": { "createdAt": { "type": "string", "format": "date-time" }, "description": { "type": [ "string", "null" ] }, "discountKind": { "$ref": "#/components/schemas/discountKindSchema" }, "discountValue": { "type": "number" }, "id": { "type": "string", "format": "uuid" }, "isOptional": { "type": "boolean" }, "kind": { "$ref": "#/components/schemas/templateLineKindSchema" }, "lineType": { "anyOf": [ { "$ref": "#/components/schemas/templateLineTypeSchema" }, { "type": "null" } ] }, "organizationId": { "type": "string", "format": "uuid" }, "position": { "type": "number" }, "pricingMode": { "$ref": "#/components/schemas/quoteTemplateLinePricingModeSchema" }, "productId": { "type": [ "string", "null" ], "format": "uuid" }, "quantity": { "type": "number" }, "subscriptionInterval": { "anyOf": [ { "$ref": "#/components/schemas/quoteTemplateSubscriptionIntervalSchema" }, { "type": "null" } ] }, "subscriptionPeriods": { "type": [ "integer", "null" ], "exclusiveMinimum": 0 }, "templateId": { "$ref": "#/components/schemas/templateIdSchema" }, "title": { "type": "string" }, "unit": { "type": [ "string", "null" ] }, "unitPrice": { "type": "number" }, "updatedAt": { "type": "string", "format": "date-time" }, "variantId": { "type": [ "string", "null" ], "format": "uuid" }, "vatRate": { "type": "number" } }, "required": [ "createdAt", "description", "discountKind", "discountValue", "id", "isOptional", "kind", "lineType", "organizationId", "position", "pricingMode", "productId", "quantity", "subscriptionInterval", "subscriptionPeriods", "templateId", "title", "unit", "unitPrice", "updatedAt", "variantId", "vatRate" ] }, "quoteTemplateSchema": { "type": "object", "properties": { "id": { "$ref": "#/components/schemas/templateIdSchema" }, "organizationId": { "type": "string", "format": "uuid" }, "type": { "type": "string", "enum": [ "quote" ] }, "language": { "$ref": "#/components/schemas/templateLanguageSchema" }, "isDefault": { "type": "boolean" }, "name": { "type": "string" }, "description": { "type": [ "string", "null" ] }, "title": { "type": "string" }, "intro": { "type": [ "string", "null" ] }, "terms": { "type": [ "string", "null" ] }, "internalNotes": { "type": [ "string", "null" ] }, "publicMessage": { "type": [ "string", "null" ] }, "footerText": { "type": [ "string", "null" ] }, "currency": { "$ref": "#/components/schemas/currencyCode" }, "vatInclusive": { "type": "boolean" }, "discountKind": { "$ref": "#/components/schemas/discountKindSchema" }, "discountValue": { "type": "number" }, "defaultDueDays": { "type": [ "integer", "null" ], "minimum": 0 }, "defaultValidityDays": { "type": [ "integer", "null" ], "exclusiveMinimum": 0 }, "vatRegime": { "$ref": "#/components/schemas/documentVatRegimeSchema" }, "termsPdfFileId": { "type": [ "string", "null" ], "format": "uuid" }, "termsPdfVersionId": { "type": [ "string", "null" ], "format": "uuid" }, "termsRichText": { "type": [ "string", "null" ], "maxLength": 100000 }, "termsUrl": { "type": [ "string", "null" ] }, "depositKind": { "$ref": "#/components/schemas/quoteDepositKindSchema" }, "depositRequiredToAccept": { "type": "boolean" }, "depositValue": { "type": "number", "minimum": 0 }, "lifecycleWorkflowKey": { "type": [ "string", "null" ] }, "orderDeliveryMethod": { "anyOf": [ { "$ref": "#/components/schemas/orderDeliveryMethodSchema" }, { "type": "null" } ] }, "orderInvoicingPolicy": { "anyOf": [ { "$ref": "#/components/schemas/orderInvoicingPolicySchema" }, { "type": "null" } ] }, "orderPaymentRequired": { "type": "boolean" }, "shipToLocationId": { "type": [ "string", "null" ], "format": "uuid" }, "createOrderOnAccept": { "type": "boolean" }, "archivedAt": { "type": [ "string", "null" ], "format": "date-time" }, "createdBy": { "type": [ "string", "null" ], "format": "uuid" }, "updatedBy": { "type": [ "string", "null" ], "format": "uuid" }, "createdAt": { "type": "string", "format": "date-time" }, "updatedAt": { "type": "string", "format": "date-time" } }, "required": [ "id", "organizationId", "type", "language", "isDefault", "name", "description", "title", "intro", "terms", "internalNotes", "publicMessage", "footerText", "currency", "vatInclusive", "discountKind", "discountValue", "defaultDueDays", "defaultValidityDays", "vatRegime", "termsPdfFileId", "termsPdfVersionId", "termsRichText", "termsUrl", "depositKind", "depositRequiredToAccept", "depositValue", "lifecycleWorkflowKey", "orderDeliveryMethod", "orderInvoicingPolicy", "orderPaymentRequired", "shipToLocationId", "createOrderOnAccept", "archivedAt", "createdBy", "updatedBy", "createdAt", "updatedAt" ] }, "quoteTemplateSubscriptionIntervalSchema": { "type": "string", "enum": [ "weekly", "monthly", "quarterly", "yearly" ] }, "taskPrioritySchema": { "type": "string", "enum": [ "low", "medium", "high", "urgent" ] }, "taskStatusSchema": { "type": "string", "enum": [ "todo", "in_progress", "on_hold", "done" ] }, "taskTemplateDetailSchema": { "type": "object", "properties": { "assigneeIds": { "type": "array", "items": { "type": "string", "format": "uuid" } }, "lines": { "type": "array", "prefixItems": [], "items": false }, "subtasks": { "type": "array", "items": { "$ref": "#/components/schemas/templateTaskSubtaskSchema" } }, "tagIds": { "type": "array", "items": { "type": "string", "format": "uuid" } }, "task": { "$ref": "#/components/schemas/templateTaskSettingsSchema" }, "template": { "$ref": "#/components/schemas/taskTemplateSchema" } }, "required": [ "assigneeIds", "lines", "subtasks", "tagIds", "task", "template" ] }, "taskTemplateSchema": { "type": "object", "properties": { "id": { "$ref": "#/components/schemas/templateIdSchema" }, "organizationId": { "type": "string", "format": "uuid" }, "type": { "type": "string", "enum": [ "task" ] }, "language": { "$ref": "#/components/schemas/templateLanguageSchema" }, "isDefault": { "type": "boolean" }, "name": { "type": "string" }, "description": { "type": [ "string", "null" ] }, "title": { "type": "string" }, "intro": { "type": [ "string", "null" ] }, "terms": { "type": [ "string", "null" ] }, "internalNotes": { "type": [ "string", "null" ] }, "publicMessage": { "type": [ "string", "null" ] }, "footerText": { "type": [ "string", "null" ] }, "currency": { "$ref": "#/components/schemas/currencyCode" }, "vatInclusive": { "type": "boolean" }, "discountKind": { "$ref": "#/components/schemas/discountKindSchema" }, "discountValue": { "type": "number" }, "defaultDueDays": { "type": [ "integer", "null" ], "minimum": 0 }, "defaultValidityDays": { "type": [ "integer", "null" ], "exclusiveMinimum": 0 }, "vatRegime": { "$ref": "#/components/schemas/documentVatRegimeSchema" }, "termsPdfFileId": { "type": [ "string", "null" ], "format": "uuid" }, "termsPdfVersionId": { "type": [ "string", "null" ], "format": "uuid" }, "termsRichText": { "type": [ "string", "null" ], "maxLength": 100000 }, "termsUrl": { "type": [ "string", "null" ] }, "depositKind": { "$ref": "#/components/schemas/quoteDepositKindSchema" }, "depositRequiredToAccept": { "type": "boolean" }, "depositValue": { "type": "number", "minimum": 0 }, "lifecycleWorkflowKey": { "type": [ "string", "null" ] }, "orderDeliveryMethod": { "anyOf": [ { "$ref": "#/components/schemas/orderDeliveryMethodSchema" }, { "type": "null" } ] }, "orderInvoicingPolicy": { "anyOf": [ { "$ref": "#/components/schemas/orderInvoicingPolicySchema" }, { "type": "null" } ] }, "orderPaymentRequired": { "type": "boolean" }, "shipToLocationId": { "type": [ "string", "null" ], "format": "uuid" }, "createOrderOnAccept": { "type": "boolean" }, "archivedAt": { "type": [ "string", "null" ], "format": "date-time" }, "createdBy": { "type": [ "string", "null" ], "format": "uuid" }, "updatedBy": { "type": [ "string", "null" ], "format": "uuid" }, "createdAt": { "type": "string", "format": "date-time" }, "updatedAt": { "type": "string", "format": "date-time" } }, "required": [ "id", "organizationId", "type", "language", "isDefault", "name", "description", "title", "intro", "terms", "internalNotes", "publicMessage", "footerText", "currency", "vatInclusive", "discountKind", "discountValue", "defaultDueDays", "defaultValidityDays", "vatRegime", "termsPdfFileId", "termsPdfVersionId", "termsRichText", "termsUrl", "depositKind", "depositRequiredToAccept", "depositValue", "lifecycleWorkflowKey", "orderDeliveryMethod", "orderInvoicingPolicy", "orderPaymentRequired", "shipToLocationId", "createOrderOnAccept", "archivedAt", "createdBy", "updatedBy", "createdAt", "updatedAt" ] }, "templateDetailSchema": { "anyOf": [ { "$ref": "#/components/schemas/invoiceTemplateDetailSchema" }, { "$ref": "#/components/schemas/quoteTemplateDetailSchema" }, { "$ref": "#/components/schemas/taskTemplateDetailSchema" }, { "$ref": "#/components/schemas/orderTemplateDetailSchema" }, { "$ref": "#/components/schemas/purchaseOrderTemplateDetailSchema" } ] }, "templateIdSchema": { "type": "string", "format": "uuid" }, "templateLanguageSchema": { "$ref": "#/components/schemas/appLocaleSchema" }, "templateLineKindSchema": { "type": "string", "enum": [ "line", "section_header" ] }, "templateLineTypeSchema": { "type": "string", "enum": [ "product", "labor", "custom" ] }, "templateTaskSettingsSchema": { "type": "object", "properties": { "customerAssetId": { "type": [ "string", "null" ], "format": "uuid" }, "customerId": { "type": [ "string", "null" ], "format": "uuid" }, "description": { "type": [ "string", "null" ] }, "priority": { "$ref": "#/components/schemas/taskPrioritySchema" }, "status": { "$ref": "#/components/schemas/taskStatusSchema" }, "title": { "type": "string" } }, "required": [ "customerAssetId", "customerId", "description", "priority", "status", "title" ] }, "templateTaskSubtaskSchema": { "type": "object", "properties": { "createdAt": { "type": "string", "format": "date-time" }, "id": { "type": "string", "format": "uuid" }, "organizationId": { "type": "string", "format": "uuid" }, "position": { "type": "number" }, "priority": { "$ref": "#/components/schemas/taskPrioritySchema" }, "status": { "$ref": "#/components/schemas/taskStatusSchema" }, "templateId": { "$ref": "#/components/schemas/templateIdSchema" }, "title": { "type": "string" }, "updatedAt": { "type": "string", "format": "date-time" } }, "required": [ "createdAt", "id", "organizationId", "position", "priority", "status", "templateId", "title", "updatedAt" ] } } ``` # Update a template. (/docs/openapi/updateTemplate) Update a reusable quote or invoice template for the specified organization. ## PATCH /v1/organizations/{organizationId}/templates/{templateId} ```json { "operationId": "updateTemplate", "summary": "Update a template.", "description": "Update a reusable quote or invoice template for the specified organization.", "tags": [ "Templates" ], "parameters": [ { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" }, { "in": "path", "name": "templateId", "required": true, "schema": { "$ref": "#/components/schemas/templateIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/templatePatchSchema" } } }, "description": "The template patch payload." }, "responses": { "200": { "description": "Returns the updated template.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/templateSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "appLocaleSchema": { "type": "string", "enum": [ "en", "nl", "de" ] }, "currencyCode": { "type": "string", "pattern": "^[A-Z]{3}$" }, "discountKindSchema": { "type": "string", "enum": [ "none", "percentage", "fixed" ] }, "documentVatRegimeSchema": { "type": "string", "enum": [ "standard", "reverse_charge", "exempt", "kor", "margin_scheme" ] }, "invoiceTemplateLineInputSchema": { "type": "object", "properties": { "description": { "type": [ "string", "null" ], "maxLength": 20000 }, "discountKind": { "allOf": [ { "$ref": "#/components/schemas/discountKindSchema" } ] }, "discountValue": { "type": "number", "minimum": 0 }, "id": { "type": "string", "format": "uuid" }, "kind": { "allOf": [ { "$ref": "#/components/schemas/templateLineKindSchema" } ] }, "lineType": { "anyOf": [ { "$ref": "#/components/schemas/templateLineTypeSchema" }, { "type": "null" } ] }, "position": { "type": "number" }, "pricingMode": { "allOf": [ { "$ref": "#/components/schemas/invoiceTemplateLinePricingModeSchema" } ] }, "productId": { "type": [ "string", "null" ], "format": "uuid" }, "quantity": { "type": "number", "minimum": 0 }, "title": { "type": "string", "minLength": 1, "maxLength": 500 }, "unit": { "type": [ "string", "null" ], "maxLength": 40 }, "unitPrice": { "type": "number" }, "variantId": { "type": [ "string", "null" ], "format": "uuid" }, "vatRate": { "type": "number", "minimum": 0, "maximum": 100 } }, "required": [ "title" ], "additionalProperties": false }, "invoiceTemplateLinePricingModeSchema": { "type": "string", "enum": [ "unit", "hourly", "fixed" ] }, "invoiceTemplatePatchSchema": { "type": "object", "properties": { "currency": { "allOf": [ { "$ref": "#/components/schemas/currencyCode" } ] }, "defaultDueDays": { "type": [ "integer", "null" ], "minimum": 0 }, "defaultValidityDays": { "type": [ "integer", "null" ], "exclusiveMinimum": 0 }, "description": { "type": [ "string", "null" ], "maxLength": 20000 }, "discountKind": { "allOf": [ { "$ref": "#/components/schemas/discountKindSchema" } ] }, "discountValue": { "type": "number", "minimum": 0 }, "footerText": { "type": [ "string", "null" ], "maxLength": 20000 }, "internalNotes": { "type": [ "string", "null" ], "maxLength": 20000 }, "intro": { "type": [ "string", "null" ], "maxLength": 20000 }, "isDefault": { "type": "boolean" }, "language": { "allOf": [ { "$ref": "#/components/schemas/templateLanguageSchema" } ] }, "lifecycleWorkflowKey": { "allOf": [ { "$ref": "#/components/schemas/templateLifecycleWorkflowKeySchema" } ] }, "name": { "type": "string", "minLength": 1, "maxLength": 240 }, "publicMessage": { "type": [ "string", "null" ], "maxLength": 20000 }, "terms": { "type": [ "string", "null" ], "maxLength": 20000 }, "title": { "type": "string", "minLength": 1, "maxLength": 240 }, "vatInclusive": { "type": "boolean" }, "vatRegime": { "allOf": [ { "$ref": "#/components/schemas/documentVatRegimeSchema" } ] }, "termsPdfFileId": { "type": [ "string", "null" ], "format": "uuid" }, "termsPdfVersionId": { "type": [ "string", "null" ], "format": "uuid" }, "termsRichText": { "type": [ "string", "null" ], "maxLength": 100000 }, "termsUrl": { "allOf": [ { "$ref": "#/components/schemas/optionalUrl" } ] }, "lines": { "type": "array", "items": { "$ref": "#/components/schemas/invoiceTemplateLineInputSchema" }, "maxItems": 500 }, "type": { "type": "string", "enum": [ "invoice" ] } } }, "optionalUrl": { "type": [ "string", "null" ], "maxLength": 2000 }, "orderDeliveryMethodSchema": { "type": "string", "enum": [ "pickup", "own_delivery", "carrier", "digital", "none" ] }, "orderInvoicingPolicySchema": { "type": "string", "enum": [ "before_fulfillment", "on_fulfillment", "manual" ] }, "orderTemplatePatchSchema": { "type": "object", "properties": { "currency": { "allOf": [ { "$ref": "#/components/schemas/currencyCode" } ] }, "defaultDueDays": { "type": [ "integer", "null" ], "minimum": 0 }, "defaultValidityDays": { "type": [ "integer", "null" ], "exclusiveMinimum": 0 }, "description": { "type": [ "string", "null" ], "maxLength": 20000 }, "discountKind": { "allOf": [ { "$ref": "#/components/schemas/discountKindSchema" } ] }, "discountValue": { "type": "number", "minimum": 0 }, "footerText": { "type": [ "string", "null" ], "maxLength": 20000 }, "internalNotes": { "type": [ "string", "null" ], "maxLength": 20000 }, "intro": { "type": [ "string", "null" ], "maxLength": 20000 }, "isDefault": { "type": "boolean" }, "language": { "allOf": [ { "$ref": "#/components/schemas/templateLanguageSchema" } ] }, "lifecycleWorkflowKey": { "allOf": [ { "$ref": "#/components/schemas/templateLifecycleWorkflowKeySchema" } ] }, "name": { "type": "string", "minLength": 1, "maxLength": 240 }, "publicMessage": { "type": [ "string", "null" ], "maxLength": 20000 }, "terms": { "type": [ "string", "null" ], "maxLength": 20000 }, "title": { "type": "string", "minLength": 1, "maxLength": 240 }, "vatInclusive": { "type": "boolean" }, "vatRegime": { "allOf": [ { "$ref": "#/components/schemas/documentVatRegimeSchema" } ] }, "termsPdfFileId": { "type": [ "string", "null" ], "format": "uuid" }, "termsPdfVersionId": { "type": [ "string", "null" ], "format": "uuid" }, "termsRichText": { "type": [ "string", "null" ], "maxLength": 100000 }, "termsUrl": { "allOf": [ { "$ref": "#/components/schemas/optionalUrl" } ] }, "orderDeliveryMethod": { "anyOf": [ { "$ref": "#/components/schemas/orderDeliveryMethodSchema" }, { "type": "null" } ] }, "orderInvoicingPolicy": { "anyOf": [ { "$ref": "#/components/schemas/orderInvoicingPolicySchema" }, { "type": "null" } ] }, "orderPaymentRequired": { "type": "boolean" }, "type": { "type": "string", "enum": [ "order" ] } } }, "organizationIdSchema": { "type": "string", "format": "uuid" }, "purchaseOrderTemplatePatchSchema": { "type": "object", "properties": { "currency": { "allOf": [ { "$ref": "#/components/schemas/currencyCode" } ] }, "defaultDueDays": { "type": [ "integer", "null" ], "minimum": 0 }, "defaultValidityDays": { "type": [ "integer", "null" ], "exclusiveMinimum": 0 }, "description": { "type": [ "string", "null" ], "maxLength": 20000 }, "discountKind": { "allOf": [ { "$ref": "#/components/schemas/discountKindSchema" } ] }, "discountValue": { "type": "number", "minimum": 0 }, "footerText": { "type": [ "string", "null" ], "maxLength": 20000 }, "internalNotes": { "type": [ "string", "null" ], "maxLength": 20000 }, "intro": { "type": [ "string", "null" ], "maxLength": 20000 }, "isDefault": { "type": "boolean" }, "language": { "allOf": [ { "$ref": "#/components/schemas/templateLanguageSchema" } ] }, "lifecycleWorkflowKey": { "allOf": [ { "$ref": "#/components/schemas/templateLifecycleWorkflowKeySchema" } ] }, "name": { "type": "string", "minLength": 1, "maxLength": 240 }, "publicMessage": { "type": [ "string", "null" ], "maxLength": 20000 }, "terms": { "type": [ "string", "null" ], "maxLength": 20000 }, "title": { "type": "string", "minLength": 1, "maxLength": 240 }, "vatInclusive": { "type": "boolean" }, "vatRegime": { "allOf": [ { "$ref": "#/components/schemas/documentVatRegimeSchema" } ] }, "termsPdfFileId": { "type": [ "string", "null" ], "format": "uuid" }, "termsPdfVersionId": { "type": [ "string", "null" ], "format": "uuid" }, "termsRichText": { "type": [ "string", "null" ], "maxLength": 100000 }, "termsUrl": { "allOf": [ { "$ref": "#/components/schemas/optionalUrl" } ] }, "shipToLocationId": { "type": [ "string", "null" ], "format": "uuid" }, "type": { "type": "string", "enum": [ "purchase_order" ] } } }, "quoteDepositKindSchema": { "type": "string", "enum": [ "none", "percent", "fixed" ] }, "quoteTemplateLineInputSchema": { "type": "object", "properties": { "description": { "type": [ "string", "null" ], "maxLength": 20000 }, "discountKind": { "allOf": [ { "$ref": "#/components/schemas/discountKindSchema" } ] }, "discountValue": { "type": "number", "minimum": 0 }, "id": { "type": "string", "format": "uuid" }, "isOptional": { "type": "boolean" }, "kind": { "allOf": [ { "$ref": "#/components/schemas/templateLineKindSchema" } ] }, "lineType": { "anyOf": [ { "$ref": "#/components/schemas/templateLineTypeSchema" }, { "type": "null" } ] }, "position": { "type": "number" }, "pricingMode": { "allOf": [ { "$ref": "#/components/schemas/quoteTemplateLinePricingModeSchema" } ] }, "productId": { "type": [ "string", "null" ], "format": "uuid" }, "quantity": { "type": "number", "minimum": 0 }, "subscriptionInterval": { "anyOf": [ { "$ref": "#/components/schemas/quoteTemplateSubscriptionIntervalSchema" }, { "type": "null" } ] }, "subscriptionPeriods": { "type": [ "integer", "null" ], "exclusiveMinimum": 0 }, "title": { "type": "string", "minLength": 1, "maxLength": 500 }, "unit": { "type": [ "string", "null" ], "maxLength": 40 }, "unitPrice": { "type": "number" }, "variantId": { "type": [ "string", "null" ], "format": "uuid" }, "vatRate": { "type": "number", "minimum": 0, "maximum": 100 } }, "required": [ "title" ], "additionalProperties": false }, "quoteTemplateLinePricingModeSchema": { "type": "string", "enum": [ "unit", "hourly", "subscription", "fixed" ] }, "quoteTemplatePatchSchema": { "type": "object", "properties": { "currency": { "allOf": [ { "$ref": "#/components/schemas/currencyCode" } ] }, "defaultDueDays": { "type": [ "integer", "null" ], "minimum": 0 }, "defaultValidityDays": { "type": [ "integer", "null" ], "exclusiveMinimum": 0 }, "description": { "type": [ "string", "null" ], "maxLength": 20000 }, "discountKind": { "allOf": [ { "$ref": "#/components/schemas/discountKindSchema" } ] }, "discountValue": { "type": "number", "minimum": 0 }, "footerText": { "type": [ "string", "null" ], "maxLength": 20000 }, "internalNotes": { "type": [ "string", "null" ], "maxLength": 20000 }, "intro": { "type": [ "string", "null" ], "maxLength": 20000 }, "isDefault": { "type": "boolean" }, "language": { "allOf": [ { "$ref": "#/components/schemas/templateLanguageSchema" } ] }, "lifecycleWorkflowKey": { "allOf": [ { "$ref": "#/components/schemas/templateLifecycleWorkflowKeySchema" } ] }, "name": { "type": "string", "minLength": 1, "maxLength": 240 }, "publicMessage": { "type": [ "string", "null" ], "maxLength": 20000 }, "terms": { "type": [ "string", "null" ], "maxLength": 20000 }, "title": { "type": "string", "minLength": 1, "maxLength": 240 }, "vatInclusive": { "type": "boolean" }, "vatRegime": { "allOf": [ { "$ref": "#/components/schemas/documentVatRegimeSchema" } ] }, "termsPdfFileId": { "type": [ "string", "null" ], "format": "uuid" }, "termsPdfVersionId": { "type": [ "string", "null" ], "format": "uuid" }, "termsRichText": { "type": [ "string", "null" ], "maxLength": 100000 }, "termsUrl": { "allOf": [ { "$ref": "#/components/schemas/optionalUrl" } ] }, "createOrderOnAccept": { "type": "boolean" }, "depositKind": { "allOf": [ { "$ref": "#/components/schemas/quoteDepositKindSchema" } ] }, "depositRequiredToAccept": { "type": "boolean" }, "depositValue": { "type": "number", "minimum": 0 }, "lines": { "type": "array", "items": { "$ref": "#/components/schemas/quoteTemplateLineInputSchema" }, "maxItems": 500 }, "type": { "type": "string", "enum": [ "quote" ] } } }, "quoteTemplateSubscriptionIntervalSchema": { "type": "string", "enum": [ "weekly", "monthly", "quarterly", "yearly" ] }, "taskPrioritySchema": { "type": "string", "enum": [ "low", "medium", "high", "urgent" ] }, "taskStatusSchema": { "type": "string", "enum": [ "todo", "in_progress", "on_hold", "done" ] }, "taskTemplatePatchSchema": { "type": "object", "properties": { "currency": { "allOf": [ { "$ref": "#/components/schemas/currencyCode" } ] }, "defaultDueDays": { "type": [ "integer", "null" ], "minimum": 0 }, "defaultValidityDays": { "type": [ "integer", "null" ], "exclusiveMinimum": 0 }, "description": { "type": [ "string", "null" ], "maxLength": 20000 }, "discountKind": { "allOf": [ { "$ref": "#/components/schemas/discountKindSchema" } ] }, "discountValue": { "type": "number", "minimum": 0 }, "footerText": { "type": [ "string", "null" ], "maxLength": 20000 }, "internalNotes": { "type": [ "string", "null" ], "maxLength": 20000 }, "intro": { "type": [ "string", "null" ], "maxLength": 20000 }, "isDefault": { "type": "boolean" }, "language": { "allOf": [ { "$ref": "#/components/schemas/templateLanguageSchema" } ] }, "lifecycleWorkflowKey": { "allOf": [ { "$ref": "#/components/schemas/templateLifecycleWorkflowKeySchema" } ] }, "name": { "type": "string", "minLength": 1, "maxLength": 240 }, "publicMessage": { "type": [ "string", "null" ], "maxLength": 20000 }, "terms": { "type": [ "string", "null" ], "maxLength": 20000 }, "title": { "type": "string", "minLength": 1, "maxLength": 240 }, "vatInclusive": { "type": "boolean" }, "vatRegime": { "allOf": [ { "$ref": "#/components/schemas/documentVatRegimeSchema" } ] }, "termsPdfFileId": { "type": [ "string", "null" ], "format": "uuid" }, "termsPdfVersionId": { "type": [ "string", "null" ], "format": "uuid" }, "termsRichText": { "type": [ "string", "null" ], "maxLength": 100000 }, "termsUrl": { "allOf": [ { "$ref": "#/components/schemas/optionalUrl" } ] }, "assigneeIds": { "type": "array", "items": { "type": "string", "format": "uuid" }, "maxItems": 50 }, "subtasks": { "type": "array", "items": { "$ref": "#/components/schemas/templateTaskSubtaskInputSchema" }, "maxItems": 100 }, "tagIds": { "type": "array", "items": { "type": "string", "format": "uuid" }, "maxItems": 50 }, "task": { "$ref": "#/components/schemas/templateTaskSettingsInputSchema" }, "type": { "type": "string", "enum": [ "task" ] } } }, "templateIdSchema": { "type": "string", "format": "uuid" }, "templateLanguageSchema": { "$ref": "#/components/schemas/appLocaleSchema" }, "templateLifecycleWorkflowKeySchema": { "type": [ "string", "null" ], "minLength": 1, "maxLength": 120, "pattern": "^[a-z0-9][a-z0-9-]*$" }, "templateLineKindSchema": { "type": "string", "enum": [ "line", "section_header" ] }, "templateLineTypeSchema": { "type": "string", "enum": [ "product", "labor", "custom" ] }, "templatePatchSchema": { "anyOf": [ { "$ref": "#/components/schemas/invoiceTemplatePatchSchema" }, { "$ref": "#/components/schemas/quoteTemplatePatchSchema" }, { "$ref": "#/components/schemas/taskTemplatePatchSchema" }, { "$ref": "#/components/schemas/orderTemplatePatchSchema" }, { "$ref": "#/components/schemas/purchaseOrderTemplatePatchSchema" } ] }, "templateSchema": { "type": "object", "properties": { "id": { "$ref": "#/components/schemas/templateIdSchema" }, "organizationId": { "type": "string", "format": "uuid" }, "type": { "$ref": "#/components/schemas/templateTypeSchema" }, "language": { "$ref": "#/components/schemas/templateLanguageSchema" }, "isDefault": { "type": "boolean" }, "name": { "type": "string" }, "description": { "type": [ "string", "null" ] }, "title": { "type": "string" }, "intro": { "type": [ "string", "null" ] }, "terms": { "type": [ "string", "null" ] }, "internalNotes": { "type": [ "string", "null" ] }, "publicMessage": { "type": [ "string", "null" ] }, "footerText": { "type": [ "string", "null" ] }, "currency": { "$ref": "#/components/schemas/currencyCode" }, "vatInclusive": { "type": "boolean" }, "discountKind": { "$ref": "#/components/schemas/discountKindSchema" }, "discountValue": { "type": "number" }, "defaultDueDays": { "type": [ "integer", "null" ], "minimum": 0 }, "defaultValidityDays": { "type": [ "integer", "null" ], "exclusiveMinimum": 0 }, "vatRegime": { "$ref": "#/components/schemas/documentVatRegimeSchema" }, "termsPdfFileId": { "type": [ "string", "null" ], "format": "uuid" }, "termsPdfVersionId": { "type": [ "string", "null" ], "format": "uuid" }, "termsRichText": { "type": [ "string", "null" ], "maxLength": 100000 }, "termsUrl": { "type": [ "string", "null" ] }, "depositKind": { "$ref": "#/components/schemas/quoteDepositKindSchema" }, "depositRequiredToAccept": { "type": "boolean" }, "depositValue": { "type": "number", "minimum": 0 }, "lifecycleWorkflowKey": { "type": [ "string", "null" ] }, "orderDeliveryMethod": { "anyOf": [ { "$ref": "#/components/schemas/orderDeliveryMethodSchema" }, { "type": "null" } ] }, "orderInvoicingPolicy": { "anyOf": [ { "$ref": "#/components/schemas/orderInvoicingPolicySchema" }, { "type": "null" } ] }, "orderPaymentRequired": { "type": "boolean" }, "shipToLocationId": { "type": [ "string", "null" ], "format": "uuid" }, "createOrderOnAccept": { "type": "boolean" }, "archivedAt": { "type": [ "string", "null" ], "format": "date-time" }, "createdBy": { "type": [ "string", "null" ], "format": "uuid" }, "updatedBy": { "type": [ "string", "null" ], "format": "uuid" }, "createdAt": { "type": "string", "format": "date-time" }, "updatedAt": { "type": "string", "format": "date-time" } }, "required": [ "id", "organizationId", "type", "language", "isDefault", "name", "description", "title", "intro", "terms", "internalNotes", "publicMessage", "footerText", "currency", "vatInclusive", "discountKind", "discountValue", "defaultDueDays", "defaultValidityDays", "vatRegime", "termsPdfFileId", "termsPdfVersionId", "termsRichText", "termsUrl", "depositKind", "depositRequiredToAccept", "depositValue", "lifecycleWorkflowKey", "orderDeliveryMethod", "orderInvoicingPolicy", "orderPaymentRequired", "shipToLocationId", "createOrderOnAccept", "archivedAt", "createdBy", "updatedBy", "createdAt", "updatedAt" ] }, "templateTaskSettingsInputSchema": { "type": "object", "properties": { "customerAssetId": { "type": [ "string", "null" ], "format": "uuid" }, "customerId": { "type": [ "string", "null" ], "format": "uuid" }, "description": { "type": [ "string", "null" ], "maxLength": 20000 }, "priority": { "allOf": [ { "$ref": "#/components/schemas/taskPrioritySchema" } ] }, "status": { "allOf": [ { "$ref": "#/components/schemas/taskStatusSchema" } ] }, "title": { "type": "string", "minLength": 1, "maxLength": 240 } }, "required": [ "title" ] }, "templateTaskSubtaskInputSchema": { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "position": { "type": "number" }, "priority": { "allOf": [ { "$ref": "#/components/schemas/taskPrioritySchema" } ] }, "status": { "allOf": [ { "$ref": "#/components/schemas/taskStatusSchema" } ] }, "title": { "type": "string", "minLength": 1, "maxLength": 240 } }, "required": [ "title" ] }, "templateTypeSchema": { "type": "string", "enum": [ "invoice", "quote", "task", "order", "purchase_order" ] } } ``` # Delete a template. (/docs/openapi/deleteTemplate) Delete a reusable quote or invoice template for the specified organization. ## DELETE /v1/organizations/{organizationId}/templates/{templateId} ```json { "operationId": "deleteTemplate", "summary": "Delete a template.", "description": "Delete a reusable quote or invoice template for the specified organization.", "tags": [ "Templates" ], "parameters": [ { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" }, { "in": "path", "name": "templateId", "required": true, "schema": { "$ref": "#/components/schemas/templateIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "responses": { "200": { "description": "Returns a success payload when the template is deleted.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/publicApiSuccessResponseSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "organizationIdSchema": { "type": "string", "format": "uuid" }, "publicApiSuccessResponseSchema": { "type": "object", "properties": { "success": { "type": "boolean", "enum": [ true ] } }, "required": [ "success" ] }, "templateIdSchema": { "type": "string", "format": "uuid" } } ``` # Download a team leave feed by token. (/docs/openapi/downloadPublicTimeOffFeed) Returns approved team leave as an iCalendar feed through a private token link. Access is revalidated on every fetch and sick-leave reasons are hidden. ## GET /v1/public/time-off/{token} ```json { "operationId": "downloadPublicTimeOffFeed", "summary": "Download a team leave feed by token.", "description": "Returns approved team leave as an iCalendar feed through a private token link. Access is revalidated on every fetch and sick-leave reasons are hidden.", "tags": [ "Time off" ], "parameters": [ { "in": "path", "name": "token", "required": true, "schema": { "type": "string", "minLength": 1 }, "example": "example" } ], "responses": { "200": { "description": "Returns team leave as an iCalendar document.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/publicTimeOffFeedResponseSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "publicTimeOffFeedResponseSchema": { "type": "string" } } ``` # Get the current authenticated user. (/docs/openapi/getCurrentUser) Return the currently authenticated user for the active request. Accepts a bearer token for API clients while continuing to support the current browser session. ## GET /v1/users/me ```json { "operationId": "getCurrentUser", "summary": "Get the current authenticated user.", "description": "Return the currently authenticated user for the active request. Accepts a bearer token for API clients while continuing to support the current browser session.", "tags": [ "Users" ], "parameters": [], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "responses": { "200": { "description": "Returns the current authenticated user.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/authUserSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "authUserIdSchema": { "type": "string", "format": "uuid" }, "authUserSchema": { "type": "object", "properties": { "createdAt": { "type": "string", "format": "date-time" }, "email": { "type": [ "string", "null" ], "format": "email" }, "emailVerified": { "type": "boolean" }, "id": { "$ref": "#/components/schemas/authUserIdSchema" }, "lastSignInAt": { "type": [ "string", "null" ], "format": "date-time" } }, "required": [ "createdAt", "email", "emailVerified", "id", "lastSignInAt" ] } } ``` # Receive an incoming organization webhook. (/docs/openapi/receivePublicWebhook) Accepts a public token URL and checks the endpoint's verification: a Standard Webhooks signature (`webhook-id`, `webhook-timestamp` and `webhook-signature` within five minutes), the hex HMAC-SHA256 of the body in the endpoint's signature header, or the token alone. Verified deliveries are stored and start matching workflows shortly after; a repeated `webhook-id` is stored once. Bodies over 256 KB are rejected. ## POST /v1/public/webhooks/{token} ```json { "operationId": "receivePublicWebhook", "summary": "Receive an incoming organization webhook.", "description": "Accepts a public token URL and checks the endpoint's verification: a Standard Webhooks signature (`webhook-id`, `webhook-timestamp` and `webhook-signature` within five minutes), the hex HMAC-SHA256 of the body in the endpoint's signature header, or the token alone. Verified deliveries are stored and start matching workflows shortly after; a repeated `webhook-id` is stored once. Bodies over 256 KB are rejected.", "tags": [ "Webhooks" ], "parameters": [ { "in": "path", "name": "token", "required": true, "schema": { "type": "string", "minLength": 16, "maxLength": 80 }, "example": "example" } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/publicWebhookBodySchema" } } }, "description": "JSON payload forwarded to matching workflow triggers." }, "responses": { "202": { "description": "Returns the stored delivery id; `200` with `duplicate: true` for a delivery received before.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/publicWebhookResponseSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "jsonValueSchema": { "anyOf": [ { "type": "string" }, { "type": "number" }, { "type": "boolean" }, { "type": "null" }, { "type": "array", "items": { "$ref": "#/components/schemas/jsonValueSchema" } }, { "type": "object", "additionalProperties": { "$ref": "#/components/schemas/jsonValueSchema" }, "propertyNames": { "type": "string" } } ] }, "publicWebhookBodySchema": { "type": "object", "additionalProperties": { "$ref": "#/components/schemas/jsonValueSchema" }, "propertyNames": { "type": "string" }, "examples": [ { "orderId": "ord_123" } ] }, "publicWebhookResponseSchema": { "type": "object", "properties": { "duplicate": { "type": "boolean" }, "id": { "type": "integer" } }, "required": [ "duplicate", "id" ], "examples": [ { "duplicate": false, "id": 4182 } ] } } ``` # List webhook deliveries. (/docs/openapi/listWebhookDeliveries) Return paginated delivery attempts, optionally filtered by destination. ## GET /v1/organizations/{organizationId}/webhooks/deliveries ```json { "operationId": "listWebhookDeliveries", "summary": "List webhook deliveries.", "description": "Return paginated delivery attempts, optionally filtered by destination.", "tags": [ "Webhooks" ], "parameters": [ { "in": "query", "name": "destinationId", "required": false, "schema": { "type": "string" }, "example": "123" }, { "in": "query", "name": "limit", "required": false, "schema": { "type": "string" }, "example": "example" }, { "in": "query", "name": "offset", "required": false, "schema": { "type": "string" }, "example": "example" }, { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "responses": { "200": { "description": "Returns paginated outgoing webhook deliveries.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/webhookDeliveriesListResponseSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "dateTimeSchema": { "type": "string" }, "jsonObjectSchema": { "type": "object", "additionalProperties": { "$ref": "#/components/schemas/jsonValueSchema" }, "propertyNames": { "type": "string" } }, "jsonValueSchema": { "anyOf": [ { "type": "string" }, { "type": "number" }, { "type": "boolean" }, { "type": "null" }, { "type": "array", "items": { "$ref": "#/components/schemas/jsonValueSchema" } }, { "type": "object", "additionalProperties": { "$ref": "#/components/schemas/jsonValueSchema" }, "propertyNames": { "type": "string" } } ] }, "listPaginationSchema": { "type": "object", "properties": { "hasMore": { "type": "boolean" }, "limit": { "type": "integer", "minimum": 1, "maximum": 100 }, "offset": { "type": "integer", "minimum": 0 }, "total": { "type": "integer", "minimum": 0 }, "totalIsEstimate": { "type": "boolean" } }, "required": [ "hasMore", "limit", "offset", "total" ] }, "organizationIdSchema": { "type": "string", "format": "uuid" }, "webhookDeliveriesListResponseSchema": { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/components/schemas/webhookDeliverySchema" } }, "pagination": { "$ref": "#/components/schemas/listPaginationSchema" } }, "required": [ "data", "pagination" ] }, "webhookDeliverySchema": { "type": "object", "properties": { "attempt": { "type": "integer", "minimum": 0 }, "availableAt": { "$ref": "#/components/schemas/dateTimeSchema" }, "createdAt": { "$ref": "#/components/schemas/dateTimeSchema" }, "destinationId": { "type": "string", "format": "uuid" }, "durationMs": { "type": [ "integer", "null" ] }, "eventId": { "type": [ "string", "null" ], "format": "uuid" }, "eventKind": { "type": "string", "minLength": 1, "maxLength": 160 }, "id": { "type": "string", "format": "uuid" }, "lastError": { "type": [ "string", "null" ] }, "leaseExpiresAt": { "anyOf": [ { "$ref": "#/components/schemas/dateTimeSchema" }, { "type": "null" } ] }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "payload": { "$ref": "#/components/schemas/jsonObjectSchema" }, "processedAt": { "anyOf": [ { "$ref": "#/components/schemas/dateTimeSchema" }, { "type": "null" } ] }, "responseBody": { "type": [ "string", "null" ] }, "responseStatus": { "type": [ "integer", "null" ] }, "status": { "$ref": "#/components/schemas/webhookDeliveryStatusSchema" }, "updatedAt": { "$ref": "#/components/schemas/dateTimeSchema" }, "workflowRunId": { "type": [ "string", "null" ], "format": "uuid" } }, "required": [ "attempt", "availableAt", "createdAt", "destinationId", "durationMs", "eventId", "eventKind", "id", "lastError", "leaseExpiresAt", "organizationId", "payload", "processedAt", "responseBody", "responseStatus", "status", "updatedAt", "workflowRunId" ] }, "webhookDeliveryStatusSchema": { "type": "string", "enum": [ "pending", "processing", "completed", "failed", "dead_lettered", "canceled" ] } } ``` # List webhook destinations. (/docs/openapi/listWebhookDestinations) Return outgoing webhook destinations without their signing secrets. ## GET /v1/organizations/{organizationId}/webhooks/destinations ```json { "operationId": "listWebhookDestinations", "summary": "List webhook destinations.", "description": "Return outgoing webhook destinations without their signing secrets.", "tags": [ "Webhooks" ], "parameters": [ { "in": "query", "name": "limit", "required": false, "schema": { "type": "string" }, "example": "example" }, { "in": "query", "name": "offset", "required": false, "schema": { "type": "string" }, "example": "example" }, { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "responses": { "200": { "description": "Returns paginated outgoing webhook destinations.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/webhookDestinationsListResponseSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "dateTimeSchema": { "type": "string" }, "listPaginationSchema": { "type": "object", "properties": { "hasMore": { "type": "boolean" }, "limit": { "type": "integer", "minimum": 1, "maximum": 100 }, "offset": { "type": "integer", "minimum": 0 }, "total": { "type": "integer", "minimum": 0 }, "totalIsEstimate": { "type": "boolean" } }, "required": [ "hasMore", "limit", "offset", "total" ] }, "organizationIdSchema": { "type": "string", "format": "uuid" }, "webhookDestinationSchema": { "type": "object", "properties": { "createdAt": { "$ref": "#/components/schemas/dateTimeSchema" }, "createdBy": { "type": [ "string", "null" ], "format": "uuid" }, "disabledAt": { "anyOf": [ { "$ref": "#/components/schemas/dateTimeSchema" }, { "type": "null" } ] }, "disabledReason": { "type": [ "string", "null" ] }, "enabled": { "type": "boolean" }, "eventKinds": { "type": "array", "items": { "type": "string", "minLength": 1, "maxLength": 160 } }, "failingAt": { "anyOf": [ { "$ref": "#/components/schemas/dateTimeSchema" }, { "type": "null" } ] }, "id": { "type": "string", "format": "uuid" }, "name": { "type": "string", "minLength": 1, "maxLength": 160 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "updatedAt": { "$ref": "#/components/schemas/dateTimeSchema" }, "url": { "type": "string", "format": "uri" } }, "required": [ "createdAt", "createdBy", "disabledAt", "disabledReason", "enabled", "eventKinds", "failingAt", "id", "name", "organizationId", "updatedAt", "url" ] }, "webhookDestinationsListResponseSchema": { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/components/schemas/webhookDestinationSchema" } }, "pagination": { "$ref": "#/components/schemas/listPaginationSchema" } }, "required": [ "data", "pagination" ] } } ``` # Create a webhook destination. (/docs/openapi/createWebhookDestination) Create an HTTPS destination for outgoing webhook events. The signing secret is returned only by this operation or an explicit rotation. ## POST /v1/organizations/{organizationId}/webhooks/destinations ```json { "operationId": "createWebhookDestination", "summary": "Create a webhook destination.", "description": "Create an HTTPS destination for outgoing webhook events. The signing secret is returned only by this operation or an explicit rotation.", "tags": [ "Webhooks" ], "parameters": [ { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/webhookDestinationCreateRequestSchema" } } }, "description": "Outgoing webhook destination settings and subscribed event kinds.", "required": true }, "responses": { "201": { "description": "Returns the created destination and its one-time signing secret.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/webhookDestinationSecretResponseSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "dateTimeSchema": { "type": "string" }, "organizationIdSchema": { "type": "string", "format": "uuid" }, "webhookDestinationCreateRequestSchema": { "type": "object", "properties": { "eventKinds": { "type": "array", "items": { "type": "string", "minLength": 1, "maxLength": 160 } }, "name": { "type": "string", "minLength": 1, "maxLength": 160 }, "url": { "type": "string", "format": "uri" } }, "required": [ "eventKinds", "name", "url" ] }, "webhookDestinationSecretResponseSchema": { "type": "object", "properties": { "createdAt": { "$ref": "#/components/schemas/dateTimeSchema" }, "createdBy": { "type": [ "string", "null" ], "format": "uuid" }, "disabledAt": { "anyOf": [ { "$ref": "#/components/schemas/dateTimeSchema" }, { "type": "null" } ] }, "disabledReason": { "type": [ "string", "null" ] }, "enabled": { "type": "boolean" }, "eventKinds": { "type": "array", "items": { "type": "string", "minLength": 1, "maxLength": 160 } }, "failingAt": { "anyOf": [ { "$ref": "#/components/schemas/dateTimeSchema" }, { "type": "null" } ] }, "id": { "type": "string", "format": "uuid" }, "name": { "type": "string", "minLength": 1, "maxLength": 160 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "updatedAt": { "$ref": "#/components/schemas/dateTimeSchema" }, "url": { "type": "string", "format": "uri" }, "secret": { "type": "string", "minLength": 16, "maxLength": 200 } }, "required": [ "createdAt", "createdBy", "disabledAt", "disabledReason", "enabled", "eventKinds", "failingAt", "id", "name", "organizationId", "updatedAt", "url", "secret" ] } } ``` # List webhook endpoints. (/docs/openapi/listWebhookEndpoints) Return incoming webhook endpoints without their URL tokens or signing secrets. ## GET /v1/organizations/{organizationId}/webhooks/endpoints ```json { "operationId": "listWebhookEndpoints", "summary": "List webhook endpoints.", "description": "Return incoming webhook endpoints without their URL tokens or signing secrets.", "tags": [ "Webhooks" ], "parameters": [ { "in": "query", "name": "limit", "required": false, "schema": { "type": "string" }, "example": "example" }, { "in": "query", "name": "offset", "required": false, "schema": { "type": "string" }, "example": "example" }, { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "responses": { "200": { "description": "Returns paginated incoming webhook endpoints.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/webhookEndpointsListResponseSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "dateTimeSchema": { "type": "string" }, "listPaginationSchema": { "type": "object", "properties": { "hasMore": { "type": "boolean" }, "limit": { "type": "integer", "minimum": 1, "maximum": 100 }, "offset": { "type": "integer", "minimum": 0 }, "total": { "type": "integer", "minimum": 0 }, "totalIsEstimate": { "type": "boolean" } }, "required": [ "hasMore", "limit", "offset", "total" ] }, "organizationIdSchema": { "type": "string", "format": "uuid" }, "webhookEndpointHttpSchema": { "type": "object", "properties": { "createdAt": { "$ref": "#/components/schemas/dateTimeSchema" }, "createdBy": { "type": [ "string", "null" ], "format": "uuid" }, "enabled": { "type": "boolean" }, "id": { "type": "string", "format": "uuid" }, "lastReceivedAt": { "anyOf": [ { "$ref": "#/components/schemas/dateTimeSchema" }, { "type": "null" } ] }, "lastStatus": { "type": [ "integer", "null" ] }, "maxBodyBytes": { "type": "integer", "exclusiveMinimum": 0 }, "name": { "type": "string", "minLength": 1 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "receiveCount": { "type": "integer", "minimum": 0 }, "signatureHeader": { "type": [ "string", "null" ] }, "verify": { "$ref": "#/components/schemas/webhookEndpointVerifySchema" }, "workflowDefinitionId": { "type": [ "string", "null" ], "format": "uuid" } }, "required": [ "createdAt", "createdBy", "enabled", "id", "lastReceivedAt", "lastStatus", "maxBodyBytes", "name", "organizationId", "receiveCount", "signatureHeader", "verify", "workflowDefinitionId" ] }, "webhookEndpointVerifySchema": { "type": "string", "enum": [ "standard-webhooks", "hmac-sha256", "shared-secret", "none" ] }, "webhookEndpointsListResponseSchema": { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/components/schemas/webhookEndpointHttpSchema" } }, "pagination": { "$ref": "#/components/schemas/listPaginationSchema" } }, "required": [ "data", "pagination" ] } } ``` # Create a webhook endpoint. (/docs/openapi/createWebhookEndpoint) Create an incoming webhook endpoint. Its URL token and signing secret are returned only by this operation or an explicit rotation; the endpoint keeps only a hash of the token. ## POST /v1/organizations/{organizationId}/webhooks/endpoints ```json { "operationId": "createWebhookEndpoint", "summary": "Create a webhook endpoint.", "description": "Create an incoming webhook endpoint. Its URL token and signing secret are returned only by this operation or an explicit rotation; the endpoint keeps only a hash of the token.", "tags": [ "Webhooks" ], "parameters": [ { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/webhookEndpointCreateRequestSchema" } } }, "description": "Incoming webhook endpoint settings.", "required": true }, "responses": { "201": { "description": "Returns the created endpoint with its one-time URL token and signing secret.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/webhookEndpointCreateResponseSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "dateTimeSchema": { "type": "string" }, "organizationIdSchema": { "type": "string", "format": "uuid" }, "webhookEndpointCreateRequestSchema": { "type": "object", "properties": { "name": { "type": "string", "minLength": 1, "maxLength": 160 }, "signatureHeader": { "allOf": [ { "$ref": "#/components/schemas/webhookEndpointSignatureHeaderSchema" } ] }, "verify": { "allOf": [ { "$ref": "#/components/schemas/webhookEndpointVerifySchema" } ] }, "workflowDefinitionId": { "type": [ "string", "null" ], "format": "uuid" } }, "required": [ "name" ] }, "webhookEndpointCreateResponseSchema": { "type": "object", "properties": { "createdAt": { "$ref": "#/components/schemas/dateTimeSchema" }, "createdBy": { "type": [ "string", "null" ], "format": "uuid" }, "enabled": { "type": "boolean" }, "id": { "type": "string", "format": "uuid" }, "lastReceivedAt": { "anyOf": [ { "$ref": "#/components/schemas/dateTimeSchema" }, { "type": "null" } ] }, "lastStatus": { "type": [ "integer", "null" ] }, "maxBodyBytes": { "type": "integer", "exclusiveMinimum": 0 }, "name": { "type": "string", "minLength": 1 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "receiveCount": { "type": "integer", "minimum": 0 }, "signatureHeader": { "type": [ "string", "null" ] }, "verify": { "$ref": "#/components/schemas/webhookEndpointVerifySchema" }, "workflowDefinitionId": { "type": [ "string", "null" ], "format": "uuid" }, "secret": { "type": [ "string", "null" ], "minLength": 16, "maxLength": 200 }, "token": { "$ref": "#/components/schemas/webhookEndpointTokenSchema" } }, "required": [ "createdAt", "createdBy", "enabled", "id", "lastReceivedAt", "lastStatus", "maxBodyBytes", "name", "organizationId", "receiveCount", "signatureHeader", "verify", "workflowDefinitionId", "secret", "token" ] }, "webhookEndpointSignatureHeaderSchema": { "type": "string", "pattern": "^[a-z0-9][a-z0-9-]{0,99}$" }, "webhookEndpointTokenSchema": { "type": "string", "minLength": 16, "maxLength": 80 }, "webhookEndpointVerifySchema": { "type": "string", "enum": [ "standard-webhooks", "hmac-sha256", "shared-secret", "none" ] } } ``` # Get a webhook delivery. (/docs/openapi/getWebhookDelivery) Return one outgoing webhook delivery attempt and its observed response. ## GET /v1/organizations/{organizationId}/webhooks/deliveries/{deliveryId} ```json { "operationId": "getWebhookDelivery", "summary": "Get a webhook delivery.", "description": "Return one outgoing webhook delivery attempt and its observed response.", "tags": [ "Webhooks" ], "parameters": [ { "in": "path", "name": "deliveryId", "required": true, "schema": { "type": "string", "format": "uuid" }, "example": "123e4567-e89b-12d3-a456-426614174000" }, { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "responses": { "200": { "description": "Returns the requested webhook delivery.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/webhookDeliverySchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "dateTimeSchema": { "type": "string" }, "jsonObjectSchema": { "type": "object", "additionalProperties": { "$ref": "#/components/schemas/jsonValueSchema" }, "propertyNames": { "type": "string" } }, "jsonValueSchema": { "anyOf": [ { "type": "string" }, { "type": "number" }, { "type": "boolean" }, { "type": "null" }, { "type": "array", "items": { "$ref": "#/components/schemas/jsonValueSchema" } }, { "type": "object", "additionalProperties": { "$ref": "#/components/schemas/jsonValueSchema" }, "propertyNames": { "type": "string" } } ] }, "organizationIdSchema": { "type": "string", "format": "uuid" }, "webhookDeliverySchema": { "type": "object", "properties": { "attempt": { "type": "integer", "minimum": 0 }, "availableAt": { "$ref": "#/components/schemas/dateTimeSchema" }, "createdAt": { "$ref": "#/components/schemas/dateTimeSchema" }, "destinationId": { "type": "string", "format": "uuid" }, "durationMs": { "type": [ "integer", "null" ] }, "eventId": { "type": [ "string", "null" ], "format": "uuid" }, "eventKind": { "type": "string", "minLength": 1, "maxLength": 160 }, "id": { "type": "string", "format": "uuid" }, "lastError": { "type": [ "string", "null" ] }, "leaseExpiresAt": { "anyOf": [ { "$ref": "#/components/schemas/dateTimeSchema" }, { "type": "null" } ] }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "payload": { "$ref": "#/components/schemas/jsonObjectSchema" }, "processedAt": { "anyOf": [ { "$ref": "#/components/schemas/dateTimeSchema" }, { "type": "null" } ] }, "responseBody": { "type": [ "string", "null" ] }, "responseStatus": { "type": [ "integer", "null" ] }, "status": { "$ref": "#/components/schemas/webhookDeliveryStatusSchema" }, "updatedAt": { "$ref": "#/components/schemas/dateTimeSchema" }, "workflowRunId": { "type": [ "string", "null" ], "format": "uuid" } }, "required": [ "attempt", "availableAt", "createdAt", "destinationId", "durationMs", "eventId", "eventKind", "id", "lastError", "leaseExpiresAt", "organizationId", "payload", "processedAt", "responseBody", "responseStatus", "status", "updatedAt", "workflowRunId" ] }, "webhookDeliveryStatusSchema": { "type": "string", "enum": [ "pending", "processing", "completed", "failed", "dead_lettered", "canceled" ] } } ``` # Get a webhook destination. (/docs/openapi/getWebhookDestination) Return an outgoing webhook destination without its signing secret. ## GET /v1/organizations/{organizationId}/webhooks/destinations/{destinationId} ```json { "operationId": "getWebhookDestination", "summary": "Get a webhook destination.", "description": "Return an outgoing webhook destination without its signing secret.", "tags": [ "Webhooks" ], "parameters": [ { "in": "path", "name": "destinationId", "required": true, "schema": { "type": "string", "format": "uuid" }, "example": "123e4567-e89b-12d3-a456-426614174000" }, { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "responses": { "200": { "description": "Returns the requested webhook destination.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/webhookDestinationSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "dateTimeSchema": { "type": "string" }, "organizationIdSchema": { "type": "string", "format": "uuid" }, "webhookDestinationSchema": { "type": "object", "properties": { "createdAt": { "$ref": "#/components/schemas/dateTimeSchema" }, "createdBy": { "type": [ "string", "null" ], "format": "uuid" }, "disabledAt": { "anyOf": [ { "$ref": "#/components/schemas/dateTimeSchema" }, { "type": "null" } ] }, "disabledReason": { "type": [ "string", "null" ] }, "enabled": { "type": "boolean" }, "eventKinds": { "type": "array", "items": { "type": "string", "minLength": 1, "maxLength": 160 } }, "failingAt": { "anyOf": [ { "$ref": "#/components/schemas/dateTimeSchema" }, { "type": "null" } ] }, "id": { "type": "string", "format": "uuid" }, "name": { "type": "string", "minLength": 1, "maxLength": 160 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "updatedAt": { "$ref": "#/components/schemas/dateTimeSchema" }, "url": { "type": "string", "format": "uri" } }, "required": [ "createdAt", "createdBy", "disabledAt", "disabledReason", "enabled", "eventKinds", "failingAt", "id", "name", "organizationId", "updatedAt", "url" ] } } ``` # Update a webhook destination. (/docs/openapi/updateWebhookDestination) Update the name, URL, event subscriptions, or enabled state of an outgoing webhook destination. ## PATCH /v1/organizations/{organizationId}/webhooks/destinations/{destinationId} ```json { "operationId": "updateWebhookDestination", "summary": "Update a webhook destination.", "description": "Update the name, URL, event subscriptions, or enabled state of an outgoing webhook destination.", "tags": [ "Webhooks" ], "parameters": [ { "in": "path", "name": "destinationId", "required": true, "schema": { "type": "string", "format": "uuid" }, "example": "123e4567-e89b-12d3-a456-426614174000" }, { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/updateWebhookDestinationInputSchema" } } }, "description": "Webhook destination fields to update.", "required": true }, "responses": { "200": { "description": "Returns the updated webhook destination without its signing secret.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/webhookDestinationSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "dateTimeSchema": { "type": "string" }, "organizationIdSchema": { "type": "string", "format": "uuid" }, "updateWebhookDestinationInputSchema": { "type": "object", "properties": { "enabled": { "type": "boolean" }, "eventKinds": { "type": "array", "items": { "type": "string", "minLength": 1, "maxLength": 160 } }, "name": { "type": "string", "minLength": 1, "maxLength": 160 }, "url": { "type": "string", "format": "uri" } } }, "webhookDestinationSchema": { "type": "object", "properties": { "createdAt": { "$ref": "#/components/schemas/dateTimeSchema" }, "createdBy": { "type": [ "string", "null" ], "format": "uuid" }, "disabledAt": { "anyOf": [ { "$ref": "#/components/schemas/dateTimeSchema" }, { "type": "null" } ] }, "disabledReason": { "type": [ "string", "null" ] }, "enabled": { "type": "boolean" }, "eventKinds": { "type": "array", "items": { "type": "string", "minLength": 1, "maxLength": 160 } }, "failingAt": { "anyOf": [ { "$ref": "#/components/schemas/dateTimeSchema" }, { "type": "null" } ] }, "id": { "type": "string", "format": "uuid" }, "name": { "type": "string", "minLength": 1, "maxLength": 160 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "updatedAt": { "$ref": "#/components/schemas/dateTimeSchema" }, "url": { "type": "string", "format": "uri" } }, "required": [ "createdAt", "createdBy", "disabledAt", "disabledReason", "enabled", "eventKinds", "failingAt", "id", "name", "organizationId", "updatedAt", "url" ] } } ``` # Delete a webhook destination. (/docs/openapi/deleteWebhookDestination) Permanently delete an outgoing webhook destination. ## DELETE /v1/organizations/{organizationId}/webhooks/destinations/{destinationId} ```json { "operationId": "deleteWebhookDestination", "summary": "Delete a webhook destination.", "description": "Permanently delete an outgoing webhook destination.", "tags": [ "Webhooks" ], "parameters": [ { "in": "path", "name": "destinationId", "required": true, "schema": { "type": "string", "format": "uuid" }, "example": "123e4567-e89b-12d3-a456-426614174000" }, { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "responses": { "200": { "description": "Confirms the webhook destination was deleted.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/publicApiSuccessResponseSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "organizationIdSchema": { "type": "string", "format": "uuid" }, "publicApiSuccessResponseSchema": { "type": "object", "properties": { "success": { "type": "boolean", "enum": [ true ] } }, "required": [ "success" ] } } ``` # Get a webhook endpoint. (/docs/openapi/getWebhookEndpoint) Return an incoming webhook endpoint without its URL token or signing secret. ## GET /v1/organizations/{organizationId}/webhooks/endpoints/{endpointId} ```json { "operationId": "getWebhookEndpoint", "summary": "Get a webhook endpoint.", "description": "Return an incoming webhook endpoint without its URL token or signing secret.", "tags": [ "Webhooks" ], "parameters": [ { "in": "path", "name": "endpointId", "required": true, "schema": { "type": "string", "format": "uuid" }, "example": "123e4567-e89b-12d3-a456-426614174000" }, { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "responses": { "200": { "description": "Returns the requested webhook endpoint.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/webhookEndpointHttpSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "dateTimeSchema": { "type": "string" }, "organizationIdSchema": { "type": "string", "format": "uuid" }, "webhookEndpointHttpSchema": { "type": "object", "properties": { "createdAt": { "$ref": "#/components/schemas/dateTimeSchema" }, "createdBy": { "type": [ "string", "null" ], "format": "uuid" }, "enabled": { "type": "boolean" }, "id": { "type": "string", "format": "uuid" }, "lastReceivedAt": { "anyOf": [ { "$ref": "#/components/schemas/dateTimeSchema" }, { "type": "null" } ] }, "lastStatus": { "type": [ "integer", "null" ] }, "maxBodyBytes": { "type": "integer", "exclusiveMinimum": 0 }, "name": { "type": "string", "minLength": 1 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "receiveCount": { "type": "integer", "minimum": 0 }, "signatureHeader": { "type": [ "string", "null" ] }, "verify": { "$ref": "#/components/schemas/webhookEndpointVerifySchema" }, "workflowDefinitionId": { "type": [ "string", "null" ], "format": "uuid" } }, "required": [ "createdAt", "createdBy", "enabled", "id", "lastReceivedAt", "lastStatus", "maxBodyBytes", "name", "organizationId", "receiveCount", "signatureHeader", "verify", "workflowDefinitionId" ] }, "webhookEndpointVerifySchema": { "type": "string", "enum": [ "standard-webhooks", "hmac-sha256", "shared-secret", "none" ] } } ``` # Update a webhook endpoint. (/docs/openapi/updateWebhookEndpoint) Rename an incoming webhook endpoint, bind it to another workflow, change how deliveries are verified or the signature header, or enable or disable it. The URL stays the same. A new signing mode returns its secret once. ## PATCH /v1/organizations/{organizationId}/webhooks/endpoints/{endpointId} ```json { "operationId": "updateWebhookEndpoint", "summary": "Update a webhook endpoint.", "description": "Rename an incoming webhook endpoint, bind it to another workflow, change how deliveries are verified or the signature header, or enable or disable it. The URL stays the same. A new signing mode returns its secret once.", "tags": [ "Webhooks" ], "parameters": [ { "in": "path", "name": "endpointId", "required": true, "schema": { "type": "string", "format": "uuid" }, "example": "123e4567-e89b-12d3-a456-426614174000" }, { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/updateWebhookEndpointInputSchema" } } }, "description": "Webhook endpoint fields to update.", "required": true }, "responses": { "200": { "description": "Returns the updated webhook endpoint, with the new signing secret when the verification mode changed.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/webhookEndpointUpdateResponseSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "dateTimeSchema": { "type": "string" }, "organizationIdSchema": { "type": "string", "format": "uuid" }, "updateWebhookEndpointInputSchema": { "type": "object", "properties": { "enabled": { "type": "boolean" }, "name": { "type": "string", "minLength": 1, "maxLength": 160 }, "signatureHeader": { "allOf": [ { "$ref": "#/components/schemas/webhookEndpointSignatureHeaderSchema" } ] }, "verify": { "allOf": [ { "$ref": "#/components/schemas/webhookEndpointVerifySchema" } ] }, "workflowDefinitionId": { "type": [ "string", "null" ], "format": "uuid" } } }, "webhookEndpointSignatureHeaderSchema": { "type": "string", "pattern": "^[a-z0-9][a-z0-9-]{0,99}$" }, "webhookEndpointUpdateResponseSchema": { "type": "object", "properties": { "createdAt": { "$ref": "#/components/schemas/dateTimeSchema" }, "createdBy": { "type": [ "string", "null" ], "format": "uuid" }, "enabled": { "type": "boolean" }, "id": { "type": "string", "format": "uuid" }, "lastReceivedAt": { "anyOf": [ { "$ref": "#/components/schemas/dateTimeSchema" }, { "type": "null" } ] }, "lastStatus": { "type": [ "integer", "null" ] }, "maxBodyBytes": { "type": "integer", "exclusiveMinimum": 0 }, "name": { "type": "string", "minLength": 1 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "receiveCount": { "type": "integer", "minimum": 0 }, "signatureHeader": { "type": [ "string", "null" ] }, "verify": { "$ref": "#/components/schemas/webhookEndpointVerifySchema" }, "workflowDefinitionId": { "type": [ "string", "null" ], "format": "uuid" }, "secret": { "type": [ "string", "null" ], "minLength": 16, "maxLength": 200 } }, "required": [ "createdAt", "createdBy", "enabled", "id", "lastReceivedAt", "lastStatus", "maxBodyBytes", "name", "organizationId", "receiveCount", "signatureHeader", "verify", "workflowDefinitionId", "secret" ] }, "webhookEndpointVerifySchema": { "type": "string", "enum": [ "standard-webhooks", "hmac-sha256", "shared-secret", "none" ] } } ``` # Delete a webhook endpoint. (/docs/openapi/deleteWebhookEndpoint) Permanently delete an incoming webhook endpoint. ## DELETE /v1/organizations/{organizationId}/webhooks/endpoints/{endpointId} ```json { "operationId": "deleteWebhookEndpoint", "summary": "Delete a webhook endpoint.", "description": "Permanently delete an incoming webhook endpoint.", "tags": [ "Webhooks" ], "parameters": [ { "in": "path", "name": "endpointId", "required": true, "schema": { "type": "string", "format": "uuid" }, "example": "123e4567-e89b-12d3-a456-426614174000" }, { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "responses": { "200": { "description": "Confirms the webhook endpoint was deleted.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/publicApiSuccessResponseSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "organizationIdSchema": { "type": "string", "format": "uuid" }, "publicApiSuccessResponseSchema": { "type": "object", "properties": { "success": { "type": "boolean", "enum": [ true ] } }, "required": [ "success" ] } } ``` # Rotate a webhook destination signing secret. (/docs/openapi/rotateWebhookDestinationSecret) Replace the signing secret used for outgoing webhook deliveries. The new secret is returned only once. ## POST /v1/organizations/{organizationId}/webhooks/destinations/{destinationId}/rotate-secret ```json { "operationId": "rotateWebhookDestinationSecret", "summary": "Rotate a webhook destination signing secret.", "description": "Replace the signing secret used for outgoing webhook deliveries. The new secret is returned only once.", "tags": [ "Webhooks" ], "parameters": [ { "in": "path", "name": "destinationId", "required": true, "schema": { "type": "string", "format": "uuid" }, "example": "123e4567-e89b-12d3-a456-426614174000" }, { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "responses": { "200": { "description": "Returns the destination and its new one-time signing secret.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/webhookDestinationSecretResponseSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "dateTimeSchema": { "type": "string" }, "organizationIdSchema": { "type": "string", "format": "uuid" }, "webhookDestinationSecretResponseSchema": { "type": "object", "properties": { "createdAt": { "$ref": "#/components/schemas/dateTimeSchema" }, "createdBy": { "type": [ "string", "null" ], "format": "uuid" }, "disabledAt": { "anyOf": [ { "$ref": "#/components/schemas/dateTimeSchema" }, { "type": "null" } ] }, "disabledReason": { "type": [ "string", "null" ] }, "enabled": { "type": "boolean" }, "eventKinds": { "type": "array", "items": { "type": "string", "minLength": 1, "maxLength": 160 } }, "failingAt": { "anyOf": [ { "$ref": "#/components/schemas/dateTimeSchema" }, { "type": "null" } ] }, "id": { "type": "string", "format": "uuid" }, "name": { "type": "string", "minLength": 1, "maxLength": 160 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "updatedAt": { "$ref": "#/components/schemas/dateTimeSchema" }, "url": { "type": "string", "format": "uri" }, "secret": { "type": "string", "minLength": 16, "maxLength": 200 } }, "required": [ "createdAt", "createdBy", "disabledAt", "disabledReason", "enabled", "eventKinds", "failingAt", "id", "name", "organizationId", "updatedAt", "url", "secret" ] } } ``` # Rotate a webhook endpoint signing secret. (/docs/openapi/rotateWebhookEndpointSecret) Replace the signing secret of an incoming webhook endpoint and keep its URL. During the grace window the previous secret still verifies deliveries. The new secret is returned only once. ## POST /v1/organizations/{organizationId}/webhooks/endpoints/{endpointId}/rotate-secret ```json { "operationId": "rotateWebhookEndpointSecret", "summary": "Rotate a webhook endpoint signing secret.", "description": "Replace the signing secret of an incoming webhook endpoint and keep its URL. During the grace window the previous secret still verifies deliveries. The new secret is returned only once.", "tags": [ "Webhooks" ], "parameters": [ { "in": "path", "name": "endpointId", "required": true, "schema": { "type": "string", "format": "uuid" }, "example": "123e4567-e89b-12d3-a456-426614174000" }, { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/webhookEndpointRotateSecretRequestSchema" } } }, "description": "Hours the previous secret keeps verifying deliveries, 0 to stop it at once." }, "responses": { "200": { "description": "Returns the endpoint with its new one-time signing secret and the end of the grace window.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/webhookEndpointSecretResponseSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "dateTimeSchema": { "type": "string" }, "organizationIdSchema": { "type": "string", "format": "uuid" }, "webhookEndpointRotateSecretRequestSchema": { "type": "object", "properties": { "graceHours": { "type": "integer", "minimum": 0, "maximum": 168, "default": 0 } }, "required": [ "graceHours" ] }, "webhookEndpointSecretResponseSchema": { "type": "object", "properties": { "createdAt": { "$ref": "#/components/schemas/dateTimeSchema" }, "createdBy": { "type": [ "string", "null" ], "format": "uuid" }, "enabled": { "type": "boolean" }, "id": { "type": "string", "format": "uuid" }, "lastReceivedAt": { "anyOf": [ { "$ref": "#/components/schemas/dateTimeSchema" }, { "type": "null" } ] }, "lastStatus": { "type": [ "integer", "null" ] }, "maxBodyBytes": { "type": "integer", "exclusiveMinimum": 0 }, "name": { "type": "string", "minLength": 1 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "receiveCount": { "type": "integer", "minimum": 0 }, "signatureHeader": { "type": [ "string", "null" ] }, "verify": { "$ref": "#/components/schemas/webhookEndpointVerifySchema" }, "workflowDefinitionId": { "type": [ "string", "null" ], "format": "uuid" }, "previousSecretExpiresAt": { "anyOf": [ { "$ref": "#/components/schemas/dateTimeSchema" }, { "type": "null" } ] }, "secret": { "type": "string", "minLength": 16, "maxLength": 200 } }, "required": [ "createdAt", "createdBy", "enabled", "id", "lastReceivedAt", "lastStatus", "maxBodyBytes", "name", "organizationId", "receiveCount", "signatureHeader", "verify", "workflowDefinitionId", "previousSecretExpiresAt", "secret" ] }, "webhookEndpointVerifySchema": { "type": "string", "enum": [ "standard-webhooks", "hmac-sha256", "shared-secret", "none" ] } } ``` # Rotate a webhook endpoint token. (/docs/openapi/rotateWebhookEndpointToken) Replace the public receiver token for an incoming webhook endpoint, invalidating the previous URL. ## POST /v1/organizations/{organizationId}/webhooks/endpoints/{endpointId}/rotate-token ```json { "operationId": "rotateWebhookEndpointToken", "summary": "Rotate a webhook endpoint token.", "description": "Replace the public receiver token for an incoming webhook endpoint, invalidating the previous URL.", "tags": [ "Webhooks" ], "parameters": [ { "in": "path", "name": "endpointId", "required": true, "schema": { "type": "string", "format": "uuid" }, "example": "123e4567-e89b-12d3-a456-426614174000" }, { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "responses": { "200": { "description": "Returns the endpoint with its replacement receiver token.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/webhookEndpointTokenResponseSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "dateTimeSchema": { "type": "string" }, "organizationIdSchema": { "type": "string", "format": "uuid" }, "webhookEndpointTokenResponseSchema": { "type": "object", "properties": { "createdAt": { "$ref": "#/components/schemas/dateTimeSchema" }, "createdBy": { "type": [ "string", "null" ], "format": "uuid" }, "enabled": { "type": "boolean" }, "id": { "type": "string", "format": "uuid" }, "lastReceivedAt": { "anyOf": [ { "$ref": "#/components/schemas/dateTimeSchema" }, { "type": "null" } ] }, "lastStatus": { "type": [ "integer", "null" ] }, "maxBodyBytes": { "type": "integer", "exclusiveMinimum": 0 }, "name": { "type": "string", "minLength": 1 }, "organizationId": { "$ref": "#/components/schemas/organizationIdSchema" }, "receiveCount": { "type": "integer", "minimum": 0 }, "signatureHeader": { "type": [ "string", "null" ] }, "verify": { "$ref": "#/components/schemas/webhookEndpointVerifySchema" }, "workflowDefinitionId": { "type": [ "string", "null" ], "format": "uuid" }, "token": { "$ref": "#/components/schemas/webhookEndpointTokenSchema" } }, "required": [ "createdAt", "createdBy", "enabled", "id", "lastReceivedAt", "lastStatus", "maxBodyBytes", "name", "organizationId", "receiveCount", "signatureHeader", "verify", "workflowDefinitionId", "token" ] }, "webhookEndpointTokenSchema": { "type": "string", "minLength": 16, "maxLength": 80 }, "webhookEndpointVerifySchema": { "type": "string", "enum": [ "standard-webhooks", "hmac-sha256", "shared-secret", "none" ] } } ``` # List workflow definitions. (/docs/openapi/listWorkflowDefinitions) Return paginated summaries of published workflow definitions available to the authenticated organization user. Draft graphs and editor metadata are never returned. ## GET /v1/organizations/{organizationId}/workflows/definitions ```json { "operationId": "listWorkflowDefinitions", "summary": "List workflow definitions.", "description": "Return paginated summaries of published workflow definitions available to the authenticated organization user. Draft graphs and editor metadata are never returned.", "tags": [ "Workflows" ], "parameters": [ { "in": "query", "name": "limit", "required": false, "schema": { "type": "string" }, "example": "example" }, { "in": "query", "name": "offset", "required": false, "schema": { "type": "string" }, "example": "example" }, { "in": "query", "name": "q", "required": false, "schema": { "type": "string" }, "example": "example" }, { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "responses": { "200": { "description": "Returns paginated published workflow definition summaries.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/workflowDefinitionSummariesListResponseSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "dateTimeSchema": { "type": "string" }, "listPaginationSchema": { "type": "object", "properties": { "hasMore": { "type": "boolean" }, "limit": { "type": "integer", "minimum": 1, "maximum": 100 }, "offset": { "type": "integer", "minimum": 0 }, "total": { "type": "integer", "minimum": 0 }, "totalIsEstimate": { "type": "boolean" } }, "required": [ "hasMore", "limit", "offset", "total" ] }, "organizationIdSchema": { "type": "string", "format": "uuid" }, "workflowDefinitionHttpSummarySchema": { "$ref": "#/components/schemas/workflowDefinitionSummarySchema" }, "workflowDefinitionStatusSchema": { "type": "string", "enum": [ "draft", "published", "archived" ] }, "workflowDefinitionSummariesListResponseSchema": { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/components/schemas/workflowDefinitionHttpSummarySchema" } }, "pagination": { "$ref": "#/components/schemas/listPaginationSchema" } }, "required": [ "data", "pagination" ] }, "workflowDefinitionSummarySchema": { "type": "object", "properties": { "description": { "type": [ "string", "null" ], "maxLength": 2000 }, "forkedFromDefinitionId": { "type": [ "string", "null" ], "format": "uuid", "default": null }, "forkedFromVersion": { "type": [ "integer", "null" ], "exclusiveMinimum": 0, "default": null }, "id": { "type": "string", "format": "uuid" }, "isDefault": { "type": "boolean", "default": false }, "key": { "type": "string", "minLength": 1, "maxLength": 160 }, "name": { "type": "string", "minLength": 1, "maxLength": 160 }, "ownerScope": { "$ref": "#/components/schemas/workflowOwnerScopeSchema" }, "purpose": { "type": "string", "enum": [ "automation", "lifecycle" ], "default": "automation" }, "status": { "$ref": "#/components/schemas/workflowDefinitionStatusSchema" }, "subjectType": { "anyOf": [ { "$ref": "#/components/schemas/workflowSubjectTypeSchema" }, { "type": "null" } ], "default": null }, "updatedAt": { "$ref": "#/components/schemas/dateTimeSchema" }, "version": { "type": "integer", "exclusiveMinimum": 0 }, "visibility": { "type": "string", "enum": [ "automatic", "restricted" ], "default": "automatic" } }, "required": [ "description", "forkedFromDefinitionId", "forkedFromVersion", "id", "isDefault", "key", "name", "ownerScope", "purpose", "status", "subjectType", "updatedAt", "version", "visibility" ] }, "workflowOwnerScopeSchema": { "type": "string", "enum": [ "system", "organization" ] }, "workflowSubjectTypeSchema": { "type": "string", "enum": [ "invoice", "order", "purchase_order", "quote", "shipment", "task" ] } } ``` # List workflow runs. (/docs/openapi/listWorkflowRuns) Return paginated organization workflow runs, optionally filtered by definition version, workflow key (every version), status, or trigger kind. Graph snapshots are never returned. ## GET /v1/organizations/{organizationId}/workflows/runs ```json { "operationId": "listWorkflowRuns", "summary": "List workflow runs.", "description": "Return paginated organization workflow runs, optionally filtered by definition version, workflow key (every version), status, or trigger kind. Graph snapshots are never returned.", "tags": [ "Workflows" ], "parameters": [ { "in": "query", "name": "definitionId", "required": false, "schema": { "type": "string" }, "example": "123" }, { "in": "query", "name": "definitionKey", "required": false, "schema": { "type": "string" }, "example": "example" }, { "in": "query", "name": "limit", "required": false, "schema": { "type": "string" }, "example": "example" }, { "in": "query", "name": "offset", "required": false, "schema": { "type": "string" }, "example": "example" }, { "in": "query", "name": "status", "required": false, "schema": { "type": "string" }, "example": "example" }, { "in": "query", "name": "triggerKind", "required": false, "schema": { "type": "string" }, "example": "example" }, { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "responses": { "200": { "description": "Returns paginated workflow run summaries.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/workflowRunsListResponseSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "dateTimeSchema": { "type": "string" }, "listPaginationSchema": { "type": "object", "properties": { "hasMore": { "type": "boolean" }, "limit": { "type": "integer", "minimum": 1, "maximum": 100 }, "offset": { "type": "integer", "minimum": 0 }, "total": { "type": "integer", "minimum": 0 }, "totalIsEstimate": { "type": "boolean" } }, "required": [ "hasMore", "limit", "offset", "total" ] }, "organizationIdSchema": { "type": "string", "format": "uuid" }, "workflowRunHttpSummarySchema": { "type": "object", "properties": { "completedAt": { "anyOf": [ { "$ref": "#/components/schemas/dateTimeSchema" }, { "type": "null" } ] }, "createdAt": { "$ref": "#/components/schemas/dateTimeSchema" }, "createdBy": { "type": [ "string", "null" ], "format": "uuid" }, "definitionId": { "type": "string", "format": "uuid" }, "definitionVersion": { "type": "integer", "exclusiveMinimum": 0 }, "id": { "type": "string", "format": "uuid" }, "idempotencyKey": { "type": [ "string", "null" ], "minLength": 1, "maxLength": 240 }, "startedAt": { "anyOf": [ { "$ref": "#/components/schemas/dateTimeSchema" }, { "type": "null" } ] }, "status": { "$ref": "#/components/schemas/workflowRunStatusSchema" }, "subjectId": { "type": [ "string", "null" ], "format": "uuid", "default": null }, "subjectType": { "anyOf": [ { "$ref": "#/components/schemas/workflowSubjectTypeSchema" }, { "type": "null" } ], "default": null }, "triggerEventId": { "type": [ "string", "null" ], "format": "uuid" }, "triggerKind": { "$ref": "#/components/schemas/workflowTriggerKindSchema" }, "updatedAt": { "$ref": "#/components/schemas/dateTimeSchema" }, "workflowRunId": { "type": [ "string", "null" ], "minLength": 1 } }, "required": [ "completedAt", "createdAt", "createdBy", "definitionId", "definitionVersion", "id", "idempotencyKey", "startedAt", "status", "subjectId", "subjectType", "triggerEventId", "triggerKind", "updatedAt", "workflowRunId" ] }, "workflowRunStatusSchema": { "type": "string", "enum": [ "queued", "running", "waiting", "completed", "failed", "canceled" ] }, "workflowRunsListResponseSchema": { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/components/schemas/workflowRunHttpSummarySchema" } }, "pagination": { "$ref": "#/components/schemas/listPaginationSchema" } }, "required": [ "data", "pagination" ] }, "workflowSubjectTypeSchema": { "type": "string", "enum": [ "invoice", "order", "purchase_order", "quote", "shipment", "task" ] }, "workflowTriggerKindSchema": { "type": "string", "enum": [ "domain-event", "schedule", "manual", "api", "mcp", "channel-message", "workflow-failed", "webhook" ] } } ``` # Get a workflow definition. (/docs/openapi/getWorkflowDefinition) Return a summary of one published workflow definition available to the authenticated organization user. The visual graph and draft content are never returned. ## GET /v1/organizations/{organizationId}/workflows/definitions/{definitionId} ```json { "operationId": "getWorkflowDefinition", "summary": "Get a workflow definition.", "description": "Return a summary of one published workflow definition available to the authenticated organization user. The visual graph and draft content are never returned.", "tags": [ "Workflows" ], "parameters": [ { "in": "path", "name": "definitionId", "required": true, "schema": { "type": "string", "format": "uuid" }, "example": "123e4567-e89b-12d3-a456-426614174000" }, { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "responses": { "200": { "description": "Returns the published workflow definition summary.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/workflowDefinitionHttpSummarySchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "dateTimeSchema": { "type": "string" }, "organizationIdSchema": { "type": "string", "format": "uuid" }, "workflowDefinitionHttpSummarySchema": { "$ref": "#/components/schemas/workflowDefinitionSummarySchema" }, "workflowDefinitionStatusSchema": { "type": "string", "enum": [ "draft", "published", "archived" ] }, "workflowDefinitionSummarySchema": { "type": "object", "properties": { "description": { "type": [ "string", "null" ], "maxLength": 2000 }, "forkedFromDefinitionId": { "type": [ "string", "null" ], "format": "uuid", "default": null }, "forkedFromVersion": { "type": [ "integer", "null" ], "exclusiveMinimum": 0, "default": null }, "id": { "type": "string", "format": "uuid" }, "isDefault": { "type": "boolean", "default": false }, "key": { "type": "string", "minLength": 1, "maxLength": 160 }, "name": { "type": "string", "minLength": 1, "maxLength": 160 }, "ownerScope": { "$ref": "#/components/schemas/workflowOwnerScopeSchema" }, "purpose": { "type": "string", "enum": [ "automation", "lifecycle" ], "default": "automation" }, "status": { "$ref": "#/components/schemas/workflowDefinitionStatusSchema" }, "subjectType": { "anyOf": [ { "$ref": "#/components/schemas/workflowSubjectTypeSchema" }, { "type": "null" } ], "default": null }, "updatedAt": { "$ref": "#/components/schemas/dateTimeSchema" }, "version": { "type": "integer", "exclusiveMinimum": 0 }, "visibility": { "type": "string", "enum": [ "automatic", "restricted" ], "default": "automatic" } }, "required": [ "description", "forkedFromDefinitionId", "forkedFromVersion", "id", "isDefault", "key", "name", "ownerScope", "purpose", "status", "subjectType", "updatedAt", "version", "visibility" ] }, "workflowOwnerScopeSchema": { "type": "string", "enum": [ "system", "organization" ] }, "workflowSubjectTypeSchema": { "type": "string", "enum": [ "invoice", "order", "purchase_order", "quote", "shipment", "task" ] } } ``` # Get a workflow run. (/docs/openapi/getWorkflowRun) Return one organization workflow run with execution steps and observed input, output, and errors. The graph snapshot is never returned. ## GET /v1/organizations/{organizationId}/workflows/runs/{runId} ```json { "operationId": "getWorkflowRun", "summary": "Get a workflow run.", "description": "Return one organization workflow run with execution steps and observed input, output, and errors. The graph snapshot is never returned.", "tags": [ "Workflows" ], "parameters": [ { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" }, { "in": "path", "name": "runId", "required": true, "schema": { "type": "string", "format": "uuid" }, "example": "123e4567-e89b-12d3-a456-426614174000" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "responses": { "200": { "description": "Returns the workflow run detail and execution steps.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/workflowRunHttpDetailSchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "dateTimeSchema": { "type": "string" }, "jsonValueSchema": { "anyOf": [ { "type": "string" }, { "type": "number" }, { "type": "boolean" }, { "type": "null" }, { "type": "array", "items": { "$ref": "#/components/schemas/jsonValueSchema" } }, { "type": "object", "additionalProperties": { "$ref": "#/components/schemas/jsonValueSchema" }, "propertyNames": { "type": "string" } } ] }, "metadataSchema": { "type": "object", "additionalProperties": { "$ref": "#/components/schemas/jsonValueSchema" }, "propertyNames": { "type": "string" } }, "organizationIdSchema": { "type": "string", "format": "uuid" }, "workflowHttpJsonObjectSchema": { "type": "object", "additionalProperties": {}, "propertyNames": { "type": "string" } }, "workflowRunHttpDetailSchema": { "type": "object", "properties": { "completedAt": { "anyOf": [ { "$ref": "#/components/schemas/dateTimeSchema" }, { "type": "null" } ] }, "createdAt": { "$ref": "#/components/schemas/dateTimeSchema" }, "createdBy": { "type": [ "string", "null" ], "format": "uuid" }, "definitionId": { "type": "string", "format": "uuid" }, "definitionVersion": { "type": "integer", "exclusiveMinimum": 0 }, "id": { "type": "string", "format": "uuid" }, "idempotencyKey": { "type": [ "string", "null" ], "minLength": 1, "maxLength": 240 }, "startedAt": { "anyOf": [ { "$ref": "#/components/schemas/dateTimeSchema" }, { "type": "null" } ] }, "status": { "$ref": "#/components/schemas/workflowRunStatusSchema" }, "subjectId": { "type": [ "string", "null" ], "format": "uuid", "default": null }, "subjectType": { "anyOf": [ { "$ref": "#/components/schemas/workflowSubjectTypeSchema" }, { "type": "null" } ], "default": null }, "triggerEventId": { "type": [ "string", "null" ], "format": "uuid" }, "triggerKind": { "$ref": "#/components/schemas/workflowTriggerKindSchema" }, "updatedAt": { "$ref": "#/components/schemas/dateTimeSchema" }, "workflowRunId": { "type": [ "string", "null" ], "minLength": 1 }, "error": { "anyOf": [ { "$ref": "#/components/schemas/workflowHttpJsonObjectSchema" }, { "type": "null" } ] }, "input": { "$ref": "#/components/schemas/workflowHttpJsonObjectSchema" }, "output": { "anyOf": [ { "$ref": "#/components/schemas/workflowHttpJsonObjectSchema" }, { "type": "null" } ] }, "steps": { "type": "array", "items": { "$ref": "#/components/schemas/workflowRunStepHttpSchema" } } }, "required": [ "completedAt", "createdAt", "createdBy", "definitionId", "definitionVersion", "id", "idempotencyKey", "startedAt", "status", "subjectId", "subjectType", "triggerEventId", "triggerKind", "updatedAt", "workflowRunId", "error", "input", "output", "steps" ] }, "workflowRunStatusSchema": { "type": "string", "enum": [ "queued", "running", "waiting", "completed", "failed", "canceled" ] }, "workflowRunStepHttpSchema": { "type": "object", "properties": { "attempt": { "type": "integer", "exclusiveMinimum": 0 }, "completedAt": { "anyOf": [ { "$ref": "#/components/schemas/dateTimeSchema" }, { "type": "null" } ] }, "createdAt": { "$ref": "#/components/schemas/dateTimeSchema" }, "error": { "anyOf": [ { "$ref": "#/components/schemas/metadataSchema" }, { "type": "null" } ] }, "id": { "type": "string", "format": "uuid" }, "input": { "anyOf": [ { "$ref": "#/components/schemas/metadataSchema" }, { "type": "null" } ] }, "nodeId": { "type": "string", "minLength": 1, "maxLength": 120 }, "nodeKind": { "type": "string", "minLength": 1, "maxLength": 80 }, "output": { "anyOf": [ { "$ref": "#/components/schemas/metadataSchema" }, { "type": "null" } ] }, "startedAt": { "anyOf": [ { "$ref": "#/components/schemas/dateTimeSchema" }, { "type": "null" } ] }, "status": { "$ref": "#/components/schemas/workflowRunStepStatusSchema" }, "updatedAt": { "$ref": "#/components/schemas/dateTimeSchema" } }, "required": [ "attempt", "completedAt", "createdAt", "error", "id", "input", "nodeId", "nodeKind", "output", "startedAt", "status", "updatedAt" ] }, "workflowRunStepStatusSchema": { "type": "string", "enum": [ "pending", "running", "waiting", "succeeded", "failed", "skipped", "canceled" ] }, "workflowSubjectTypeSchema": { "type": "string", "enum": [ "invoice", "order", "purchase_order", "quote", "shipment", "task" ] }, "workflowTriggerKindSchema": { "type": "string", "enum": [ "domain-event", "schedule", "manual", "api", "mcp", "channel-message", "workflow-failed", "webhook" ] } } ``` # Publish a workflow. (/docs/openapi/publishWorkflowDefinition) Publish an organization workflow draft as the next live version. Earlier published versions of the same key are archived and their schedules removed; access grants and watchers carry over. Invalid graphs are rejected. ## POST /v1/organizations/{organizationId}/workflows/definitions/{definitionId}/publish ```json { "operationId": "publishWorkflowDefinition", "summary": "Publish a workflow.", "description": "Publish an organization workflow draft as the next live version. Earlier published versions of the same key are archived and their schedules removed; access grants and watchers carry over. Invalid graphs are rejected.", "tags": [ "Workflows" ], "parameters": [ { "in": "path", "name": "definitionId", "required": true, "schema": { "type": "string", "format": "uuid" }, "example": "123e4567-e89b-12d3-a456-426614174000" }, { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "responses": { "200": { "description": "Returns the newly published workflow version.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/workflowDefinitionHttpSummarySchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "dateTimeSchema": { "type": "string" }, "organizationIdSchema": { "type": "string", "format": "uuid" }, "workflowDefinitionHttpSummarySchema": { "$ref": "#/components/schemas/workflowDefinitionSummarySchema" }, "workflowDefinitionStatusSchema": { "type": "string", "enum": [ "draft", "published", "archived" ] }, "workflowDefinitionSummarySchema": { "type": "object", "properties": { "description": { "type": [ "string", "null" ], "maxLength": 2000 }, "forkedFromDefinitionId": { "type": [ "string", "null" ], "format": "uuid", "default": null }, "forkedFromVersion": { "type": [ "integer", "null" ], "exclusiveMinimum": 0, "default": null }, "id": { "type": "string", "format": "uuid" }, "isDefault": { "type": "boolean", "default": false }, "key": { "type": "string", "minLength": 1, "maxLength": 160 }, "name": { "type": "string", "minLength": 1, "maxLength": 160 }, "ownerScope": { "$ref": "#/components/schemas/workflowOwnerScopeSchema" }, "purpose": { "type": "string", "enum": [ "automation", "lifecycle" ], "default": "automation" }, "status": { "$ref": "#/components/schemas/workflowDefinitionStatusSchema" }, "subjectType": { "anyOf": [ { "$ref": "#/components/schemas/workflowSubjectTypeSchema" }, { "type": "null" } ], "default": null }, "updatedAt": { "$ref": "#/components/schemas/dateTimeSchema" }, "version": { "type": "integer", "exclusiveMinimum": 0 }, "visibility": { "type": "string", "enum": [ "automatic", "restricted" ], "default": "automatic" } }, "required": [ "description", "forkedFromDefinitionId", "forkedFromVersion", "id", "isDefault", "key", "name", "ownerScope", "purpose", "status", "subjectType", "updatedAt", "version", "visibility" ] }, "workflowOwnerScopeSchema": { "type": "string", "enum": [ "system", "organization" ] }, "workflowSubjectTypeSchema": { "type": "string", "enum": [ "invoice", "order", "purchase_order", "quote", "shipment", "task" ] } } ``` # Start a workflow run. (/docs/openapi/startWorkflowRun) Start a published organization workflow that declares a valid manual trigger. Reusing an idempotency key returns the original organization run. ## POST /v1/organizations/{organizationId}/workflows/definitions/{definitionId}/runs ```json { "operationId": "startWorkflowRun", "summary": "Start a workflow run.", "description": "Start a published organization workflow that declares a valid manual trigger. Reusing an idempotency key returns the original organization run.", "tags": [ "Workflows" ], "parameters": [ { "in": "path", "name": "definitionId", "required": true, "schema": { "type": "string", "format": "uuid" }, "example": "123e4567-e89b-12d3-a456-426614174000" }, { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/workflowRunStartHttpRequestSchema" } } }, "description": "Manual workflow input and an optional organization-scoped idempotency key.", "required": true }, "responses": { "202": { "description": "Returns the accepted workflow run without its graph snapshot.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/workflowRunHttpSummarySchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "dateTimeSchema": { "type": "string" }, "organizationIdSchema": { "type": "string", "format": "uuid" }, "workflowRunHttpSummarySchema": { "type": "object", "properties": { "completedAt": { "anyOf": [ { "$ref": "#/components/schemas/dateTimeSchema" }, { "type": "null" } ] }, "createdAt": { "$ref": "#/components/schemas/dateTimeSchema" }, "createdBy": { "type": [ "string", "null" ], "format": "uuid" }, "definitionId": { "type": "string", "format": "uuid" }, "definitionVersion": { "type": "integer", "exclusiveMinimum": 0 }, "id": { "type": "string", "format": "uuid" }, "idempotencyKey": { "type": [ "string", "null" ], "minLength": 1, "maxLength": 240 }, "startedAt": { "anyOf": [ { "$ref": "#/components/schemas/dateTimeSchema" }, { "type": "null" } ] }, "status": { "$ref": "#/components/schemas/workflowRunStatusSchema" }, "subjectId": { "type": [ "string", "null" ], "format": "uuid", "default": null }, "subjectType": { "anyOf": [ { "$ref": "#/components/schemas/workflowSubjectTypeSchema" }, { "type": "null" } ], "default": null }, "triggerEventId": { "type": [ "string", "null" ], "format": "uuid" }, "triggerKind": { "$ref": "#/components/schemas/workflowTriggerKindSchema" }, "updatedAt": { "$ref": "#/components/schemas/dateTimeSchema" }, "workflowRunId": { "type": [ "string", "null" ], "minLength": 1 } }, "required": [ "completedAt", "createdAt", "createdBy", "definitionId", "definitionVersion", "id", "idempotencyKey", "startedAt", "status", "subjectId", "subjectType", "triggerEventId", "triggerKind", "updatedAt", "workflowRunId" ] }, "workflowRunStartHttpRequestSchema": { "type": "object", "properties": { "idempotencyKey": { "type": [ "string", "null" ], "minLength": 1, "maxLength": 240 }, "payload": { "type": "object", "additionalProperties": {}, "propertyNames": { "type": "string" }, "default": {} } }, "required": [ "payload" ] }, "workflowRunStatusSchema": { "type": "string", "enum": [ "queued", "running", "waiting", "completed", "failed", "canceled" ] }, "workflowSubjectTypeSchema": { "type": "string", "enum": [ "invoice", "order", "purchase_order", "quote", "shipment", "task" ] }, "workflowTriggerKindSchema": { "type": "string", "enum": [ "domain-event", "schedule", "manual", "api", "mcp", "channel-message", "workflow-failed", "webhook" ] } } ``` # Decide a workflow approval. (/docs/openapi/decideWorkflowRunApproval) Approve or deny the approval step a workflow run is waiting on. Who may decide follows the approval request: its named approvers, holders of its approver permission, or their delegates. Requesters cannot approve their own authorize requests. With several approvers required, the run resumes once enough have approved. ## POST /v1/organizations/{organizationId}/workflows/runs/{runId}/approval ```json { "operationId": "decideWorkflowRunApproval", "summary": "Decide a workflow approval.", "description": "Approve or deny the approval step a workflow run is waiting on. Who may decide follows the approval request: its named approvers, holders of its approver permission, or their delegates. Requesters cannot approve their own authorize requests. With several approvers required, the run resumes once enough have approved.", "tags": [ "Workflows" ], "parameters": [ { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" }, { "in": "path", "name": "runId", "required": true, "schema": { "type": "string", "format": "uuid" }, "example": "123e4567-e89b-12d3-a456-426614174000" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/workflowRunApprovalHttpRequestSchema" } } }, "description": "The decision and an optional comment shown on the run.", "required": true }, "responses": { "202": { "description": "Returns the run; it resumes asynchronously after the decision.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/workflowRunHttpSummarySchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "dateTimeSchema": { "type": "string" }, "organizationIdSchema": { "type": "string", "format": "uuid" }, "workflowRunApprovalHttpRequestSchema": { "type": "object", "properties": { "approved": { "type": "boolean" }, "comment": { "type": [ "string", "null" ], "maxLength": 2000 } }, "required": [ "approved" ] }, "workflowRunHttpSummarySchema": { "type": "object", "properties": { "completedAt": { "anyOf": [ { "$ref": "#/components/schemas/dateTimeSchema" }, { "type": "null" } ] }, "createdAt": { "$ref": "#/components/schemas/dateTimeSchema" }, "createdBy": { "type": [ "string", "null" ], "format": "uuid" }, "definitionId": { "type": "string", "format": "uuid" }, "definitionVersion": { "type": "integer", "exclusiveMinimum": 0 }, "id": { "type": "string", "format": "uuid" }, "idempotencyKey": { "type": [ "string", "null" ], "minLength": 1, "maxLength": 240 }, "startedAt": { "anyOf": [ { "$ref": "#/components/schemas/dateTimeSchema" }, { "type": "null" } ] }, "status": { "$ref": "#/components/schemas/workflowRunStatusSchema" }, "subjectId": { "type": [ "string", "null" ], "format": "uuid", "default": null }, "subjectType": { "anyOf": [ { "$ref": "#/components/schemas/workflowSubjectTypeSchema" }, { "type": "null" } ], "default": null }, "triggerEventId": { "type": [ "string", "null" ], "format": "uuid" }, "triggerKind": { "$ref": "#/components/schemas/workflowTriggerKindSchema" }, "updatedAt": { "$ref": "#/components/schemas/dateTimeSchema" }, "workflowRunId": { "type": [ "string", "null" ], "minLength": 1 } }, "required": [ "completedAt", "createdAt", "createdBy", "definitionId", "definitionVersion", "id", "idempotencyKey", "startedAt", "status", "subjectId", "subjectType", "triggerEventId", "triggerKind", "updatedAt", "workflowRunId" ] }, "workflowRunStatusSchema": { "type": "string", "enum": [ "queued", "running", "waiting", "completed", "failed", "canceled" ] }, "workflowSubjectTypeSchema": { "type": "string", "enum": [ "invoice", "order", "purchase_order", "quote", "shipment", "task" ] }, "workflowTriggerKindSchema": { "type": "string", "enum": [ "domain-event", "schedule", "manual", "api", "mcp", "channel-message", "workflow-failed", "webhook" ] } } ``` # Cancel a workflow run. (/docs/openapi/cancelWorkflowRun) Cancel a queued, running, or waiting organization workflow run. Completed, failed, and cancelled runs are left unchanged. ## POST /v1/organizations/{organizationId}/workflows/runs/{runId}/cancel ```json { "operationId": "cancelWorkflowRun", "summary": "Cancel a workflow run.", "description": "Cancel a queued, running, or waiting organization workflow run. Completed, failed, and cancelled runs are left unchanged.", "tags": [ "Workflows" ], "parameters": [ { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" }, { "in": "path", "name": "runId", "required": true, "schema": { "type": "string", "format": "uuid" }, "example": "123e4567-e89b-12d3-a456-426614174000" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/workflowRunCancelHttpRequestSchema" } } }, "description": "Optional cancellation reason recorded on the durable run." }, "responses": { "200": { "description": "Returns the run after cancellation.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/workflowRunHttpSummarySchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "dateTimeSchema": { "type": "string" }, "organizationIdSchema": { "type": "string", "format": "uuid" }, "workflowRunCancelHttpRequestSchema": { "type": "object", "properties": { "reason": { "type": "string", "minLength": 1, "maxLength": 500 } } }, "workflowRunHttpSummarySchema": { "type": "object", "properties": { "completedAt": { "anyOf": [ { "$ref": "#/components/schemas/dateTimeSchema" }, { "type": "null" } ] }, "createdAt": { "$ref": "#/components/schemas/dateTimeSchema" }, "createdBy": { "type": [ "string", "null" ], "format": "uuid" }, "definitionId": { "type": "string", "format": "uuid" }, "definitionVersion": { "type": "integer", "exclusiveMinimum": 0 }, "id": { "type": "string", "format": "uuid" }, "idempotencyKey": { "type": [ "string", "null" ], "minLength": 1, "maxLength": 240 }, "startedAt": { "anyOf": [ { "$ref": "#/components/schemas/dateTimeSchema" }, { "type": "null" } ] }, "status": { "$ref": "#/components/schemas/workflowRunStatusSchema" }, "subjectId": { "type": [ "string", "null" ], "format": "uuid", "default": null }, "subjectType": { "anyOf": [ { "$ref": "#/components/schemas/workflowSubjectTypeSchema" }, { "type": "null" } ], "default": null }, "triggerEventId": { "type": [ "string", "null" ], "format": "uuid" }, "triggerKind": { "$ref": "#/components/schemas/workflowTriggerKindSchema" }, "updatedAt": { "$ref": "#/components/schemas/dateTimeSchema" }, "workflowRunId": { "type": [ "string", "null" ], "minLength": 1 } }, "required": [ "completedAt", "createdAt", "createdBy", "definitionId", "definitionVersion", "id", "idempotencyKey", "startedAt", "status", "subjectId", "subjectType", "triggerEventId", "triggerKind", "updatedAt", "workflowRunId" ] }, "workflowRunStatusSchema": { "type": "string", "enum": [ "queued", "running", "waiting", "completed", "failed", "canceled" ] }, "workflowSubjectTypeSchema": { "type": "string", "enum": [ "invoice", "order", "purchase_order", "quote", "shipment", "task" ] }, "workflowTriggerKindSchema": { "type": "string", "enum": [ "domain-event", "schedule", "manual", "api", "mcp", "channel-message", "workflow-failed", "webhook" ] } } ``` # Restart a workflow run. (/docs/openapi/restartWorkflowRun) Start a failed or cancelled organization workflow run again from the record's current state. Steps with side effects that already succeeded, such as sent emails and recorded stages, are not repeated. ## POST /v1/organizations/{organizationId}/workflows/runs/{runId}/restart ```json { "operationId": "restartWorkflowRun", "summary": "Restart a workflow run.", "description": "Start a failed or cancelled organization workflow run again from the record's current state. Steps with side effects that already succeeded, such as sent emails and recorded stages, are not repeated.", "tags": [ "Workflows" ], "parameters": [ { "in": "path", "name": "organizationId", "required": true, "schema": { "$ref": "#/components/schemas/organizationIdSchema" }, "example": "123" }, { "in": "path", "name": "runId", "required": true, "schema": { "type": "string", "format": "uuid" }, "example": "123e4567-e89b-12d3-a456-426614174000" } ], "security": [ { "SessionCookie": [] }, { "BearerAuth": [] } ], "responses": { "201": { "description": "Returns the new run.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/workflowRunHttpSummarySchema" } } } }, "400": { "$ref": "#/components/responses/400" }, "401": { "$ref": "#/components/responses/401" }, "403": { "$ref": "#/components/responses/403" }, "404": { "$ref": "#/components/responses/404" }, "409": { "$ref": "#/components/responses/409" }, "422": { "$ref": "#/components/responses/422" }, "500": { "$ref": "#/components/responses/500" } } } ``` ## Referenced schemas ```json { "dateTimeSchema": { "type": "string" }, "organizationIdSchema": { "type": "string", "format": "uuid" }, "workflowRunHttpSummarySchema": { "type": "object", "properties": { "completedAt": { "anyOf": [ { "$ref": "#/components/schemas/dateTimeSchema" }, { "type": "null" } ] }, "createdAt": { "$ref": "#/components/schemas/dateTimeSchema" }, "createdBy": { "type": [ "string", "null" ], "format": "uuid" }, "definitionId": { "type": "string", "format": "uuid" }, "definitionVersion": { "type": "integer", "exclusiveMinimum": 0 }, "id": { "type": "string", "format": "uuid" }, "idempotencyKey": { "type": [ "string", "null" ], "minLength": 1, "maxLength": 240 }, "startedAt": { "anyOf": [ { "$ref": "#/components/schemas/dateTimeSchema" }, { "type": "null" } ] }, "status": { "$ref": "#/components/schemas/workflowRunStatusSchema" }, "subjectId": { "type": [ "string", "null" ], "format": "uuid", "default": null }, "subjectType": { "anyOf": [ { "$ref": "#/components/schemas/workflowSubjectTypeSchema" }, { "type": "null" } ], "default": null }, "triggerEventId": { "type": [ "string", "null" ], "format": "uuid" }, "triggerKind": { "$ref": "#/components/schemas/workflowTriggerKindSchema" }, "updatedAt": { "$ref": "#/components/schemas/dateTimeSchema" }, "workflowRunId": { "type": [ "string", "null" ], "minLength": 1 } }, "required": [ "completedAt", "createdAt", "createdBy", "definitionId", "definitionVersion", "id", "idempotencyKey", "startedAt", "status", "subjectId", "subjectType", "triggerEventId", "triggerKind", "updatedAt", "workflowRunId" ] }, "workflowRunStatusSchema": { "type": "string", "enum": [ "queued", "running", "waiting", "completed", "failed", "canceled" ] }, "workflowSubjectTypeSchema": { "type": "string", "enum": [ "invoice", "order", "purchase_order", "quote", "shipment", "task" ] }, "workflowTriggerKindSchema": { "type": "string", "enum": [ "domain-event", "schedule", "manual", "api", "mcp", "channel-message", "workflow-failed", "webhook" ] } } ```