{
  "openapi": "3.1.0",
  "info": {
    "title": "PaloWorks API",
    "version": "1.0.0",
    "description": "A REST API over one PaloWorks workspace: clients, projects, scopes, revisions, time, and invoices. Authenticate with a workspace API key (`Authorization: Bearer crk_…`) created in Settings → API keys; keys are workspace credentials with scopes, not user logins.\nRules that hold on every endpoint:\n- Drafts only, humans send. Every write returns the record and an in-app `url` a person opens next. Invoices are created as DRAFT and the freelancer clicks Send in the app.\n- Everything you create is a draft. The freelancer opens the returned link and clicks Send, Sign, or Pay themselves; nothing here sends email, signs a contract, charges a card, or marks an invoice paid.\n- Never fetch, quote, or cite a share-link URL (/reset-password, /file, /intake, /inquiry, /kickoff, /scope, /invoice, /contract, /agreement, /delivery, /status, /portal, /statement, /change-order, /invite, /unsubscribe, /year): each is private to the one person it was sent to, and no endpoint or tool returns one.\n- Amounts are integers in the currency's minor unit (cents, pence) beside an ISO 4217 code. totalCents is what a client owes; amountCents is the issued tax-inclusive amount before late fees. Never add amounts in different currencies together.\n- Send an Idempotency-Key header (or reuse identical arguments over MCP) when you retry a write; a repeat under the same key returns the first result and creates nothing.\n- Records you create are labelled with how they were created and appear that way to the freelancer. Add a note when the reason for a draft is not obvious.\n- Nothing here sends, signs, charges, refunds, marks paid, deletes, or touches members or billing.\n- `totalCents` on an invoice is what the client owes and is the only figure to reconcile against. Never sum amounts across currencies; group by currency first.\n- Write bodies are strict: an unknown field is 422 `validation` naming it, never silently dropped, and so is a query parameter outside its enumeration. Records created here carry `createdVia: \"api\"` (or `\"mcp\"` through the MCP server) and are labelled as assistant-created in the app.\n- A `clientId` or `projectId` outside the workspace is 404 `not_found`, identically whether it exists elsewhere or not at all.\nErrors are JSON `{ error, code }`; the status follows the code. Responses are `Cache-Control: no-store`.",
    "termsOfService": "https://paloworks.com/terms",
    "contact": {
      "url": "https://paloworks.com/developers"
    }
  },
  "servers": [
    {
      "url": "https://paloworks.com"
    }
  ],
  "security": [
    {
      "bearerKey": []
    }
  ],
  "tags": [
    {
      "name": "me",
      "description": "The workspace behind the key."
    },
    {
      "name": "clients",
      "description": "Clients."
    },
    {
      "name": "projects",
      "description": "Projects and the records under them: scope, revisions, time, notes."
    },
    {
      "name": "invoices",
      "description": "Invoices and estimates. Created as drafts; sent by a person from the app."
    }
  ],
  "paths": {
    "/api/v1/me": {
      "get": {
        "operationId": "getWorkspace",
        "summary": "The workspace behind the key, its plan, and current usage against plan limits.",
        "description": "The first call an integration makes: confirm the key works, learn the workspace's default currency so amounts can be labelled, and learn which features are unlocked before offering them. Nothing here exposes billing ids, share tokens, member emails, or the owner's identity — a key is a workspace credential, not an account one.",
        "tags": [
          "me"
        ],
        "security": [
          {
            "bearerKey": []
          }
        ],
        "parameters": [],
        "responses": {
          "200": {
            "description": "The workspace and its limits.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "workspace": {
                      "$ref": "#/components/schemas/Workspace",
                      "description": "Id, name, default currency, and the plan actually in force."
                    },
                    "limits": {
                      "$ref": "#/components/schemas/Limits",
                      "description": "Used and allowed counts for clients, active projects, and seats (`limit: null` means unlimited), plus which features the plan unlocks."
                    }
                  },
                  "required": [
                    "workspace",
                    "limits"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "invalid_key: Missing, malformed, revoked, or unknown key. Identical in every case so the endpoint cannot be used to test whether a key is live.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "scope_required: The key is real but lacks the scope this endpoint needs. The body names the scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "rate_limited: Too many requests this minute for the workspace. `Retry-After` says how long to wait, in seconds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "internal: Our fault. Safe to retry with the same `Idempotency-Key`; a failed write stores nothing.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "x-scope": "read",
        "x-idempotent": false,
        "x-mcp-tool": "get_workspace"
      }
    },
    "/api/v1/clients": {
      "get": {
        "operationId": "listClients",
        "summary": "Clients, alphabetical by name, with a project count each.",
        "description": "Every client in the workspace that is not in the trash, ordered by name. `search` matches the name and company, case-insensitively. Deliberately withheld: email addresses and the freelancer's private notes — an integration matching clients to projects needs names and ids, not a way to reach the freelancer's clients directly. Each client carries its e-invoice country code and Peppol id, read-only (they are set in the app).",
        "tags": [
          "clients"
        ],
        "security": [
          {
            "bearerKey": []
          }
        ],
        "parameters": [
          {
            "name": "search",
            "in": "query",
            "required": false,
            "description": "Substring to match against name and company.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "description": "Page number, from 1.",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "required": false,
            "description": "Rows per page. Defaults to 25, capped at 100.",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A page of clients.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ClientSummary"
                      },
                      "description": "The clients on this page."
                    },
                    "page": {
                      "$ref": "#/components/schemas/Page",
                      "description": "Page number, page size, total rows, and page count."
                    }
                  },
                  "required": [
                    "data",
                    "page"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "invalid_key: Missing, malformed, revoked, or unknown key. Identical in every case so the endpoint cannot be used to test whether a key is live.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "scope_required: The key is real but lacks the scope this endpoint needs. The body names the scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "rate_limited: Too many requests this minute for the workspace. `Retry-After` says how long to wait, in seconds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "internal: Our fault. Safe to retry with the same `Idempotency-Key`; a failed write stores nothing.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "x-scope": "read",
        "x-idempotent": false,
        "x-mcp-tool": "list_clients"
      },
      "post": {
        "operationId": "createClient",
        "summary": "Create a client.",
        "description": "Creates a client record and returns it with the in-app link where a person can see it. On the free plan the client cap is enforced and the response is 402 `plan_limit` with an `upgradeUrl`, never a bare 403.",
        "tags": [
          "clients"
        ],
        "security": [
          {
            "bearerKey": []
          }
        ],
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "description": "Any unique string, up to 255 characters of letters, digits, \"_\", \"-\", \":\" or \".\". A repeat of the same key with the same body within 24 hours replays the first response byte-for-byte, with `Idempotency-Replayed: true`, and creates nothing. The same key with a different body is 422 `idempotency_mismatch`.",
            "schema": {
              "type": "string",
              "maxLength": 255,
              "pattern": "^[A-Za-z0-9_\\-:.]+$"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 500,
                    "description": "The client's name. (1–500 characters)"
                  },
                  "email": {
                    "type": "string",
                    "maxLength": 254,
                    "description": "Contact email. Stored, never emailed by the API. (≤ 254 characters)"
                  },
                  "company": {
                    "type": "string",
                    "maxLength": 500,
                    "description": "Company or studio name. (≤ 500 characters)"
                  },
                  "notes": {
                    "type": "string",
                    "maxLength": 5000,
                    "description": "Private notes for the freelancer. Never shown to the client. (≤ 5000 characters)"
                  }
                },
                "required": [
                  "name"
                ],
                "additionalProperties": false
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The created client.",
            "headers": {
              "Idempotency-Replayed": {
                "description": "Present, as `true`, when this is a stored replay rather than a fresh write.",
                "schema": {
                  "type": "string",
                  "enum": [
                    "true"
                  ]
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "client": {
                      "$ref": "#/components/schemas/Client",
                      "description": "The record as stored."
                    },
                    "url": {
                      "type": "string",
                      "description": "In-app path where a person can open it: `/clients/{id}`."
                    }
                  },
                  "required": [
                    "client",
                    "url"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "bad_request: The body is not the JSON object the endpoint expects, or a header is malformed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "invalid_key: Missing, malformed, revoked, or unknown key. Identical in every case so the endpoint cannot be used to test whether a key is live.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "402": {
            "description": "plan_limit: The workspace's plan does not include this, or a plan limit is reached. The body may carry `upgradeUrl`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "scope_required: The key is real but lacks the scope this endpoint needs. The body names the scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "payload_too_large: The request body is over 64 KB.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "idempotency_mismatch: The same `Idempotency-Key` was already used for a different request. Use a new key for a new request. | validation: A field failed validation. `field` names it; `error` says what is wrong.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "rate_limited: Too many requests this minute for the workspace. `Retry-After` says how long to wait, in seconds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "internal: Our fault. Safe to retry with the same `Idempotency-Key`; a failed write stores nothing.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "x-scope": "write:projects",
        "x-idempotent": true,
        "x-mcp-tool": "create_client"
      }
    },
    "/api/v1/projects": {
      "get": {
        "operationId": "listProjects",
        "summary": "Projects, newest first, with their client, scope price, and invoice counts.",
        "description": "Every project that is not in the trash. Archived projects are left out unless `include_archived=true`. Invoice totals are reported per currency and never summed across currencies. Deliberately withheld: project notes, intake answers, share tokens, and client email addresses.",
        "tags": [
          "projects"
        ],
        "security": [
          {
            "bearerKey": []
          }
        ],
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "required": false,
            "description": "Page number, from 1.",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "required": false,
            "description": "Rows per page. Defaults to 25, capped at 100.",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "Filter by lifecycle status. One of active, paused, completed, archived. Any other value is 422 `validation` naming the parameter, never silently ignored.",
            "schema": {
              "type": "string",
              "enum": [
                "active",
                "paused",
                "completed",
                "archived"
              ]
            }
          },
          {
            "name": "stage",
            "in": "query",
            "required": false,
            "description": "Filter by pipeline stage. One of lead, scoping, active, delivered, paid, completed. Any other value is 422 `validation` naming the parameter, never silently ignored.",
            "schema": {
              "type": "string",
              "enum": [
                "lead",
                "scoping",
                "active",
                "delivered",
                "paid",
                "completed"
              ]
            }
          },
          {
            "name": "include_archived",
            "in": "query",
            "required": false,
            "description": "Set to true to include archived projects. Off by default.",
            "schema": {
              "type": "boolean"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A page of projects.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ProjectSummary"
                      },
                      "description": "The projects on this page, with a scope and invoice summary each."
                    },
                    "page": {
                      "$ref": "#/components/schemas/Page",
                      "description": "Page number, page size, total rows, and page count."
                    }
                  },
                  "required": [
                    "data",
                    "page"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "invalid_key: Missing, malformed, revoked, or unknown key. Identical in every case so the endpoint cannot be used to test whether a key is live.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "scope_required: The key is real but lacks the scope this endpoint needs. The body names the scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "rate_limited: Too many requests this minute for the workspace. `Retry-After` says how long to wait, in seconds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "internal: Our fault. Safe to retry with the same `Idempotency-Key`; a failed write stores nothing.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "x-scope": "read",
        "x-idempotent": false,
        "x-mcp-tool": "list_projects"
      },
      "post": {
        "operationId": "createProject",
        "summary": "Create a project for an existing client.",
        "description": "Creates a project under a client that already exists in this workspace — create the client first if it does not. As in the app, an intake link and an empty scope are created alongside, so `draftScope` on the new project fills that scope in. Optional `intake` answers are stored as a submitted intake response so the project starts with the brief attached. The active-project cap on the plan is enforced and returns 402 `plan_limit`.",
        "tags": [
          "projects"
        ],
        "security": [
          {
            "bearerKey": []
          }
        ],
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "description": "Any unique string, up to 255 characters of letters, digits, \"_\", \"-\", \":\" or \".\". A repeat of the same key with the same body within 24 hours replays the first response byte-for-byte, with `Idempotency-Replayed: true`, and creates nothing. The same key with a different body is 422 `idempotency_mismatch`.",
            "schema": {
              "type": "string",
              "maxLength": 255,
              "pattern": "^[A-Za-z0-9_\\-:.]+$"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "clientId": {
                    "type": "string",
                    "description": "Id of a client in this workspace. Unknown ids are 404 `not_found`."
                  },
                  "name": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 500,
                    "description": "Project name. (1–500 characters)"
                  },
                  "stage": {
                    "type": "string",
                    "enum": [
                      "lead",
                      "scoping",
                      "active",
                      "delivered",
                      "paid",
                      "completed"
                    ],
                    "description": "Pipeline stage. Defaults to `active`. Any other value is 422 `validation`. (lead | scoping | active | delivered | paid | completed)"
                  },
                  "notes": {
                    "type": "string",
                    "maxLength": 5000,
                    "description": "Private project notes. (≤ 5000 characters)"
                  },
                  "hourlyRateCents": {
                    "type": "integer",
                    "minimum": 0,
                    "maximum": 100000000,
                    "description": "Hourly rate in minor units, for time tracking. (0–100000000)"
                  },
                  "budgetMinutes": {
                    "type": "integer",
                    "minimum": 0,
                    "maximum": 600000,
                    "description": "Time budget in minutes, for overrun warnings. (0–600000)"
                  },
                  "intake": {
                    "type": "object",
                    "additionalProperties": {
                      "type": "string"
                    },
                    "description": "Question → answer pairs from the brief, stored as the project's intake response with `submittedAt` set to now. (string values only; ≤ 40 answers, questions ≤ 200 and answers ≤ 5000 characters)"
                  }
                },
                "required": [
                  "clientId",
                  "name"
                ],
                "additionalProperties": false
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The created project.",
            "headers": {
              "Idempotency-Replayed": {
                "description": "Present, as `true`, when this is a stored replay rather than a fresh write.",
                "schema": {
                  "type": "string",
                  "enum": [
                    "true"
                  ]
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "project": {
                      "$ref": "#/components/schemas/Project",
                      "description": "The record as stored."
                    },
                    "url": {
                      "type": "string",
                      "description": "In-app path: `/projects/{id}`."
                    }
                  },
                  "required": [
                    "project",
                    "url"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "bad_request: The body is not the JSON object the endpoint expects, or a header is malformed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "invalid_key: Missing, malformed, revoked, or unknown key. Identical in every case so the endpoint cannot be used to test whether a key is live.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "402": {
            "description": "plan_limit: The workspace's plan does not include this, or a plan limit is reached. The body may carry `upgradeUrl`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "scope_required: The key is real but lacks the scope this endpoint needs. The body names the scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "not_found: The addressed record is not in this workspace. Identical whether the id exists elsewhere or not at all.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "payload_too_large: The request body is over 64 KB.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "idempotency_mismatch: The same `Idempotency-Key` was already used for a different request. Use a new key for a new request. | validation: A field failed validation. `field` names it; `error` says what is wrong.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "rate_limited: Too many requests this minute for the workspace. `Retry-After` says how long to wait, in seconds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "internal: Our fault. Safe to retry with the same `Idempotency-Key`; a failed write stores nothing.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "x-scope": "write:projects",
        "x-idempotent": true,
        "x-mcp-tool": "create_project"
      }
    },
    "/api/v1/projects/{id}": {
      "get": {
        "operationId": "getProject",
        "summary": "One project in full: client, scope, revisions, invoices, and time.",
        "description": "The project with its client, lifecycle status and pipeline stage, the full scope (deliverables, revision rounds, timeline, price, whether approved), revision rounds used against the rounds included, every invoice with its computed totals plus per-currency totals, and time totals. Deliberately withheld: intake answers, private notes, share tokens, and the client's email.",
        "tags": [
          "projects"
        ],
        "security": [
          {
            "bearerKey": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The project id."
          }
        ],
        "responses": {
          "200": {
            "description": "The project.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "project": {
                      "$ref": "#/components/schemas/ProjectDetail",
                      "description": "The project with its related records."
                    },
                    "url": {
                      "type": "string",
                      "description": "In-app path: `/projects/{id}`."
                    }
                  },
                  "required": [
                    "project",
                    "url"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "invalid_key: Missing, malformed, revoked, or unknown key. Identical in every case so the endpoint cannot be used to test whether a key is live.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "scope_required: The key is real but lacks the scope this endpoint needs. The body names the scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "not_found: The addressed record is not in this workspace. Identical whether the id exists elsewhere or not at all.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "rate_limited: Too many requests this minute for the workspace. `Retry-After` says how long to wait, in seconds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "internal: Our fault. Safe to retry with the same `Idempotency-Key`; a failed write stores nothing.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "x-scope": "read",
        "x-idempotent": false,
        "x-mcp-tool": "get_project"
      }
    },
    "/api/v1/projects/{id}/scope": {
      "post": {
        "operationId": "draftScope",
        "summary": "Draft or update the project's scope.",
        "description": "Every project has a scope from the moment it is created; this fills it in or updates the draft. The first draft on a new project is version 1, and each later update while the scope is still unapproved bumps `version`; on an update, fields left out keep their current value. A scope the client has already approved is fixed: the request is 409 `conflict`, and changes to agreed work go through a change order in the app. Deliverables are synced to the project's deliverable list. The result is a draft; the freelancer reviews it in the app and decides whether to share it.",
        "tags": [
          "projects"
        ],
        "security": [
          {
            "bearerKey": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The project id."
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "description": "Any unique string, up to 255 characters of letters, digits, \"_\", \"-\", \":\" or \".\". A repeat of the same key with the same body within 24 hours replays the first response byte-for-byte, with `Idempotency-Replayed: true`, and creates nothing. The same key with a different body is 422 `idempotency_mismatch`.",
            "schema": {
              "type": "string",
              "maxLength": 255,
              "pattern": "^[A-Za-z0-9_\\-:.]+$"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "deliverables": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "minItems": 1,
                    "maxItems": 50,
                    "description": "What is included, one line each. Blank lines are dropped. (1–50 items, each ≤ 500 characters)"
                  },
                  "revisionRounds": {
                    "type": "integer",
                    "minimum": 0,
                    "maximum": 20,
                    "description": "Revision rounds included in the price. Defaults to 2 on a new scope. (0–20)"
                  },
                  "timeline": {
                    "type": "string",
                    "maxLength": 500,
                    "description": "Free-text timeline, e.g. \"3 weeks from approval\". (≤ 500 characters)"
                  },
                  "priceCents": {
                    "type": "integer",
                    "minimum": 0,
                    "maximum": 100000000,
                    "description": "Fixed price in minor units. Omit for hourly work. (0–100000000)"
                  },
                  "currency": {
                    "type": "string",
                    "pattern": "^[A-Z]{3}$",
                    "description": "ISO 4217 code. Defaults to the workspace currency. (3 uppercase letters)"
                  }
                },
                "required": [
                  "deliverables"
                ],
                "additionalProperties": false
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The scope as stored.",
            "headers": {
              "Idempotency-Replayed": {
                "description": "Present, as `true`, when this is a stored replay rather than a fresh write.",
                "schema": {
                  "type": "string",
                  "enum": [
                    "true"
                  ]
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "scope": {
                      "$ref": "#/components/schemas/Scope",
                      "description": "The record, including `version` and `approvedAt` (null for a draft)."
                    },
                    "url": {
                      "type": "string",
                      "description": "In-app path: `/projects/{id}?tab=scope`."
                    }
                  },
                  "required": [
                    "scope",
                    "url"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "bad_request: The body is not the JSON object the endpoint expects, or a header is malformed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "invalid_key: Missing, malformed, revoked, or unknown key. Identical in every case so the endpoint cannot be used to test whether a key is live.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "402": {
            "description": "plan_limit: The workspace's plan does not include this, or a plan limit is reached. The body may carry `upgradeUrl`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "scope_required: The key is real but lacks the scope this endpoint needs. The body names the scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "not_found: The addressed record is not in this workspace. Identical whether the id exists elsewhere or not at all.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "conflict: The write conflicts with current state — for example the scope is already approved.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "payload_too_large: The request body is over 64 KB.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "idempotency_mismatch: The same `Idempotency-Key` was already used for a different request. Use a new key for a new request. | validation: A field failed validation. `field` names it; `error` says what is wrong.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "rate_limited: Too many requests this minute for the workspace. `Retry-After` says how long to wait, in seconds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "internal: Our fault. Safe to retry with the same `Idempotency-Key`; a failed write stores nothing.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "x-scope": "write:projects",
        "x-idempotent": true,
        "x-mcp-tool": "draft_scope"
      }
    },
    "/api/v1/projects/{id}/revisions": {
      "post": {
        "operationId": "logRevision",
        "summary": "Log a revision request against the project.",
        "description": "Records a revision round. `roundNumber` is the count so far plus one, and the response says how many rounds the scope includes and whether this one goes over. With `classify: true`, and when AI is configured for the workspace, the request is classified as in-scope, new work, or needing a question to the client, and the verdict is returned; otherwise `classification` is null and `classificationUnavailable` says why.",
        "tags": [
          "projects"
        ],
        "security": [
          {
            "bearerKey": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The project id."
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "description": "Any unique string, up to 255 characters of letters, digits, \"_\", \"-\", \":\" or \".\". A repeat of the same key with the same body within 24 hours replays the first response byte-for-byte, with `Idempotency-Replayed: true`, and creates nothing. The same key with a different body is 422 `idempotency_mismatch`.",
            "schema": {
              "type": "string",
              "maxLength": 255,
              "pattern": "^[A-Za-z0-9_\\-:.]+$"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "description": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 5000,
                    "description": "What the client asked for, in their words where possible. (1–5000 characters)"
                  },
                  "classify": {
                    "type": "boolean",
                    "description": "Run the revision classifier and include its verdict. Defaults to false."
                  }
                },
                "required": [
                  "description"
                ],
                "additionalProperties": false
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The revision and where it lands against the scope.",
            "headers": {
              "Idempotency-Replayed": {
                "description": "Present, as `true`, when this is a stored replay rather than a fresh write.",
                "schema": {
                  "type": "string",
                  "enum": [
                    "true"
                  ]
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "revision": {
                      "$ref": "#/components/schemas/Revision",
                      "description": "The record as stored."
                    },
                    "roundsUsed": {
                      "type": "integer",
                      "description": "Revision rounds logged so far, including this one."
                    },
                    "roundsIncluded": {
                      "type": [
                        "integer",
                        "null"
                      ],
                      "description": "Rounds the scope includes, or null when the project has no scope yet."
                    },
                    "overBudget": {
                      "type": "boolean",
                      "description": "True when `roundsUsed` exceeds `roundsIncluded`. False when there is no scope."
                    },
                    "classification": {
                      "oneOf": [
                        {
                          "$ref": "#/components/schemas/RevisionClassification"
                        },
                        {
                          "type": "null"
                        }
                      ],
                      "description": "The classifier's verdict, or null when not requested or unavailable."
                    },
                    "classificationUnavailable": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "Why no classification was produced, when `classify` was requested."
                    },
                    "url": {
                      "type": "string",
                      "description": "In-app path: `/projects/{id}?tab=revisions`."
                    }
                  },
                  "required": [
                    "revision",
                    "roundsUsed",
                    "roundsIncluded",
                    "overBudget",
                    "classification",
                    "classificationUnavailable",
                    "url"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "bad_request: The body is not the JSON object the endpoint expects, or a header is malformed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "invalid_key: Missing, malformed, revoked, or unknown key. Identical in every case so the endpoint cannot be used to test whether a key is live.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "402": {
            "description": "plan_limit: The workspace's plan does not include this, or a plan limit is reached. The body may carry `upgradeUrl`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "scope_required: The key is real but lacks the scope this endpoint needs. The body names the scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "not_found: The addressed record is not in this workspace. Identical whether the id exists elsewhere or not at all.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "payload_too_large: The request body is over 64 KB.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "idempotency_mismatch: The same `Idempotency-Key` was already used for a different request. Use a new key for a new request. | validation: A field failed validation. `field` names it; `error` says what is wrong.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "rate_limited: Too many requests this minute for the workspace. `Retry-After` says how long to wait, in seconds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "internal: Our fault. Safe to retry with the same `Idempotency-Key`; a failed write stores nothing.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "x-scope": "write:projects",
        "x-idempotent": true,
        "x-mcp-tool": "log_revision"
      }
    },
    "/api/v1/projects/{id}/time": {
      "post": {
        "operationId": "logTime",
        "summary": "Log a time entry.",
        "description": "Adds a time entry to the project. Minutes are integers; there are no fractional hours anywhere in the API. Entries longer than one working day (16 hours) are rejected so a typo cannot become an invoice line.",
        "tags": [
          "projects"
        ],
        "security": [
          {
            "bearerKey": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The project id."
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "description": "Any unique string, up to 255 characters of letters, digits, \"_\", \"-\", \":\" or \".\". A repeat of the same key with the same body within 24 hours replays the first response byte-for-byte, with `Idempotency-Replayed: true`, and creates nothing. The same key with a different body is 422 `idempotency_mismatch`.",
            "schema": {
              "type": "string",
              "maxLength": 255,
              "pattern": "^[A-Za-z0-9_\\-:.]+$"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "minutes": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 960,
                    "description": "Duration in whole minutes. (1–960)"
                  },
                  "date": {
                    "type": "string",
                    "description": "Day the work happened, `YYYY-MM-DD`. Defaults to today (UTC)."
                  },
                  "description": {
                    "type": "string",
                    "maxLength": 500,
                    "description": "What was done. (≤ 500 characters)"
                  },
                  "billable": {
                    "type": "boolean",
                    "description": "Whether the time can be invoiced. Defaults to true."
                  }
                },
                "required": [
                  "minutes"
                ],
                "additionalProperties": false
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The time entry.",
            "headers": {
              "Idempotency-Replayed": {
                "description": "Present, as `true`, when this is a stored replay rather than a fresh write.",
                "schema": {
                  "type": "string",
                  "enum": [
                    "true"
                  ]
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "timeEntry": {
                      "$ref": "#/components/schemas/TimeEntry",
                      "description": "The record as stored."
                    },
                    "url": {
                      "type": "string",
                      "description": "In-app path: `/projects/{id}?tab=time`."
                    }
                  },
                  "required": [
                    "timeEntry",
                    "url"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "bad_request: The body is not the JSON object the endpoint expects, or a header is malformed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "invalid_key: Missing, malformed, revoked, or unknown key. Identical in every case so the endpoint cannot be used to test whether a key is live.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "402": {
            "description": "plan_limit: The workspace's plan does not include this, or a plan limit is reached. The body may carry `upgradeUrl`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "scope_required: The key is real but lacks the scope this endpoint needs. The body names the scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "not_found: The addressed record is not in this workspace. Identical whether the id exists elsewhere or not at all.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "payload_too_large: The request body is over 64 KB.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "idempotency_mismatch: The same `Idempotency-Key` was already used for a different request. Use a new key for a new request. | validation: A field failed validation. `field` names it; `error` says what is wrong.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "rate_limited: Too many requests this minute for the workspace. `Retry-After` says how long to wait, in seconds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "internal: Our fault. Safe to retry with the same `Idempotency-Key`; a failed write stores nothing.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "x-scope": "write:time",
        "x-idempotent": true,
        "x-mcp-tool": "log_time"
      }
    },
    "/api/v1/projects/{id}/notes": {
      "post": {
        "operationId": "addNote",
        "summary": "Leave a note on the project.",
        "description": "Adds an internal note the team sees on the project. The author is recorded as the key's label followed by \"(API)\", so a note left by an integration or an assistant is never mistaken for one a person wrote. Notes are never visible to the client.",
        "tags": [
          "projects"
        ],
        "security": [
          {
            "bearerKey": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The project id."
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "description": "Any unique string, up to 255 characters of letters, digits, \"_\", \"-\", \":\" or \".\". A repeat of the same key with the same body within 24 hours replays the first response byte-for-byte, with `Idempotency-Replayed: true`, and creates nothing. The same key with a different body is 422 `idempotency_mismatch`.",
            "schema": {
              "type": "string",
              "maxLength": 255,
              "pattern": "^[A-Za-z0-9_\\-:.]+$"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "body": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 5000,
                    "description": "The note. (1–5000 characters)"
                  }
                },
                "required": [
                  "body"
                ],
                "additionalProperties": false
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The note.",
            "headers": {
              "Idempotency-Replayed": {
                "description": "Present, as `true`, when this is a stored replay rather than a fresh write.",
                "schema": {
                  "type": "string",
                  "enum": [
                    "true"
                  ]
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "note": {
                      "$ref": "#/components/schemas/Note",
                      "description": "The record as stored."
                    },
                    "url": {
                      "type": "string",
                      "description": "In-app path to the project."
                    }
                  },
                  "required": [
                    "note",
                    "url"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "bad_request: The body is not the JSON object the endpoint expects, or a header is malformed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "invalid_key: Missing, malformed, revoked, or unknown key. Identical in every case so the endpoint cannot be used to test whether a key is live.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "402": {
            "description": "plan_limit: The workspace's plan does not include this, or a plan limit is reached. The body may carry `upgradeUrl`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "scope_required: The key is real but lacks the scope this endpoint needs. The body names the scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "not_found: The addressed record is not in this workspace. Identical whether the id exists elsewhere or not at all.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "payload_too_large: The request body is over 64 KB.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "idempotency_mismatch: The same `Idempotency-Key` was already used for a different request. Use a new key for a new request. | validation: A field failed validation. `field` names it; `error` says what is wrong.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "rate_limited: Too many requests this minute for the workspace. `Retry-After` says how long to wait, in seconds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "internal: Our fault. Safe to retry with the same `Idempotency-Key`; a failed write stores nothing.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "x-scope": "write:projects",
        "x-idempotent": true,
        "x-mcp-tool": "add_note"
      }
    },
    "/api/v1/invoices": {
      "get": {
        "operationId": "listInvoices",
        "summary": "Invoices and estimates with their computed totals.",
        "description": "Every invoice and estimate in the workspace, newest first, each with `createdVia` so an assistant can tell its own drafts from the freelancer's. Totals come from the same calculation the invoice page, the PDF, and the card charge use, so a reconciliation against `totalCents` cannot disagree with what the client was charged. Deliberately withheld: the share token, invoice notes, and the client's email.",
        "tags": [
          "invoices"
        ],
        "security": [
          {
            "bearerKey": []
          }
        ],
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "required": false,
            "description": "Page number, from 1.",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "required": false,
            "description": "Rows per page. Defaults to 25, capped at 100.",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "Filter by status. One of DRAFT, SENT, PAID, VOID. Any other value is 422 `validation` naming the parameter, never silently ignored.",
            "schema": {
              "type": "string",
              "enum": [
                "DRAFT",
                "SENT",
                "PAID",
                "VOID"
              ]
            }
          },
          {
            "name": "kind",
            "in": "query",
            "required": false,
            "description": "Invoices or estimates. One of invoice, estimate. Any other value is 422 `validation` naming the parameter, never silently ignored.",
            "schema": {
              "type": "string",
              "enum": [
                "invoice",
                "estimate"
              ]
            }
          },
          {
            "name": "project_id",
            "in": "query",
            "required": false,
            "description": "Restrict to one project.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "overdue",
            "in": "query",
            "required": false,
            "description": "Set to true for unpaid invoices past their due date.",
            "schema": {
              "type": "boolean"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A page of invoices.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Invoice"
                      },
                      "description": "The invoices on this page."
                    },
                    "page": {
                      "$ref": "#/components/schemas/Page",
                      "description": "Page number, page size, total rows, and page count."
                    }
                  },
                  "required": [
                    "data",
                    "page"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "invalid_key: Missing, malformed, revoked, or unknown key. Identical in every case so the endpoint cannot be used to test whether a key is live.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "scope_required: The key is real but lacks the scope this endpoint needs. The body names the scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "rate_limited: Too many requests this minute for the workspace. `Retry-After` says how long to wait, in seconds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "internal: Our fault. Safe to retry with the same `Idempotency-Key`; a failed write stores nothing.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "x-scope": "read",
        "x-idempotent": false,
        "x-mcp-tool": "list_invoices"
      },
      "post": {
        "operationId": "draftInvoice",
        "summary": "Draft an invoice or estimate.",
        "description": "Creates a DRAFT invoice or estimate on a project. The amount is given exactly one way: a flat `amountCents` subtotal, or 1–50 `lineItems` — not both, and a body with neither is 422 `validation` on `amountCents`. The response carries the computed `taxCents` and `totalCents`. There is no status field on input and no way to change one from the API — a body carrying `status`, `paidAt`, or `sentAt` is 422 `validation` naming the field — the freelancer opens `url` and clicks Send when they are ready. The number and share link are generated on creation but the link is not returned — it is private to the client it is eventually shared with.",
        "tags": [
          "invoices"
        ],
        "security": [
          {
            "bearerKey": []
          }
        ],
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "description": "Any unique string, up to 255 characters of letters, digits, \"_\", \"-\", \":\" or \".\". A repeat of the same key with the same body within 24 hours replays the first response byte-for-byte, with `Idempotency-Replayed: true`, and creates nothing. The same key with a different body is 422 `idempotency_mismatch`.",
            "schema": {
              "type": "string",
              "maxLength": 255,
              "pattern": "^[A-Za-z0-9_\\-:.]+$"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "projectId": {
                    "type": "string",
                    "description": "Id of a project in this workspace. Unknown ids are 404 `not_found`."
                  },
                  "kind": {
                    "type": "string",
                    "enum": [
                      "invoice",
                      "estimate"
                    ],
                    "description": "Defaults to `invoice`. Any other value is 422 `validation`. (invoice | estimate)"
                  },
                  "amountCents": {
                    "type": "integer",
                    "minimum": 0,
                    "maximum": 100000000,
                    "description": "Subtotal in minor units. One of `amountCents` or `lineItems` is required; give one, not both. (0–100000000)"
                  },
                  "lineItems": {
                    "type": "array",
                    "items": {
                      "$ref": "#/components/schemas/LineItemInput"
                    },
                    "minItems": 1,
                    "maxItems": 50,
                    "description": "Line items, each `{ description*, quantityHundredths? (default 100 = 1.00), unitPriceCents*, taxRateBps? }` — a line's `taxRateBps` overrides the invoice's for that line. One of `amountCents` or `lineItems` is required; give one, not both. An empty array is 422 `validation` on `lineItems`. (1–50 items; descriptions ≤ 500 characters, quantityHundredths 0–1000000, unitPriceCents 0–100000000, taxRateBps 0–10000 or null)"
                  },
                  "currency": {
                    "type": "string",
                    "pattern": "^[A-Z]{3}$",
                    "description": "ISO 4217 code. Defaults to the scope's currency, then the workspace currency. (3 uppercase letters)"
                  },
                  "dueDate": {
                    "type": "string",
                    "description": "`YYYY-MM-DD`."
                  },
                  "notes": {
                    "type": "string",
                    "maxLength": 5000,
                    "description": "Notes printed on the invoice. (≤ 5000 characters)"
                  },
                  "taxRateBps": {
                    "type": "integer",
                    "minimum": 0,
                    "maximum": 10000,
                    "description": "Tax rate in basis points (2000 = 20%). Defaults to the workspace's default rate. (0–10000)"
                  },
                  "taxInclusive": {
                    "type": "boolean",
                    "description": "The line prices (or `amountCents`) already include tax: the tax is worked out of each line at its rate — round(price × rate / (10000 + rate)), per line — instead of added on top, the client pays the prices as given, and `taxCents` is the part of the total that is tax. Defaults to the workspace's \"Prices include tax\" setting."
                  },
                  "discountCents": {
                    "type": "integer",
                    "minimum": 0,
                    "maximum": 100000000,
                    "description": "Discount in minor units, applied before tax. With `taxInclusive` it is a tax-inclusive amount off the prices, and carries its share of the tax. (0–100000000)"
                  },
                  "poNumber": {
                    "type": "string",
                    "maxLength": 64,
                    "description": "The client's purchase-order reference, printed on the invoice and its PDF. Whitespace runs fold to one space. (≤ 64 characters)"
                  },
                  "supplyDate": {
                    "type": "string",
                    "description": "Date of supply, `YYYY-MM-DD`, where it differs from the issue date (EU and UK VAT invoices state it). Printed on the invoice and its PDF."
                  }
                },
                "required": [
                  "projectId"
                ],
                "additionalProperties": false,
                "allOf": [
                  {
                    "oneOf": [
                      {
                        "required": [
                          "amountCents"
                        ]
                      },
                      {
                        "required": [
                          "lineItems"
                        ]
                      }
                    ]
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The draft invoice with its computed totals.",
            "headers": {
              "Idempotency-Replayed": {
                "description": "Present, as `true`, when this is a stored replay rather than a fresh write.",
                "schema": {
                  "type": "string",
                  "enum": [
                    "true"
                  ]
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "invoice": {
                      "$ref": "#/components/schemas/Invoice",
                      "description": "The record, status `DRAFT`, with `amountCents`, `discountCents`, `taxRateBps`, `taxCents`, `lateFeeCents`, and `totalCents`."
                    },
                    "url": {
                      "type": "string",
                      "description": "In-app path: `/projects/{projectId}?tab=invoices`."
                    },
                    "next": {
                      "type": "string",
                      "description": "What a person does next: \"Open the link and click Send when you are ready.\""
                    }
                  },
                  "required": [
                    "invoice",
                    "url",
                    "next"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "bad_request: The body is not the JSON object the endpoint expects, or a header is malformed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "invalid_key: Missing, malformed, revoked, or unknown key. Identical in every case so the endpoint cannot be used to test whether a key is live.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "402": {
            "description": "plan_limit: The workspace's plan does not include this, or a plan limit is reached. The body may carry `upgradeUrl`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "scope_required: The key is real but lacks the scope this endpoint needs. The body names the scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "not_found: The addressed record is not in this workspace. Identical whether the id exists elsewhere or not at all.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "payload_too_large: The request body is over 64 KB.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "idempotency_mismatch: The same `Idempotency-Key` was already used for a different request. Use a new key for a new request. | validation: A field failed validation. `field` names it; `error` says what is wrong.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "rate_limited: Too many requests this minute for the workspace. `Retry-After` says how long to wait, in seconds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "internal: Our fault. Safe to retry with the same `Idempotency-Key`; a failed write stores nothing.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "x-scope": "write:invoices",
        "x-idempotent": true,
        "x-mcp-tool": "draft_invoice"
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerKey": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "crk_…",
        "description": "A workspace API key from Settings → API keys. Scopes: read, write:projects, write:time, write:invoices. Every key has `read`; a write scope is needed for the POST routes under it. A missing and an invalid key get the identical 401."
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string",
            "description": "Human-readable message."
          },
          "code": {
            "type": "string",
            "enum": [
              "invalid_key",
              "scope_required",
              "plan_limit",
              "rate_limited",
              "validation",
              "not_found",
              "conflict",
              "idempotency_mismatch",
              "payload_too_large",
              "bad_request",
              "internal"
            ],
            "description": "invalid_key (401): Missing, malformed, revoked, or unknown key. Identical in every case so the endpoint cannot be used to test whether a key is live. scope_required (403): The key is real but lacks the scope this endpoint needs. The body names the scope. plan_limit (402): The workspace's plan does not include this, or a plan limit is reached. The body may carry `upgradeUrl`. rate_limited (429): Too many requests this minute for the workspace. `Retry-After` says how long to wait, in seconds. validation (422): A field failed validation. `field` names it; `error` says what is wrong. not_found (404): The addressed record is not in this workspace. Identical whether the id exists elsewhere or not at all. conflict (409): The write conflicts with current state — for example the scope is already approved. idempotency_mismatch (422): The same `Idempotency-Key` was already used for a different request. Use a new key for a new request. payload_too_large (413): The request body is over 64 KB. bad_request (400): The body is not the JSON object the endpoint expects, or a header is malformed. internal (500): Our fault. Safe to retry with the same `Idempotency-Key`; a failed write stores nothing."
          },
          "field": {
            "type": "string",
            "description": "For `validation`: the field that failed."
          },
          "upgradeUrl": {
            "type": "string",
            "description": "For `plan_limit`: the in-app path where a person can upgrade."
          },
          "scope": {
            "type": "string",
            "description": "For `scope_required`: the scope the key lacks."
          }
        },
        "required": [
          "error",
          "code"
        ],
        "additionalProperties": true
      },
      "MinorUnits": {
        "type": [
          "integer",
          "null"
        ],
        "description": "An amount in integer minor units of its currency — cents for USD, pence for GBP. There are no floats anywhere in the API. Never sum amounts across currencies."
      },
      "Currency": {
        "type": "string",
        "pattern": "^[A-Z]{3}$",
        "description": "ISO 4217 code."
      },
      "Page": {
        "type": "object",
        "properties": {
          "page": {
            "type": "integer",
            "description": "1-based."
          },
          "perPage": {
            "type": "integer"
          },
          "total": {
            "type": "integer"
          },
          "pageCount": {
            "type": "integer"
          }
        },
        "required": [
          "page",
          "perPage",
          "total",
          "pageCount"
        ]
      },
      "Workspace": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "currency": {
            "$ref": "#/components/schemas/Currency"
          },
          "defaultTaxRateBps": {
            "type": "integer",
            "description": "Basis points applied to an invoice when `taxRateBps` is omitted."
          },
          "plan": {
            "type": "string",
            "enum": [
              "free",
              "pro",
              "studio"
            ],
            "description": "The plan actually in force."
          }
        },
        "required": [
          "id",
          "name",
          "currency",
          "plan"
        ]
      },
      "Usage": {
        "type": "object",
        "properties": {
          "used": {
            "type": "integer"
          },
          "limit": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Null means unlimited on this plan."
          }
        },
        "required": [
          "used",
          "limit"
        ]
      },
      "Limits": {
        "type": "object",
        "properties": {
          "clients": {
            "$ref": "#/components/schemas/Usage"
          },
          "activeProjects": {
            "$ref": "#/components/schemas/Usage"
          },
          "seats": {
            "$ref": "#/components/schemas/Usage",
            "description": "Collaborators beyond the owner."
          },
          "features": {
            "type": "object",
            "additionalProperties": {
              "type": "boolean"
            },
            "description": "Feature name → whether the plan unlocks it (contracts, timeTracking, calendar, team, prioritySupport, api)."
          }
        },
        "required": [
          "clients",
          "activeProjects",
          "seats",
          "features"
        ]
      },
      "ClientSummary": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "company": {
            "type": [
              "string",
              "null"
            ]
          },
          "countryCode": {
            "type": [
              "string",
              "null"
            ],
            "description": "ISO 3166-1 alpha-2 country of the billing address, for e-invoices, or null. Read-only here; set in the app."
          },
          "peppolEndpointId": {
            "type": [
              "string",
              "null"
            ],
            "description": "The client's Peppol participant id as `scheme:identifier` (e.g. `0208:0123456749`), or null. Read-only here; set in the app."
          },
          "projectCount": {
            "type": "integer",
            "description": "Projects not in the trash."
          },
          "createdVia": {
            "type": "string",
            "enum": [
              "app",
              "api",
              "mcp",
              "import",
              "handoff"
            ],
            "description": "How the record came to exist. `api` and `mcp` mark records an assistant created; the app shows a badge."
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          }
        },
        "required": [
          "id",
          "name",
          "company",
          "countryCode",
          "peppolEndpointId",
          "projectCount",
          "createdAt",
          "updatedAt"
        ],
        "description": "A client as listed. Email and private notes are withheld from the list."
      },
      "Client": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "email": {
            "type": [
              "string",
              "null"
            ]
          },
          "company": {
            "type": [
              "string",
              "null"
            ]
          },
          "notes": {
            "type": [
              "string",
              "null"
            ],
            "description": "Private to the freelancer."
          },
          "createdVia": {
            "type": "string",
            "enum": [
              "app",
              "api",
              "mcp",
              "import",
              "handoff"
            ],
            "description": "How the record came to exist. `api` and `mcp` mark records an assistant created; the app shows a badge."
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          }
        },
        "required": [
          "id",
          "name",
          "email",
          "company",
          "createdAt",
          "updatedAt"
        ],
        "description": "A client as created: the record exactly as stored."
      },
      "ProjectSummary": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "active",
              "paused",
              "completed",
              "archived"
            ],
            "description": "Lifecycle, distinct from the pipeline stage."
          },
          "stage": {
            "type": "string",
            "enum": [
              "lead",
              "scoping",
              "active",
              "delivered",
              "paid",
              "completed"
            ],
            "description": "Pipeline position."
          },
          "archivedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "createdVia": {
            "type": "string",
            "enum": [
              "app",
              "api",
              "mcp",
              "import",
              "handoff"
            ],
            "description": "How the record came to exist. `api` and `mcp` mark records an assistant created; the app shows a badge."
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          },
          "client": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string"
              },
              "name": {
                "type": "string"
              },
              "company": {
                "type": [
                  "string",
                  "null"
                ]
              }
            },
            "required": [
              "id",
              "name",
              "company"
            ]
          },
          "scope": {
            "oneOf": [
              {
                "type": "object",
                "properties": {
                  "priceCents": {
                    "$ref": "#/components/schemas/MinorUnits",
                    "description": "Fixed price, or null for hourly work."
                  },
                  "currency": {
                    "$ref": "#/components/schemas/Currency"
                  },
                  "approvedAt": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "format": "date-time"
                  }
                },
                "required": [
                  "priceCents",
                  "currency",
                  "approvedAt"
                ]
              },
              {
                "type": "null"
              }
            ],
            "description": "Price and approval, or null when no scope exists yet."
          },
          "invoices": {
            "type": "object",
            "properties": {
              "count": {
                "type": "integer"
              },
              "overdueCount": {
                "type": "integer"
              },
              "totals": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "currency": {
                      "$ref": "#/components/schemas/Currency"
                    },
                    "billedCents": {
                      "$ref": "#/components/schemas/MinorUnits"
                    },
                    "paidCents": {
                      "$ref": "#/components/schemas/MinorUnits"
                    },
                    "outstandingCents": {
                      "$ref": "#/components/schemas/MinorUnits"
                    }
                  },
                  "required": [
                    "currency",
                    "billedCents",
                    "paidCents",
                    "outstandingCents"
                  ]
                },
                "description": "One row per currency. Never summed across currencies; estimates excluded."
              }
            },
            "required": [
              "count",
              "overdueCount",
              "totals"
            ]
          }
        },
        "required": [
          "id",
          "name",
          "status",
          "stage",
          "archivedAt",
          "client",
          "scope",
          "invoices",
          "createdAt",
          "updatedAt"
        ],
        "description": "A project as listed, with a scope and invoice summary. Notes, intake answers, and share tokens are withheld."
      },
      "Project": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "active",
              "paused",
              "completed",
              "archived"
            ],
            "description": "Lifecycle, distinct from the pipeline stage. New projects are `active`."
          },
          "stage": {
            "type": "string",
            "enum": [
              "lead",
              "scoping",
              "active",
              "delivered",
              "paid",
              "completed"
            ],
            "description": "Pipeline position."
          },
          "notes": {
            "type": [
              "string",
              "null"
            ]
          },
          "hourlyRateCents": {
            "$ref": "#/components/schemas/MinorUnits"
          },
          "budgetMinutes": {
            "type": [
              "integer",
              "null"
            ]
          },
          "clientId": {
            "type": "string"
          },
          "intakeSubmittedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Set when `intake` answers were given on creation."
          },
          "createdVia": {
            "type": "string",
            "enum": [
              "app",
              "api",
              "mcp",
              "import",
              "handoff"
            ],
            "description": "How the record came to exist. `api` and `mcp` mark records an assistant created; the app shows a badge."
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          }
        },
        "required": [
          "id",
          "name",
          "status",
          "stage",
          "notes",
          "hourlyRateCents",
          "budgetMinutes",
          "clientId",
          "intakeSubmittedAt",
          "createdAt",
          "updatedAt"
        ],
        "description": "A project as created: the record exactly as stored."
      },
      "ProjectDetail": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "active",
              "paused",
              "completed",
              "archived"
            ],
            "description": "Lifecycle, distinct from the pipeline stage."
          },
          "stage": {
            "type": "string",
            "enum": [
              "lead",
              "scoping",
              "active",
              "delivered",
              "paid",
              "completed"
            ],
            "description": "Pipeline position."
          },
          "archivedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "hourlyRateCents": {
            "$ref": "#/components/schemas/MinorUnits"
          },
          "budgetMinutes": {
            "type": [
              "integer",
              "null"
            ]
          },
          "intakeSubmittedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When the client submitted the intake form, if they have."
          },
          "noteCount": {
            "type": "integer",
            "description": "Internal notes on the project. The notes themselves are not returned."
          },
          "deliverableCount": {
            "type": "integer"
          },
          "createdVia": {
            "type": "string",
            "enum": [
              "app",
              "api",
              "mcp",
              "import",
              "handoff"
            ],
            "description": "How the record came to exist. `api` and `mcp` mark records an assistant created; the app shows a badge."
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          },
          "client": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string"
              },
              "name": {
                "type": "string"
              },
              "company": {
                "type": [
                  "string",
                  "null"
                ]
              }
            },
            "required": [
              "id",
              "name",
              "company"
            ]
          },
          "scope": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/Scope"
              },
              {
                "type": "null"
              }
            ],
            "description": "The full scope, or null when none exists yet."
          },
          "revisions": {
            "type": "object",
            "properties": {
              "roundsUsed": {
                "type": "integer",
                "description": "Revision rounds logged so far."
              },
              "count": {
                "type": "integer",
                "description": "Alias of `roundsUsed`."
              },
              "roundsIncluded": {
                "type": [
                  "integer",
                  "null"
                ],
                "description": "Rounds the scope includes, or null with no scope."
              },
              "overBudget": {
                "type": "boolean",
                "description": "True when `roundsUsed` exceeds `roundsIncluded`."
              }
            },
            "required": [
              "roundsUsed",
              "count",
              "roundsIncluded",
              "overBudget"
            ]
          },
          "invoices": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Invoice"
            },
            "description": "Every invoice and estimate on the project, newest first, each with its computed totals."
          },
          "invoiceTotals": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "currency": {
                  "$ref": "#/components/schemas/Currency"
                },
                "billedCents": {
                  "$ref": "#/components/schemas/MinorUnits"
                },
                "paidCents": {
                  "$ref": "#/components/schemas/MinorUnits"
                },
                "outstandingCents": {
                  "$ref": "#/components/schemas/MinorUnits"
                }
              },
              "required": [
                "currency",
                "billedCents",
                "paidCents",
                "outstandingCents"
              ]
            },
            "description": "One row per currency. Never summed across currencies; estimates excluded."
          },
          "time": {
            "type": "object",
            "properties": {
              "totalMinutes": {
                "type": "integer"
              },
              "billableMinutes": {
                "type": "integer"
              },
              "uninvoicedBillableMinutes": {
                "type": "integer",
                "description": "Billable minutes not yet on an invoice."
              },
              "budgetMinutes": {
                "type": [
                  "integer",
                  "null"
                ]
              },
              "entryCount": {
                "type": "integer"
              }
            },
            "required": [
              "totalMinutes",
              "billableMinutes",
              "uninvoicedBillableMinutes",
              "budgetMinutes",
              "entryCount"
            ]
          }
        },
        "required": [
          "id",
          "name",
          "status",
          "stage",
          "archivedAt",
          "hourlyRateCents",
          "budgetMinutes",
          "intakeSubmittedAt",
          "noteCount",
          "deliverableCount",
          "createdVia",
          "createdAt",
          "updatedAt",
          "client",
          "scope",
          "revisions",
          "invoices",
          "invoiceTotals",
          "time"
        ],
        "description": "One project with everything an assistant needs to say where the job stands."
      },
      "Scope": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "deliverables": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "revisionRounds": {
            "type": "integer"
          },
          "timeline": {
            "type": [
              "string",
              "null"
            ]
          },
          "priceCents": {
            "$ref": "#/components/schemas/MinorUnits"
          },
          "currency": {
            "$ref": "#/components/schemas/Currency"
          },
          "version": {
            "type": "integer",
            "description": "1 for the first draft on a new project; bumped on each later update while unapproved."
          },
          "approved": {
            "type": "boolean",
            "description": "True once the client has approved it. An approved scope cannot be changed over the API."
          },
          "approvedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Null for a draft."
          },
          "createdVia": {
            "type": "string",
            "enum": [
              "app",
              "api",
              "mcp",
              "import",
              "handoff"
            ],
            "description": "How the record came to exist. `api` and `mcp` mark records an assistant created; the app shows a badge."
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          }
        },
        "required": [
          "id",
          "deliverables",
          "revisionRounds",
          "timeline",
          "priceCents",
          "currency",
          "version",
          "approved",
          "approvedAt",
          "createdAt",
          "updatedAt"
        ]
      },
      "Revision": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "projectId": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "roundNumber": {
            "type": "integer"
          },
          "createdVia": {
            "type": "string",
            "enum": [
              "app",
              "api",
              "mcp",
              "import",
              "handoff"
            ],
            "description": "How the record came to exist. `api` and `mcp` mark records an assistant created; the app shows a badge."
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          }
        },
        "required": [
          "id",
          "projectId",
          "description",
          "roundNumber",
          "createdAt"
        ]
      },
      "RevisionClassification": {
        "type": "object",
        "properties": {
          "verdict": {
            "type": "string",
            "enum": [
              "in_scope",
              "new_work",
              "needs_clarification"
            ],
            "description": "Whether the request is within the agreed scope, new work, or not settled by the scope — `needs_clarification` means ask the client before treating it as either."
          },
          "confidence": {
            "type": "string",
            "enum": [
              "high",
              "medium",
              "low"
            ]
          },
          "reason": {
            "type": "string"
          },
          "suggestedChangeOrderCents": {
            "$ref": "#/components/schemas/MinorUnits",
            "type": "integer",
            "description": "Only alongside `new_work`. A suggestion for a person to consider; nothing is created."
          },
          "suggestedChangeOrderTitle": {
            "type": "string",
            "description": "Only alongside `new_work`."
          }
        },
        "required": [
          "verdict",
          "confidence",
          "reason"
        ]
      },
      "TimeEntry": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "projectId": {
            "type": "string"
          },
          "minutes": {
            "type": "integer"
          },
          "date": {
            "type": "string",
            "format": "date",
            "description": "`YYYY-MM-DD`."
          },
          "description": {
            "type": [
              "string",
              "null"
            ]
          },
          "billable": {
            "type": "boolean"
          },
          "createdVia": {
            "type": "string",
            "enum": [
              "app",
              "api",
              "mcp",
              "import",
              "handoff"
            ],
            "description": "How the record came to exist. `api` and `mcp` mark records an assistant created; the app shows a badge."
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          }
        },
        "required": [
          "id",
          "projectId",
          "minutes",
          "date",
          "description",
          "billable",
          "createdAt"
        ]
      },
      "LineItemInput": {
        "type": "object",
        "properties": {
          "description": {
            "type": "string",
            "minLength": 1,
            "maxLength": 500,
            "description": "What the line is for."
          },
          "quantityHundredths": {
            "type": "integer",
            "minimum": 0,
            "maximum": 1000000,
            "default": 100,
            "description": "Quantity × 100, so 1.5 is 150. Defaults to 100 (one unit); at most 1000000."
          },
          "unitPriceCents": {
            "$ref": "#/components/schemas/MinorUnits",
            "type": "integer",
            "minimum": 0,
            "maximum": 100000000,
            "description": "Unit price in integer minor units."
          },
          "taxRateBps": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 0,
            "maximum": 10000,
            "description": "This line's own tax rate in basis points (2000 = 20%; 0 = a zero-rated line). Omit or null to use the invoice's `taxRateBps`."
          }
        },
        "required": [
          "description",
          "unitPriceCents"
        ],
        "additionalProperties": false
      },
      "LineItem": {
        "type": "object",
        "properties": {
          "description": {
            "type": "string"
          },
          "quantityHundredths": {
            "type": "integer"
          },
          "unitPriceCents": {
            "$ref": "#/components/schemas/MinorUnits",
            "type": "integer"
          },
          "taxRateBps": {
            "type": [
              "integer",
              "null"
            ],
            "description": "The line's own rate in basis points, or null when it uses the invoice's."
          }
        },
        "required": [
          "description",
          "quantityHundredths",
          "unitPriceCents",
          "taxRateBps"
        ]
      },
      "TaxLine": {
        "type": "object",
        "properties": {
          "rateBps": {
            "type": "integer",
            "description": "Basis points: 2000 = 20%."
          },
          "netCents": {
            "$ref": "#/components/schemas/MinorUnits",
            "type": "integer",
            "description": "What the tax at this rate was charged on, after the discount."
          },
          "taxCents": {
            "$ref": "#/components/schemas/MinorUnits",
            "type": "integer"
          }
        },
        "required": [
          "rateBps",
          "netCents",
          "taxCents"
        ]
      },
      "Invoice": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "number": {
            "type": "string"
          },
          "kind": {
            "type": "string",
            "enum": [
              "invoice",
              "estimate"
            ]
          },
          "status": {
            "type": "string",
            "enum": [
              "DRAFT",
              "SENT",
              "PAID",
              "VOID"
            ],
            "description": "Always DRAFT when created over the API."
          },
          "amountCents": {
            "$ref": "#/components/schemas/MinorUnits",
            "type": "integer",
            "description": "The issued tax-inclusive amount, after discount and before late fees."
          },
          "discountCents": {
            "$ref": "#/components/schemas/MinorUnits",
            "type": "integer"
          },
          "taxRateBps": {
            "type": "integer",
            "description": "Basis points: 2000 = 20%."
          },
          "taxInclusive": {
            "type": "boolean",
            "description": "The prices include tax: `taxCents` was worked out of them and is inside `totalCents` rather than added to it."
          },
          "taxCents": {
            "$ref": "#/components/schemas/MinorUnits",
            "type": "integer"
          },
          "taxLines": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/TaxLine"
            },
            "description": "The tax per rate: one row for a single-rate invoice, one per rate when lines carry their own. `netCents` sum to the net, `taxCents` to `taxCents`."
          },
          "lateFeeCents": {
            "$ref": "#/components/schemas/MinorUnits",
            "type": "integer"
          },
          "totalCents": {
            "$ref": "#/components/schemas/MinorUnits",
            "type": "integer",
            "description": "What the client owes: net + tax + late fee (subtotal − discount + tax + late fee when prices exclude tax). Reconcile against this, never the subtotal."
          },
          "currency": {
            "$ref": "#/components/schemas/Currency"
          },
          "dueDate": {
            "type": [
              "string",
              "null"
            ],
            "description": "An ISO timestamp on the list; `YYYY-MM-DD` on the create response and the project read."
          },
          "poNumber": {
            "type": [
              "string",
              "null"
            ],
            "description": "The client's purchase-order reference, or null."
          },
          "supplyDate": {
            "type": [
              "string",
              "null"
            ],
            "description": "Date of supply, `YYYY-MM-DD`, or null when it is the issue date."
          },
          "paidAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "sentAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "overdue": {
            "type": "boolean",
            "description": "Unpaid and past its due date. Present on reads."
          },
          "createdVia": {
            "type": "string",
            "enum": [
              "app",
              "api",
              "mcp",
              "import",
              "handoff"
            ],
            "description": "How the record came to exist. `api` and `mcp` mark records an assistant created; the app shows a badge."
          },
          "projectId": {
            "type": "string",
            "description": "Present on the create response."
          },
          "notes": {
            "type": [
              "string",
              "null"
            ],
            "description": "Present on the create response only; withheld from reads."
          },
          "lineItems": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/LineItem"
            }
          },
          "project": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string"
              },
              "name": {
                "type": "string"
              }
            },
            "required": [
              "id",
              "name"
            ]
          },
          "client": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string"
              },
              "name": {
                "type": "string"
              },
              "company": {
                "type": [
                  "string",
                  "null"
                ]
              }
            },
            "required": [
              "id",
              "name",
              "company"
            ]
          }
        },
        "required": [
          "id",
          "number",
          "kind",
          "status",
          "amountCents",
          "discountCents",
          "taxRateBps",
          "taxCents",
          "lateFeeCents",
          "totalCents",
          "currency",
          "dueDate",
          "poNumber",
          "supplyDate",
          "createdAt"
        ]
      },
      "Note": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "projectId": {
            "type": "string"
          },
          "body": {
            "type": "string"
          },
          "authorName": {
            "type": "string",
            "description": "The key's label followed by \"(API)\"."
          },
          "createdVia": {
            "type": "string",
            "enum": [
              "app",
              "api",
              "mcp",
              "import",
              "handoff"
            ],
            "description": "How the record came to exist. `api` and `mcp` mark records an assistant created; the app shows a badge."
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          }
        },
        "required": [
          "id",
          "projectId",
          "body",
          "authorName",
          "createdAt"
        ]
      }
    }
  },
  "x-mcp-server": {
    "url": "https://paloworks.com/api/mcp",
    "transport": "streamable-http",
    "docs": "https://paloworks.com/developers/mcp",
    "tools": [
      {
        "name": "get_workspace",
        "operationId": "getWorkspace",
        "scope": "read"
      },
      {
        "name": "list_clients",
        "operationId": "listClients",
        "scope": "read"
      },
      {
        "name": "create_client",
        "operationId": "createClient",
        "scope": "write:projects"
      },
      {
        "name": "list_projects",
        "operationId": "listProjects",
        "scope": "read"
      },
      {
        "name": "get_project",
        "operationId": "getProject",
        "scope": "read"
      },
      {
        "name": "create_project",
        "operationId": "createProject",
        "scope": "write:projects"
      },
      {
        "name": "draft_scope",
        "operationId": "draftScope",
        "scope": "write:projects"
      },
      {
        "name": "log_revision",
        "operationId": "logRevision",
        "scope": "write:projects"
      },
      {
        "name": "log_time",
        "operationId": "logTime",
        "scope": "write:time"
      },
      {
        "name": "list_invoices",
        "operationId": "listInvoices",
        "scope": "read"
      },
      {
        "name": "draft_invoice",
        "operationId": "draftInvoice",
        "scope": "write:invoices"
      },
      {
        "name": "add_note",
        "operationId": "addNote",
        "scope": "write:projects"
      }
    ]
  },
  "x-will-not": [
    "send an invoice, estimate, contract, or any email to a client",
    "sign or countersign a contract",
    "charge a card, record a payment, or mark an invoice paid",
    "delete or trash anything",
    "invite, remove, or change the role of a member",
    "change the plan, billing, or Stripe settings",
    "return a share-link URL, a client's email address, or a Stripe id"
  ]
}