openapi: 3.1.0
info:
  title: Hourbrook API
  version: "1.0"
  summary: Time entries, projects, plans, reports and invoices for Hourbrook workspaces.
  description: |
    The Hourbrook API is the server behind the Mac app and the web dashboard. Everything they can do with your
    workspaces, you can do with a token: log and fix time, keep projects, clients and tags in line with another
    tool, plan the coming weeks, pull reports, make invoices, and get a webhook when something changes.

    ## What the server never has

    Hourbrook records your day on your Mac. Apps, window titles, web addresses and anything you type into an AI
    agent stay there: the server has no table, column or endpoint for them, and nothing here can read them. What
    reaches the server is what you chose to keep: entries, projects, clients, tags, plans, tasks and invoices.

    ## Authentication

    Every request carries a bearer token:

    ```http
    Authorization: Bearer hb_pat_…
    ```

    Make a **personal API token** in the dashboard (Settings → API tokens) or with `POST /v1/api-tokens`. It is
    shown once; Hourbrook stores only a hash. A token acts as you, in every workspace you belong to, with your role
    there. A few things that could hand a workspace to someone else need a real sign-in and refuse tokens with
    `403 session_required`: managing sessions and tokens, deleting the account, changing roles, removing members,
    inviting people, webhooks, Slack, scheduled report emails, and bank details on invoices. Those carry a
    **Session only** badge below; the few that refuse tokens only for some changes say which in their text.

    Revoke a token at any time with `DELETE /v1/api-tokens/{id}`; it stops working at once.

    ## Conventions

    - **JSON in, JSON out.** Request bodies are JSON (at most 5 MB). Unknown fields are ignored, never stored.
    - **Times are milliseconds** since the Unix epoch (UTC), as integers. Calendar days in plans and tasks are
      sent the way the endpoint says (local midnight in ms, or `YYYY-MM-DD`).
    - **You make the ids.** Clients, projects, tags, entries, plans, tasks and planned blocks take a UUID that you
      generate, so a write can be retried safely with `PUT`. The server makes ids for workspaces, invites,
      invoices, tokens and webhooks.
    - **Deletes are soft.** A deleted row keeps its id with `deleted_at` set, so other devices learn about it.
    - **Money** is rounded to 2 decimals and only ever sent to workspace owners and admins. Members and team leads
      get `null` where an amount, rate, cost or profit would be.
    - **Hours** are decimal (`1.5` = 1 h 30 min).
    - Every response is `200` on success.

    ## Workspaces and roles

    Everyone has one **personal** workspace (`kind: "personal"`). **Team** workspaces (`kind: "team"`) have
    members with a role: `owner`, `admin` or `member`. Inside a team workspace, **teams** group people and have
    **leads**: a lead sees and manages the time, plans and capacity of the people in their teams, never money.
    Clients, projects and tags belong to a workspace; an entry belongs to its author and to a project, and so to
    the project's workspace.

    Something you can't see answers `404`, not `403`, so ids from other workspaces don't leak.

    ## Errors

    Errors are JSON with a message for people and, for some, a `code` for programs:

    ```json
    { "error": "This project needs at least one tag on every entry.", "code": "tags_required" }
    ```

    | Status | Meaning |
    | --- | --- |
    | `400` | The input is invalid. The message says which field. |
    | `401` | No token, or it is unknown, revoked or expired. |
    | `403` | You may not do this (`code: "session_required"`: use a signed-in session, not a token). |
    | `404` | No such route, or nothing you can see by that id. |
    | `409` | The state doesn't allow it: an invoiced entry, `code: "billed"`, `"ambiguous"`, `"transfer_ownership"`. |
    | `422` | The project needs tags or notes: `code: "tags_required"` or `"notes_required"`. |
    | `429` | Too many requests. `Retry-After` says how many seconds to wait. |
    | `502` | An email could not be sent. |

    ## Rate limits

    Limits are per person (or per address for public links) and per endpoint, generous for normal use. Every `429`
    carries `Retry-After` in seconds. `GET /v1/reports` allows 120 reports a minute; public report links 60
    requests a minute per address.

    ## Webhooks

    Owners and admins can register up to 10 webhooks per workspace. Hourbrook POSTs each event as JSON and signs
    the raw body with the webhook's secret:

    ```http
    X-Hourbrook-Event: entry.created
    X-Hourbrook-Delivery: 6f1c…
    X-Hourbrook-Signature: sha256=<hex HMAC-SHA256(secret, raw body)>
    ```

    Recompute the HMAC over the raw body, compare in constant time, and reject old `created_at` values to stop
    replays. Delivery is best effort: a 5 second timeout, no redirects, no retries. The event payloads are listed
    under **Webhooks** below.

    Events exist for entries, clients, projects, tags and plans (`<row>.created`, `.updated`, `.deleted`, each
    carrying the row as the API returns it), plus `task.done`, `invoice.created` and `week.closed`. A new webhook
    gets the three entry events unless it names others; `PATCH /v1/webhooks/{id}` changes the list later. One
    request sends at most 40 deliveries; a sync that changes more rows than that drops the rest (logged).

    ## Syncing with other tools

    Clients, projects, tags, entries, plans and members carry an `external_id`: your id for the row in Jira, Linear
    or wherever it came from, so a sync finds what it already imported. Hourbrook stores it as given (one line, at
    most 200 characters) and never requires it to be unique. A write that leaves it out keeps the stored value, so
    apps that don't know the field never clear it. The lists of clients, projects, tags, plans and entries take
    `?external_id=` to find a row by it. `GET /v1/entries` pages with `limit` and `cursor` for big ranges.

    ## AI agents (MCP)

    Claude and other MCP clients can connect to Hourbrook at `/mcp` with their own sign-in (OAuth 2.1), without a
    token from you. That connector and its OAuth endpoints are described in the MCP guide, not here.
  contact:
    name: Hourbrook
    url: https://hourbrook.com
servers:
  - url: https://hourbrook-api.mlazarev.workers.dev
    description: Production
  - url: http://localhost:8787
    description: Local (npm run dev in apps/api)
security:
  - apiToken: []
tags:
  - name: Account
    description: You, your profile, your sessions and your API tokens.
  - name: Time entries
    description: |
      Logged time. Entry ids are unique per author, so endpoints that act on someone else's entry take `user_id`
      to say whose it is. An entry on an invoice, or marked billed, is locked: edits answer `409`.
  - name: Timer
    description: The running timer. One per person; stopping it makes an entry.
  - name: Projects
  - name: Clients
  - name: Tags
  - name: Workspaces
    description: Team workspaces, their settings and their members.
  - name: Invites
  - name: Teams
    description: Groups of people inside a team workspace, with leads.
  - name: Plans
    description: |
      Planned hours: who will work how long on which project on which day, plus time off. Plans are a forecast,
      never logged time.
  - name: Tasks
    description: |
      Your own task list, checklists and planned blocks on the calendar. Private to you: nobody else in any
      workspace sees them, and there is no `user_id` parameter.
  - name: Reports
    description: Totals and breakdowns of logged time, capacity and budgets.
  - name: Shared reports
    description: Public links to a report, and emailing them on a schedule.
  - name: Invoices
  - name: Webhooks
  - name: Search
  - name: Integrations
    description: Slack and connected issue trackers. Used by the dashboard and the Mac app.
  # Only on routes marked x-internal (hidden from the page); left out of x-tagGroups so the page has no empty group.
  - name: Sign-in
    description: How the apps sign in. Use a personal API token instead.
  - name: Sync
    description: The Mac app's sync protocol.
x-tagGroups:
  - name: Time
    tags: [Time entries, Timer, Plans, Tasks]
  - name: Catalog
    tags: [Projects, Clients, Tags]
  - name: People
    tags: [Account, Workspaces, Invites, Teams]
  - name: Reporting and billing
    tags: [Reports, Shared reports, Invoices]
  - name: Automation
    tags: [Webhooks, Search, Integrations]
paths:
  /v1/me:
    get:
      operationId: getMe
      tags:
      - Account
      summary: Get your account
      description: 'Returns who you are and every workspace you belong to, with your role in each. Works with a personal API
  
        token. A good first call to check that a token works. `invoice` is only filled for workspaces where you are an
  
        owner or admin.
  
        '
      responses:
        '200':
          description: Your profile and workspaces.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MeResponse'
              example:
                user:
                  id: 5d0c2f6e-3b1a-4c53-9a0e-7a1d6c2b9e41
                  email: ada@example.com
                  currency: USD
                  name: Ada Lovelace
                  timezone: Europe/Berlin
                  week_start: 1
                workspaces:
                - id: ws_8f2c41d7
                  name: Personal
                  kind: personal
                  role: owner
                  currency: USD
                  invoice:
                    company_name: ''
                    company_address: ''
                    tax_id: ''
                    prefix: INV-
                    next_number: 1
                    default_tax_rate: 0
                    payment_terms_days: 14
                    footer: ''
                    payment: {}
        '401':
          $ref: '#/components/responses/Unauthorized'
    patch:
      operationId: updateMe
      tags:
      - Account
      summary: Update your profile
      description: 'Changes your currency, display name, time zone or first day of the week. Send only the fields you want to
  
        change, and at least one. Changing the currency also changes the currency of your personal workspace. Works with
  
        a personal API token.
  
        '
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/MePatch'
            example:
              name: Ada Lovelace
              timezone: Europe/Berlin
              week_start: 1
      responses:
        '200':
          description: Your profile after the change.
          content:
            application/json:
              schema:
                type: object
                required:
                - user
                properties:
                  user:
                    $ref: '#/components/schemas/User'
              example:
                user:
                  id: 5d0c2f6e-3b1a-4c53-9a0e-7a1d6c2b9e41
                  email: ada@example.com
                  currency: USD
                  name: Ada Lovelace
                  timezone: Europe/Berlin
                  week_start: 1
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
    delete:
      x-badges: [{ name: Session only, color: "var(--scalar-color-orange)" }]
      operationId: deleteMe
      tags:
      - Account
      summary: Delete your account
      description: 'Permanently deletes your account: sessions, tokens, memberships, your personal workspace with everything
        in it,
  
        your private tasks, and team workspaces where you are the only member. Your entries in teams that stay remain
  
        with the team. Send your own email as `confirm`. Refused with `409 transfer_ownership` while you are the only
  
        owner of a team that has other members. Needs a signed-in session; API tokens are refused.
  
        '
      security:
      - session: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/MeDelete'
            example:
              confirm: ada@example.com
      responses:
        '200':
          $ref: '#/components/responses/Ok'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/SessionRequired'
        '409':
          description: You are the only owner of a team that has other members.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: You're the only owner of Studio North. Make someone else an owner first.
                code: transfer_ownership
  /v1/sessions:
    get:
      x-badges: [{ name: Session only, color: "var(--scalar-color-orange)" }]
      operationId: listSessions
      tags:
      - Account
      summary: List your sessions
      description: 'Lists the devices signed in to your account, most recently used first. The one making this request has
  
        `current: true`. Needs a signed-in session; API tokens are refused.
  
        '
      security:
      - session: []
      responses:
        '200':
          description: Your live sessions.
          content:
            application/json:
              schema:
                type: object
                required:
                - sessions
                properties:
                  sessions:
                    type: array
                    items:
                      $ref: '#/components/schemas/Session'
              example:
                sessions:
                - id: 0b5c7e44-1f43-4b0e-8c0a-52f4a8d3b7aa
                  name: Mac
                  created_at: 1759564800000
                  last_used_at: 1762156800000
                  expires_at: 1777708800000
                  current: true
                - id: c91d2a3b-6a58-4d7c-a3a4-0e6a6d1f9c10
                  name: Web - Safari
                  created_at: 1760000000000
                  last_used_at: 1761900000000
                  expires_at: 1777600000000
                  current: false
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/SessionRequired'
  /v1/sessions/{id}:
    delete:
      x-badges: [{ name: Session only, color: "var(--scalar-color-orange)" }]
      operationId: deleteSession
      tags:
      - Account
      summary: Sign out a session
      description: 'Ends one of your sessions, so that device has to sign in again. You can end the session you are using too.
  
        Needs a signed-in session; API tokens are refused.
  
        '
      security:
      - session: []
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
        description: The session id from `GET /v1/sessions`.
      responses:
        '200':
          $ref: '#/components/responses/Ok'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/SessionRequired'
        '404':
          description: You have no such session.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: Session not found.
  /v1/api-tokens:
    get:
      x-badges: [{ name: Session only, color: "var(--scalar-color-orange)" }]
      operationId: listApiTokens
      tags:
      - Account
      summary: List your API tokens
      description: 'Lists your active personal API tokens, newest first. A token''s secret is never shown again, only its name
        and
  
        the last 4 characters (`hint`). Needs a signed-in session; API tokens are refused.
  
        '
      security:
      - session: []
      responses:
        '200':
          description: Your active tokens.
          content:
            application/json:
              schema:
                type: object
                required:
                - tokens
                properties:
                  tokens:
                    type: array
                    items:
                      $ref: '#/components/schemas/ApiToken'
              example:
                tokens:
                - id: 7f3e1c52-90ab-4e3d-b6a1-2c8d5f0e9a77
                  name: Zapier
                  hint: wCq0
                  created_at: 1761000000000
                  last_used_at: 1762150000000
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/SessionRequired'
    post:
      x-badges: [{ name: Session only, color: "var(--scalar-color-orange)" }]
      operationId: createApiToken
      tags:
      - Account
      summary: Create an API token
      description: 'This is how you get a token for scripts, integrations and no-code tools. Give it a name that says where
        it is
  
        used. The response holds the token in `secret` (it starts with `hb_pat_`). **It is shown once**: Hourbrook
  
        keeps only a hash, so copy it now. Send it as `Authorization: Bearer <secret>` on every other endpoint. A token
  
        acts as you in every workspace you belong to, with your role there. It cannot manage sessions or tokens, delete
  
        the account, or do the actions marked **Session only**.
  
  
        You can have at most 20 active tokens. Revoke one with `DELETE /v1/api-tokens/{id}`. Creating a token needs a
  
        signed-in session, which you get from the dashboard (Settings, API tokens) or the app.
  
        '
      security:
      - session: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ApiTokenCreate'
            example:
              name: Zapier
      responses:
        '200':
          description: The new token. `secret` is shown only here.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiTokenCreated'
              example:
                token:
                  id: 7f3e1c52-90ab-4e3d-b6a1-2c8d5f0e9a77
                  name: Zapier
                  hint: wCq0
                  created_at: 1762156800000
                  last_used_at: null
                secret: hb_pat_k3J9xQvT2mWc8RzY1nL5aHdE7uBfPoGs4iVtXyN0wCq
        '400':
          description: The name is missing or invalid, or you already have 20 active tokens.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: You can have at most 20 API tokens. Revoke one first.
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/SessionRequired'
  /v1/api-tokens/{id}:
    delete:
      x-badges: [{ name: Session only, color: "var(--scalar-color-orange)" }]
      operationId: revokeApiToken
      tags:
      - Account
      summary: Revoke an API token
      description: 'Revokes one of your tokens. It stops working at once. Needs a signed-in session; API tokens are refused,
        so a
  
        token cannot revoke itself.
  
        '
      security:
      - session: []
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
        description: The token id from `GET /v1/api-tokens`.
      responses:
        '200':
          $ref: '#/components/responses/Ok'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/SessionRequired'
        '404':
          description: You have no such active token.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: Token not found.
  /v1/connected-apps:
    get:
      x-badges: [{ name: Session only, color: "var(--scalar-color-orange)" }]
      operationId: listConnectedApps
      tags:
      - Account
      summary: List connected AI apps
      description: 'Lists the AI apps you have allowed to use your Hourbrook over MCP (claude.ai, for example) and that still
  
        have access. Needs a signed-in session; API tokens are refused.
  
        '
      security:
      - session: []
      responses:
        '200':
          description: Your connected apps, newest first.
          content:
            application/json:
              schema:
                type: object
                required:
                - apps
                properties:
                  apps:
                    type: array
                    items:
                      $ref: '#/components/schemas/ConnectedApp'
              example:
                apps:
                - id: 3c6f0a1e-52b4-4f9a-8d1c-6e7b2a9d4f10
                  name: Claude
                  redirect_host: claude.ai
                  scopes:
                  - time:read
                  - time:write
                  created_at: 1761800000000
                  last_used_at: 1762150000000
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/SessionRequired'
  /v1/connected-apps/{id}:
    delete:
      x-badges: [{ name: Session only, color: "var(--scalar-color-orange)" }]
      operationId: revokeConnectedApp
      tags:
      - Account
      summary: Disconnect an AI app
      description: 'Revokes an app''s access. Its access and refresh tokens stop working at once. Needs a signed-in session;
        API
  
        tokens are refused.
  
        '
      security:
      - session: []
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
        description: The app id from `GET /v1/connected-apps`.
      responses:
        '200':
          $ref: '#/components/responses/Ok'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/SessionRequired'
        '404':
          description: You have no such connected app.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: App not found.
  /v1/auth/logout:
    post:
      x-badges: [{ name: Session only, color: "var(--scalar-color-orange)" }]
      operationId: logout
      tags:
      - Account
      summary: Sign out
      description: 'Ends the session making the request. Other sessions and API tokens keep working. Needs a signed-in session;
  
        API tokens are refused.
  
        '
      security:
      - session: []
      responses:
        '200':
          $ref: '#/components/responses/Ok'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/SessionRequired'
  /v1/auth/logout-all:
    post:
      x-badges: [{ name: Session only, color: "var(--scalar-color-orange)" }]
      operationId: logoutAll
      tags:
      - Account
      summary: Sign out everywhere
      description: 'Ends every session you have, this one included. API tokens keep working unless you send
  
        `revoke_tokens: true`, which revokes all of them too. The body is optional. Needs a signed-in session; API
  
        tokens are refused.
  
        '
      security:
      - session: []
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/LogoutAllRequest'
            example:
              revoke_tokens: true
      responses:
        '200':
          description: How many sessions ended, and how many tokens were revoked when you asked for that.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LogoutAllResponse'
              example:
                ok: true
                ended: 3
                revoked_tokens: 2
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/SessionRequired'
  /v1/auth/status:
    get:
      operationId: getAuthStatus
      tags:
      - Sign-in
      summary: Which sign-in methods are open
      description: Tells the apps which sign-in options to show. No sign-in needed. Used by the Mac app and the dashboard.
      x-internal: true
      security: []
      responses:
        '200':
          description: The sign-in options on this server.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AuthStatus'
              example:
                email_sign_in: true
                oauth:
                  github: true
                  google: true
                new_accounts: true
  /v1/auth/code:
    post:
      operationId: requestSignInCode
      tags:
      - Sign-in
      summary: Email a sign-in code
      description: 'Sends a 6 digit code (and a one-click link) to the address. The code lasts 10 minutes. Limits per hour:
        5 per
  
        email and address, 10 per email, 20 per address; past them it answers `429` with `Retry-After`. On a local dev
  
        server the code comes back in `devCode` instead of an email. Used by the Mac app and the dashboard.

        The public test accounts get `devCode` too, but only with the owner''s tester key in `X-Hourbrook-Test-Key`
        (otherwise `403`, `code: test_account`), and only the per-address limit applies to them.
  
        '
      x-internal: true
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AuthCodeRequest'
            example:
              email: ada@example.com
      responses:
        '200':
          description: The code was sent.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AuthCodeResponse'
              example:
                ok: true
        '400':
          $ref: '#/components/responses/BadRequest'
        '403':
          description: A public test account, without the owner's tester key (`X-Hourbrook-Test-Key`). No code is made.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: Test accounts are for Hourbrook's own testing. Sign in with your own email.
                code: test_account
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '502':
          description: The email could not be sent.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: Couldn't send the email. Try again in a minute.
        '503':
          description: Sign-in by email isn't open on this server yet.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: Sign-in by email isn't open yet.
                code: email_unavailable
  /v1/auth/verify:
    post:
      operationId: verifySignInCode
      tags:
      - Sign-in
      summary: Sign in with an emailed code
      description: 'Swaps the emailed code for a session token. A new address gets a new account. Each code allows 5 tries,
        and an
  
        address that gets 10 wrong codes in an hour needs a new code (`429` with `code: "new_code_required"` and
  
        `Retry-After: 0`). Used by the Mac app and the dashboard.
  
        '
      x-internal: true
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AuthVerifyRequest'
            example:
              email: ada@example.com
              code: '483920'
              device: Mac
      responses:
        '200':
          description: A session token and your profile.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AuthSession'
              example:
                token: Zm9vYmFyYmF6cXV4MTIzNDU2Nzg5MGFiY2RlZmdoaWo
                user:
                  id: 5d0c2f6e-3b1a-4c53-9a0e-7a1d6c2b9e41
                  email: ada@example.com
                  currency: USD
                  name: null
                  timezone: null
                  week_start: 1
                session_id: 0b5c7e44-1f43-4b0e-8c0a-52f4a8d3b7aa
        '400':
          $ref: '#/components/responses/BadRequest'
        '403':
          description: A public test account, without the owner's tester key (`X-Hourbrook-Test-Key`). No code is made.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: Test accounts are for Hourbrook's own testing. Sign in with your own email.
                code: test_account
        '429':
          $ref: '#/components/responses/TooManyRequests'
  /v1/auth/oauth/{provider}/start:
    get:
      operationId: startProviderSignIn
      tags:
      - Sign-in
      summary: Start sign-in with GitHub or Google
      description: 'A browser opens this URL. It answers `302` to the provider''s sign-in page. `challenge` is
  
        `base64url(sha256(verifier))` (43 characters) of a secret the app keeps and sends later to
  
        `POST /v1/auth/oauth/redeem`. When something is wrong the answer is a small HTML page (`text/html`) with the
  
        status `400`, `404`, `429` or `503`.
  
        '
      x-internal: true
      security: []
      parameters:
      - name: provider
        in: path
        required: true
        schema:
          type: string
          enum:
          - github
          - google
      - name: client
        in: query
        required: true
        schema:
          type: string
          enum:
          - mac
          - web
        description: '`mac` hands the result to the Mac app; `web` hands it back to the dashboard login page.'
      - name: challenge
        in: query
        required: true
        schema:
          type: string
          pattern: ^[A-Za-z0-9_-]{43}$
        description: base64url(sha256(client verifier)).
      responses:
        '302':
          description: Redirect to the provider's authorization page.
          headers:
            Location:
              schema:
                type: string
                format: uri
        '400':
          $ref: '#/components/responses/HtmlPage'
        '404':
          $ref: '#/components/responses/HtmlPage'
        '429':
          $ref: '#/components/responses/HtmlPage'
        '503':
          $ref: '#/components/responses/HtmlPage'
  /v1/auth/oauth/{provider}/callback:
    get:
      operationId: finishProviderSignIn
      tags:
      - Sign-in
      summary: Provider sign-in callback
      description: 'Where GitHub or Google sends the browser back. Not for you to call. For the web client it answers `302`
        to the
  
        dashboard login page with a ticket (or an error) in the URL fragment; for the Mac it shows an HTML page with a
  
        button that opens `hourbrook://signin?ticket=…`. Failures are HTML pages (`text/html`).
  
        '
      x-internal: true
      security: []
      parameters:
      - name: provider
        in: path
        required: true
        schema:
          type: string
          enum:
          - github
          - google
      - name: state
        in: query
        required: false
        schema:
          type: string
      - name: code
        in: query
        required: false
        schema:
          type: string
      - name: error
        in: query
        required: false
        schema:
          type: string
      - name: error_description
        in: query
        required: false
        schema:
          type: string
      responses:
        '200':
          description: An HTML page that hands the ticket to the Mac app.
          content:
            text/html:
              schema:
                type: string
        '302':
          description: Redirect to the dashboard login page, with `#oauth_ticket=…` or `#oauth_error=…`.
          headers:
            Location:
              schema:
                type: string
                format: uri
        '400':
          $ref: '#/components/responses/HtmlPage'
        '403':
          $ref: '#/components/responses/HtmlPage'
        '404':
          $ref: '#/components/responses/HtmlPage'
        '502':
          $ref: '#/components/responses/HtmlPage'
        '503':
          $ref: '#/components/responses/HtmlPage'
  /v1/auth/oauth/redeem:
    post:
      operationId: redeemProviderSignIn
      tags:
      - Sign-in
      summary: Finish sign-in with GitHub or Google
      description: 'Swaps the one-time ticket from the callback, plus the secret verifier whose hash started the flow, for a
  
        session token. A ticket lasts 2 minutes and one try; any failure answers the same `400 ticket_invalid`.
  
        Limited per address (`429`).
  
        '
      x-internal: true
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/OAuthSigninRedeem'
            example:
              ticket: Xk2mP9vQ4rT7wZ1aB5cD8eF3gH6jL0nS2uY4iO7pAqE
              client_verifier: dGhpcyBpcyBhIGxvbmcgcmFuZG9tIHZlcmlmaWVyIHN0cmluZzEyMw
      responses:
        '200':
          description: A session token and your profile.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AuthSession'
              example:
                token: Zm9vYmFyYmF6cXV4MTIzNDU2Nzg5MGFiY2RlZmdoaWo
                user:
                  id: 5d0c2f6e-3b1a-4c53-9a0e-7a1d6c2b9e41
                  email: ada@example.com
                  currency: USD
                  name: null
                  timezone: null
                  week_start: 1
                session_id: 0b5c7e44-1f43-4b0e-8c0a-52f4a8d3b7aa
        '400':
          description: The ticket or verifier is wrong, expired or already used.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: That sign-in link has expired or was already used. Start again.
                code: ticket_invalid
        '429':
          $ref: '#/components/responses/TooManyRequests'
  /v1/oauth/providers:
    get:
      operationId: listOAuthProviders
      tags:
      - Integrations
      summary: Which integrations are set up
      description: Tells the apps which providers this server can connect or sign in with. No sign-in needed.
      x-internal: true
      security: []
      responses:
        '200':
          description: What is configured.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OAuthProviders'
              example:
                connect:
                  linear: true
                  jira: false
                signin:
                  github: true
                  google: true
                github_device_client_id: null
  /v1/oauth/redeem:
    post:
      x-badges: [{ name: Session only, color: "var(--scalar-color-orange)" }]
      operationId: redeemOAuthConnect
      tags:
      - Integrations
      summary: Finish connecting Linear or Jira
      description: 'The Mac app swaps the one-time ticket from the callback, plus its secret verifier, for the provider''s tokens.
  
        The server keeps none of them. The ticket must belong to the signed-in user. Needs a signed-in session.
  
        '
      x-internal: true
      security:
      - session: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/OAuthSigninRedeem'
            example:
              ticket: Xk2mP9vQ4rT7wZ1aB5cD8eF3gH6jL0nS2uY4iO7pAqE
              client_verifier: dGhpcyBpcyBhIGxvbmcgcmFuZG9tIHZlcmlmaWVyIHN0cmluZzEyMw
      responses:
        '200':
          description: The provider's tokens and the connected account.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OAuthConnectTokens'
              example:
                provider: linear
                access_token: lin_oauth_example_access_token
                refresh_token: lin_oauth_example_refresh_token
                expires_at: 1764748800000
                scope: read
                account:
                  name: Ada Lovelace
                  email: ada@example.com
        '400':
          description: The ticket or verifier is wrong, expired or already used.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: That sign-in link has expired or was already used. Start again.
                code: ticket_invalid
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/SessionRequired'
        '429':
          $ref: '#/components/responses/TooManyRequests'
  /v1/oauth/{provider}/start:
    post:
      x-badges: [{ name: Session only, color: "var(--scalar-color-orange)" }]
      operationId: startOAuthConnect
      tags:
      - Integrations
      summary: Start connecting Linear or Jira
      description: 'Returns the provider''s authorization URL for the Mac app to open in a browser. `client_challenge` is
  
        `base64url(sha256(verifier))` (43 characters) of a secret the app keeps. The flow lasts 10 minutes. Limited to
  
        20 starts per user per hour. Needs a signed-in session.
  
        '
      x-internal: true
      security:
      - session: []
      parameters:
      - $ref: '#/components/parameters/OAuthConnectProvider'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/OAuthConnectStart'
            example:
              client: mac
              client_challenge: E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
      responses:
        '200':
          description: Where to send the person.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OAuthConnectStarted'
              example:
                authorize_url: https://linear.app/oauth/authorize?client_id=example&response_type=code&scope=read&state=Q2xh9vKp
                expires_at: 1762157400000
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/SessionRequired'
        '404':
          $ref: '#/components/responses/ProviderUnknown'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '503':
          $ref: '#/components/responses/ProviderNotConfigured'
  /v1/oauth/{provider}/refresh:
    post:
      x-badges: [{ name: Session only, color: "var(--scalar-color-orange)" }]
      operationId: refreshOAuthConnect
      tags:
      - Integrations
      summary: Refresh a Linear or Jira token
      description: 'Swaps a provider refresh token for a new set. The server keeps nothing. Answers `409 reconnect` when the
  
        provider no longer accepts the token and the person must connect again. Limited to 60 per user and provider
  
        per hour. Needs a signed-in session.
  
        '
      x-internal: true
      security:
      - session: []
      parameters:
      - $ref: '#/components/parameters/OAuthConnectProvider'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/OAuthRefreshRequest'
            example:
              refresh_token: lin_oauth_example_refresh_token
      responses:
        '200':
          description: The new tokens.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OAuthRefreshResponse'
              example:
                provider: linear
                access_token: lin_oauth_example_access_token_2
                refresh_token: lin_oauth_example_refresh_token_2
                expires_at: 1764835200000
                scope: read
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/SessionRequired'
        '404':
          $ref: '#/components/responses/ProviderUnknown'
        '409':
          description: The provider rejected the refresh token; connect again.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: Linear needs to be connected again.
                code: reconnect
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '502':
          description: The provider did not answer as expected.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: Linear didn't answer as expected. Try again in a minute.
                code: provider_error
        '503':
          $ref: '#/components/responses/ProviderNotConfigured'
  /v1/oauth/{provider}/callback:
    get:
      operationId: finishOAuthConnect
      tags:
      - Integrations
      summary: Linear or Jira connect callback
      description: 'Where Linear or Jira sends the browser back. Not for you to call. It answers an HTML page (`text/html`)
        with a
  
        button that opens `hourbrook://oauth?provider=…&ticket=…` in the Mac app. Failures are HTML pages too.
  
        '
      x-internal: true
      security: []
      parameters:
      - $ref: '#/components/parameters/OAuthConnectProvider'
      - name: state
        in: query
        required: false
        schema:
          type: string
      - name: code
        in: query
        required: false
        schema:
          type: string
      - name: error
        in: query
        required: false
        schema:
          type: string
      - name: error_description
        in: query
        required: false
        schema:
          type: string
      responses:
        '200':
          description: An HTML page that hands the ticket to the Mac app.
          content:
            text/html:
              schema:
                type: string
        '400':
          $ref: '#/components/responses/HtmlPage'
        '404':
          $ref: '#/components/responses/HtmlPage'
        '502':
          $ref: '#/components/responses/HtmlPage'
        '503':
          $ref: '#/components/responses/HtmlPage'
  /.well-known/oauth-authorization-server:
    get:
      operationId: getOAuthAuthorizationServerMetadata
      tags:
      - Sign-in
      summary: OAuth server metadata (MCP)
      description: RFC 8414 metadata for the OAuth server that MCP clients use. No sign-in needed.
      x-internal: true
      security: []
      responses:
        '200':
          description: The metadata document.
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
              example:
                issuer: https://hourbrook-api.mlazarev.workers.dev
                authorization_endpoint: https://hourbrook-api.mlazarev.workers.dev/oauth/authorize
                token_endpoint: https://hourbrook-api.mlazarev.workers.dev/oauth/token
                registration_endpoint: https://hourbrook-api.mlazarev.workers.dev/oauth/register
                revocation_endpoint: https://hourbrook-api.mlazarev.workers.dev/oauth/revoke
                response_types_supported:
                - code
                grant_types_supported:
                - authorization_code
                - refresh_token
                code_challenge_methods_supported:
                - S256
                scopes_supported:
                - time:read
                - time:write
        '503':
          $ref: '#/components/responses/NotConfigured'
  /.well-known/oauth-protected-resource:
    get:
      operationId: getOAuthProtectedResourceMetadata
      tags:
      - Sign-in
      summary: Protected resource metadata (MCP)
      description: RFC 9728 metadata for the `/mcp` endpoint. No sign-in needed. Same document as the `/mcp` variant below.
      x-internal: true
      security: []
      responses:
        '200':
          $ref: '#/components/responses/ProtectedResourceMetadata'
        '503':
          $ref: '#/components/responses/NotConfigured'
  /.well-known/oauth-protected-resource/mcp:
    get:
      operationId: getMcpProtectedResourceMetadata
      tags:
      - Sign-in
      summary: Protected resource metadata for /mcp
      description: RFC 9728 metadata for the `/mcp` endpoint, at the path its `WWW-Authenticate` challenge points to.
      x-internal: true
      security: []
      responses:
        '200':
          $ref: '#/components/responses/ProtectedResourceMetadata'
        '503':
          $ref: '#/components/responses/NotConfigured'
  /oauth/register:
    post:
      operationId: registerMcpClient
      tags:
      - Sign-in
      summary: Register an MCP client
      description: 'RFC 7591 dynamic client registration for MCP apps. Send JSON. Redirect URIs must be `https` (or `http` on
  
        localhost), 1 to 5 of them, without fragments. Errors use the OAuth shape `{ error, error_description }`.
  
        '
      x-internal: true
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/McpClientRegistration'
            example:
              client_name: Claude
              redirect_uris:
              - https://claude.ai/api/mcp/auth_callback
              token_endpoint_auth_method: none
      responses:
        '201':
          description: The registered client.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/McpClientRegistered'
              example:
                client_id: 3c6f0a1e-52b4-4f9a-8d1c-6e7b2a9d4f10
                client_id_issued_at: 1762156800
                client_name: Claude
                redirect_uris:
                - https://claude.ai/api/mcp/auth_callback
                grant_types:
                - authorization_code
                - refresh_token
                response_types:
                - code
                token_endpoint_auth_method: none
        '400':
          $ref: '#/components/responses/OAuthErrorResponse'
        '429':
          $ref: '#/components/responses/OAuthErrorResponse'
        '503':
          $ref: '#/components/responses/NotConfigured'
  /oauth/authorize:
    get:
      operationId: authorizeMcpClient
      tags:
      - Sign-in
      summary: Start an MCP authorization
      description: 'The authorization endpoint (OAuth 2.1 code flow with PKCE S256). A browser opens it. A good request answers
  
        `302` to the dashboard consent page. A problem with the client or redirect URI shows an HTML page (`text/html`);
  
        other problems redirect back to the client with `error`.
  
        '
      x-internal: true
      security: []
      parameters:
      - name: response_type
        in: query
        required: true
        schema:
          type: string
          enum:
          - code
      - name: client_id
        in: query
        required: true
        schema:
          type: string
      - name: redirect_uri
        in: query
        required: false
        schema:
          type: string
          format: uri
      - name: state
        in: query
        required: false
        schema:
          type: string
          maxLength: 512
      - name: scope
        in: query
        required: false
        schema:
          type: string
        description: 'Space separated: `time:read`, `time:write`. Both when omitted.'
      - name: code_challenge
        in: query
        required: true
        schema:
          type: string
      - name: code_challenge_method
        in: query
        required: true
        schema:
          type: string
          enum:
          - S256
      - name: resource
        in: query
        required: false
        schema:
          type: string
          format: uri
        description: The `/mcp` URL of this server.
      responses:
        '302':
          description: Redirect to the consent page, or back to the client with an OAuth `error`.
          headers:
            Location:
              schema:
                type: string
                format: uri
        '400':
          $ref: '#/components/responses/HtmlPage'
        '429':
          $ref: '#/components/responses/HtmlPage'
        '503':
          $ref: '#/components/responses/HtmlPage'
  /oauth/token:
    post:
      operationId: exchangeMcpToken
      tags:
      - Sign-in
      summary: MCP token endpoint
      description: 'Swaps an authorization code (with its PKCE `code_verifier`) or a refresh token for a 1 hour access token
        and a
  
        30 day refresh token. The body is **form encoded** (`application/x-www-form-urlencoded`), not JSON. Client
  
        authentication is HTTP Basic or `client_id` (and `client_secret`) in the form. Refresh tokens rotate; reusing
  
        an old one revokes the grant. Errors use the OAuth shape.
  
        '
      x-internal: true
      security: []
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/McpTokenRequest'
            example:
              grant_type: authorization_code
              client_id: 3c6f0a1e-52b4-4f9a-8d1c-6e7b2a9d4f10
              code: Qm4xS7vB2nK9pL6wT3yH8cJ1dF5gR0aZ2eU4iO7tNxE
              code_verifier: dGhpcyBpcyBhIGxvbmcgcmFuZG9tIHZlcmlmaWVyIHN0cmluZzEyMw
              redirect_uri: https://claude.ai/api/mcp/auth_callback
      responses:
        '200':
          description: The tokens.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/McpTokenResponse'
              example:
                access_token: hb_mcp_Zm9vYmFyYmF6cXV4MTIzNDU2Nzg5MGFiY2RlZmdoaWo
                token_type: Bearer
                expires_in: 3600
                refresh_token: hb_mcpr_Zm9vYmFyYmF6cXV4MTIzNDU2Nzg5MGFiY2RlZmdoaWo
                scope: time:read time:write
        '400':
          $ref: '#/components/responses/OAuthErrorResponse'
        '401':
          $ref: '#/components/responses/OAuthErrorResponse'
        '429':
          $ref: '#/components/responses/OAuthErrorResponse'
        '503':
          $ref: '#/components/responses/OAuthErrorResponse'
  /oauth/revoke:
    post:
      operationId: revokeMcpToken
      tags:
      - Sign-in
      summary: Revoke an MCP token
      description: 'RFC 7009 revocation. Revoking an access or refresh token ends the whole grant. The body is **form encoded**
  
        with `token`, plus the same client authentication as the token endpoint. Answers `200` with an empty body,
  
        also for tokens it does not know.
  
        '
      x-internal: true
      security: []
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/McpRevokeRequest'
            example:
              client_id: 3c6f0a1e-52b4-4f9a-8d1c-6e7b2a9d4f10
              token: hb_mcpr_Zm9vYmFyYmF6cXV4MTIzNDU2Nzg5MGFiY2RlZmdoaWo
      responses:
        '200':
          description: Done. The body is empty.
        '400':
          $ref: '#/components/responses/OAuthErrorResponse'
        '401':
          $ref: '#/components/responses/OAuthErrorResponse'
  /v1/mcp/consent/info:
    post:
      x-badges: [{ name: Session only, color: "var(--scalar-color-orange)" }]
      operationId: getMcpConsentInfo
      tags:
      - Sign-in
      summary: Read an MCP consent request
      description: 'The dashboard''s connect page reads what an app is asking for, using the handle from the URL fragment. Needs
        a
  
        signed-in session. Limited to 60 per user per hour.
  
        '
      x-internal: true
      security:
      - session: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/McpConsentRequest'
            example:
              request: Zk8nR3vT6mQ1xW4yB7cD0eF5gH2jL9pS4uA6iO8tNqE
      responses:
        '200':
          description: The app, the scopes and your account.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/McpConsentInfo'
              example:
                client:
                  name: Claude
                  redirect_host: claude.ai
                  redirect_uri: https://claude.ai/api/mcp/auth_callback
                scopes:
                - time:read
                - time:write
                account:
                  email: ada@example.com
                  name: Ada Lovelace
                expires_at: 1762157400000
        '400':
          description: The request expired or was already answered.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: This request has expired or was already answered. Start again from the app you were connecting.
                code: request_invalid
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/SessionRequired'
        '429':
          $ref: '#/components/responses/TooManyRequests'
  /v1/mcp/consent:
    post:
      x-badges: [{ name: Session only, color: "var(--scalar-color-orange)" }]
      operationId: decideMcpConsent
      tags:
      - Sign-in
      summary: Allow or deny an MCP app
      description: 'Answers a consent request once. The reply holds the client''s redirect URL, with a one-time `code` when
        allowed
  
        or `error=access_denied` when denied, for the page to open. Needs a signed-in session. Limited to 60 per user
  
        per hour.
  
        '
      x-internal: true
      security:
      - session: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/McpConsentDecision'
            example:
              request: Zk8nR3vT6mQ1xW4yB7cD0eF5gH2jL9pS4uA6iO8tNqE
              allow: true
      responses:
        '200':
          description: Where to send the browser next.
          content:
            application/json:
              schema:
                type: object
                required:
                - redirect_to
                properties:
                  redirect_to:
                    type: string
                    format: uri
              example:
                redirect_to: https://claude.ai/api/mcp/auth_callback?code=Qm4xS7vB2nK9pL6wT3yH8cJ1dF5gR0aZ2eU4iO7tNxE&iss=https%3A%2F%2Fhourbrook-api.mlazarev.workers.dev
        '400':
          description: '`allow` is not a boolean, or the request expired or was already answered.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: This request has expired or was already answered. Start again from the app you were connecting.
                code: request_invalid
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/SessionRequired'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '503':
          $ref: '#/components/responses/NotConfigured'
  /mcp:
    post:
      operationId: mcpPost
      tags:
      - Sign-in
      summary: MCP endpoint
      description: 'The MCP Streamable HTTP endpoint. It takes JSON-RPC messages and the access tokens (`hb_mcp_…`) the OAuth
  
        endpoints above issue. Sessions and personal API tokens do not work here. Without a valid token it answers
  
        `401` with a `WWW-Authenticate` challenge.
  
        '
      x-internal: true
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
            example:
              jsonrpc: '2.0'
              id: 1
              method: tools/list
      responses:
        '200':
          description: A JSON-RPC response.
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
        '202':
          description: A notification or response was accepted. The body is empty.
        '400':
          $ref: '#/components/responses/JsonRpcError'
        '401':
          $ref: '#/components/responses/JsonRpcError'
        '403':
          $ref: '#/components/responses/JsonRpcError'
        '413':
          $ref: '#/components/responses/JsonRpcError'
        '429':
          $ref: '#/components/responses/JsonRpcError'
        '503':
          $ref: '#/components/responses/JsonRpcError'
    get:
      operationId: mcpGet
      tags:
      - Sign-in
      summary: MCP endpoint (GET not supported)
      description: 'Always answers `405` with `Allow: POST`. There are no server-initiated streams.'
      x-internal: true
      security: []
      responses:
        '405':
          $ref: '#/components/responses/MethodNotAllowed'
    delete:
      operationId: mcpDelete
      tags:
      - Sign-in
      summary: MCP endpoint (DELETE not supported)
      description: 'Always answers `405` with `Allow: POST`. There are no MCP sessions to end.'
      x-internal: true
      security: []
      responses:
        '405':
          $ref: '#/components/responses/MethodNotAllowed'
  /v1/entries:
    get:
      operationId: listEntries
      tags:
      - Time entries
      summary: List entries
      description: 'Returns live (not deleted) entries that overlap the range `from` to `to`, oldest first. Without
  
        `workspace_id` you get your own entries in every workspace. With it, what you see depends on your role there:
  
        owners and admins see everyone, team leads see themselves and the people they lead, members see only their own.
  
        The range is at most 400 days. Without `limit` everything in it comes back at once. With `limit` you get
  
        pages: send the answer''s `next_cursor` back as `cursor` (with the same other parameters) until it is `null`.
  
        Entries are sorted by `started_at`, then `id`, then author, so pages never skip or repeat an entry that
  
        doesn''t change meanwhile.
  
        '
      parameters:
      - $ref: '#/components/parameters/FromQuery'
      - $ref: '#/components/parameters/ToQuery'
      - name: workspace_id
        in: query
        required: false
        schema:
          type: string
        description: 'Look at one workspace instead of only your own entries. Unknown, or a workspace you aren''t in, answers
  
          `404`.
  
          '
      - name: user_id
        in: query
        required: false
        schema:
          type: string
        description: 'Only this person''s entries. It does nothing without `workspace_id`. A person outside what you may see
  
          gives an empty list, not an error.
  
          '
      - $ref: '#/components/parameters/ExternalIdQuery'
      - name: limit
        in: query
        required: false
        schema:
          type: integer
          minimum: 1
          maximum: 1000
        description: At most this many entries, and a `next_cursor` in the answer. Default no limit.
      - name: cursor
        in: query
        required: false
        schema:
          type: string
        description: The `next_cursor` of the previous page, as it is. It means nothing else; an altered one is a `400`.
      responses:
        '200':
          description: The entries in the range (one page of them with `limit`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EntryListResponse'
              example:
                entries:
                - id: 5c0f3a0e-6a0b-4c3a-9d55-0d8f4b7a1e01
                  project_id: 9a1b7c52-3a3d-4e0c-8c1e-2f6f5d0a7b10
                  title: Landing page copy
                  started_at: 1762156800000
                  ended_at: 1762164000000
                  source: manual
                  client_updated_at: 1762164100000
                  deleted_at: null
                  notes: First pass, sent to Dana
                  billable: true
                  tag_ids:
                  - 0b6d1f4e-7d52-4a9b-a3c1-6e8d2f9b4c20
                  invoice_id: null
                  billed_at: null
                  user_id: 2f7d9e18-5b6c-4f0a-9e31-7a4c8d1b3e55
                  workspace_id: 71c4e0aa-9b2d-4d7e-8f13-5a6b0c9d2e44
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
    post:
      operationId: createEntry
      tags:
      - Time entries
      summary: Create an entry
      description: 'Logs time for yourself, or for someone else with `user_id`. Owners and admins may log time for any member
        of
  
        the project''s workspace; team leads for the people they lead. An entry for someone else stays in the
  
        project''s workspace. Send your own `id` to make the call safe to retry: a second call with the same id
  
        answers `409`. A project with `requires_tags` or `requires_notes` refuses an entry without them.
  
        '
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/EntryCreate'
            example:
              id: 5c0f3a0e-6a0b-4c3a-9d55-0d8f4b7a1e01
              project_id: 9a1b7c52-3a3d-4e0c-8c1e-2f6f5d0a7b10
              title: Landing page copy
              started_at: 1762156800000
              ended_at: 1762164000000
              notes: First pass, sent to Dana
              billable: true
              tag_ids:
              - 0b6d1f4e-7d52-4a9b-a3c1-6e8d2f9b4c20
      responses:
        '200':
          description: The new entry.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EntryResponse'
              example:
                entry:
                  id: 5c0f3a0e-6a0b-4c3a-9d55-0d8f4b7a1e01
                  project_id: 9a1b7c52-3a3d-4e0c-8c1e-2f6f5d0a7b10
                  title: Landing page copy
                  started_at: 1762156800000
                  ended_at: 1762164000000
                  source: manual
                  client_updated_at: 1762164100000
                  deleted_at: null
                  notes: First pass, sent to Dana
                  billable: true
                  tag_ids:
                  - 0b6d1f4e-7d52-4a9b-a3c1-6e8d2f9b4c20
                  invoice_id: null
                  billed_at: null
                  user_id: 2f7d9e18-5b6c-4f0a-9e31-7a4c8d1b3e55
                  workspace_id: 71c4e0aa-9b2d-4d7e-8f13-5a6b0c9d2e44
        '400':
          description: 'The input is invalid: a bad id, a missing title, times that don''t fit, an unknown project or tag, or
  
            (with `user_id`) a person who isn''t in the workspace.
  
            '
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: Entry is longer than 7 days.
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: 'You may not log time for that person, or you are no longer a member of the workspace the entry belongs
  
            to.
  
            '
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: You can only log time for people you manage.
        '409':
          description: An entry with this id already exists for that person, or the project was deleted.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: An entry with this id already exists.
        '422':
          $ref: '#/components/responses/Unprocessable'
  /v1/entries/bulk:
    post:
      operationId: bulkEditEntries
      tags:
      - Time entries
      summary: Change many entries
      description: 'Applies one change to up to 500 entries, which may belong to up to 25 people. Each entry is checked on its
  
        own: you can change your own, those in workspaces you administer, and those of people you lead. An entry that
  
        fails goes into `rejected` with a reason and the others still go through. Marking entries billed or unbilled
  
        is for owners and admins of the entry''s workspace only. The request itself answers `200` even when every
  
        entry was rejected.
  
        '
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BulkEntriesRequest'
            example:
              ids:
              - user_id: 2f7d9e18-5b6c-4f0a-9e31-7a4c8d1b3e55
                id: 5c0f3a0e-6a0b-4c3a-9d55-0d8f4b7a1e01
              - user_id: 2f7d9e18-5b6c-4f0a-9e31-7a4c8d1b3e55
                id: 8e4a2b6c-1d3f-4a7b-b5c9-3f0e6d2a8b17
              set:
                add_tag_ids:
                - 0b6d1f4e-7d52-4a9b-a3c1-6e8d2f9b4c20
      responses:
        '200':
          description: 'The current state of every entry that was in scope and not rejected, and the ones that were rejected.
  
            '
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BulkEntriesResponse'
              example:
                entries:
                - id: 5c0f3a0e-6a0b-4c3a-9d55-0d8f4b7a1e01
                  project_id: 9a1b7c52-3a3d-4e0c-8c1e-2f6f5d0a7b10
                  title: Landing page copy
                  started_at: 1762156800000
                  ended_at: 1762164000000
                  source: manual
                  client_updated_at: 1762170000000
                  deleted_at: null
                  notes: null
                  billable: true
                  tag_ids:
                  - 0b6d1f4e-7d52-4a9b-a3c1-6e8d2f9b4c20
                  invoice_id: null
                  billed_at: null
                  user_id: 2f7d9e18-5b6c-4f0a-9e31-7a4c8d1b3e55
                  workspace_id: 71c4e0aa-9b2d-4d7e-8f13-5a6b0c9d2e44
                rejected:
                - user_id: 2f7d9e18-5b6c-4f0a-9e31-7a4c8d1b3e55
                  id: 8e4a2b6c-1d3f-4a7b-b5c9-3f0e6d2a8b17
                  reason: invoiced
        '400':
          description: 'The request is invalid: `ids` is empty or over 500, covers more than 25 people, `set` changes nothing,
            or
  
            `set` mixes `billed` or `delete` with other changes.
  
            '
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: ids must be a list of 1 to 500 {user_id, id} pairs.
        '401':
          $ref: '#/components/responses/Unauthorized'
  /v1/entries/{id}:
    patch:
      operationId: updateEntry
      tags:
      - Time entries
      summary: Change an entry
      description: 'Changes the fields you send; the rest stay as they are. You can edit your own entries, entries in
  
        workspaces you administer, and entries of people you lead (name the author with `user_id`). Someone else''s
  
        entry can''t move to a project in another workspace. An entry on an invoice, or marked billed, is locked and
  
        answers `409`.
  
        '
      parameters:
      - $ref: '#/components/parameters/IdPath'
      - $ref: '#/components/parameters/EntryAuthorQuery'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/EntryPatch'
            example:
              title: Landing page copy, second pass
              ended_at: 1762167600000
              tag_ids:
              - 0b6d1f4e-7d52-4a9b-a3c1-6e8d2f9b4c20
      responses:
        '200':
          description: The entry after the change.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EntryResponse'
              example:
                entry:
                  id: 5c0f3a0e-6a0b-4c3a-9d55-0d8f4b7a1e01
                  project_id: 9a1b7c52-3a3d-4e0c-8c1e-2f6f5d0a7b10
                  title: Landing page copy, second pass
                  started_at: 1762156800000
                  ended_at: 1762167600000
                  source: manual
                  client_updated_at: 1762167700000
                  deleted_at: null
                  notes: First pass, sent to Dana
                  billable: true
                  tag_ids:
                  - 0b6d1f4e-7d52-4a9b-a3c1-6e8d2f9b4c20
                  invoice_id: null
                  billed_at: null
                  user_id: 2f7d9e18-5b6c-4f0a-9e31-7a4c8d1b3e55
                  workspace_id: 71c4e0aa-9b2d-4d7e-8f13-5a6b0c9d2e44
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: 'You may not change this person''s entries, or the entry is in a workspace its author has left.
  
            '
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: Only workspace admins can do that.
        '404':
          description: No such entry, or it is deleted, or it is in a workspace you aren't in.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: Entry not found.
        '409':
          description: 'The entry is on an invoice (no code), marked billed (`code: "billed"`), or several entries share this
            id
  
            and `user_id` is missing (`code: "ambiguous"`). It is also `409` if you move it to a deleted project.
  
            '
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: This entry is marked as billed. Unmark it to edit it.
                code: billed
        '422':
          $ref: '#/components/responses/Unprocessable'
    delete:
      operationId: deleteEntry
      tags:
      - Time entries
      summary: Delete an entry
      description: 'Deletes the entry; it stays as a row with `deleted_at` set so other devices learn about it. Who may delete
  
        is the same as for changing an entry. Deleting an entry that is already deleted answers `200`. An entry on
  
        an invoice, or marked billed, can''t be deleted.
  
        '
      parameters:
      - $ref: '#/components/parameters/IdPath'
      - $ref: '#/components/parameters/EntryAuthorQuery'
      responses:
        '200':
          $ref: '#/components/responses/Ok'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: You may not delete this person's entries.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: Only workspace admins can do that.
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
  /v1/entries/{id}/history:
    get:
      operationId: getEntryHistory
      tags:
      - Time entries
      summary: Get an entry's history
      description: 'Lists what changed on an entry, oldest first. Anyone who may change the entry may read its history. You
        only
  
        see history written in workspaces you belong to. Moving an entry to another workspace starts a new history
  
        there, so a personal entry moved into a team shows the team nothing from before the move.
  
        '
      parameters:
      - $ref: '#/components/parameters/IdPath'
      - $ref: '#/components/parameters/EntryAuthorQuery'
      responses:
        '200':
          description: The history, oldest first.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EntryHistoryResponse'
              example:
                history:
                - id: 3b9e7a52-0c1d-4f6a-8e24-9d5c7b1a0f63
                  action: create
                  changes:
                    project_id:
                    - null
                    - 9a1b7c52-3a3d-4e0c-8c1e-2f6f5d0a7b10
                    title:
                    - null
                    - Landing page copy
                    notes:
                    - null
                    - null
                    started_at:
                    - null
                    - 1762156800000
                    ended_at:
                    - null
                    - 1762164000000
                    billable:
                    - null
                    - true
                    tag_ids:
                    - null
                    - []
                  at: 1762164100000
                  actor:
                    user_id: 2f7d9e18-5b6c-4f0a-9e31-7a4c8d1b3e55
                    name: Sam Rivera
                    email: sam@example.com
                - id: 6d2c8f41-7e9a-4b30-a1d5-4c0e9f3b8a72
                  action: update
                  changes:
                    ended_at:
                    - 1762164000000
                    - 1762167600000
                  at: 1762167700000
                  actor:
                    user_id: 2f7d9e18-5b6c-4f0a-9e31-7a4c8d1b3e55
                    name: Sam Rivera
                    email: sam@example.com
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: You may not manage this person's entries.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: Only workspace admins can do that.
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          description: 'Several entries have this id and `user_id` is missing (`code: "ambiguous"`).'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: Several entries have this id. Send user_id to pick one.
                code: ambiguous
  /v1/timer:
    get:
      operationId: getTimer
      tags:
      - Timer
      summary: Get the running timer
      description: 'Returns your running timer, or `null` when none runs. The timer is the same on all your devices and private
  
        to you. `project_gone` is `true` when its project was deleted or you left its workspace: stopping then needs
  
        another `project_id`.
  
        '
      responses:
        '200':
          description: The timer, or `null`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TimerGetResponse'
              example:
                timer:
                  id: c4a8e1d7-2b5f-4930-8a6e-1f7d0b9c3e28
                  workspace_id: 71c4e0aa-9b2d-4d7e-8f13-5a6b0c9d2e44
                  project_id: 9a1b7c52-3a3d-4e0c-8c1e-2f6f5d0a7b10
                  title: Landing page copy
                  notes: null
                  billable: null
                  tag_ids: []
                  task_id: null
                  started_at: 1762156800000
                  device: Mac
                  updated_at: 1762156800000
        '401':
          $ref: '#/components/responses/Unauthorized'
    patch:
      operationId: updateTimer
      tags:
      - Timer
      summary: Change the running timer
      description: 'Changes the fields you send on the running timer. A project''s "requires tags" and "requires notes" rules
        are
  
        not checked here, only when you stop. Moving the timer to a project in another workspace needs `tag_ids` for
  
        that workspace, or the old tags answer `400 Unknown tag`. Counts toward 60 timer writes a minute.
  
        '
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TimerPatch'
            example:
              title: Landing page copy, headline
              tag_ids:
              - 0b6d1f4e-7d52-4a9b-a3c1-6e8d2f9b4c20
      responses:
        '200':
          description: The timer after the change.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TimerResponse'
              example:
                timer:
                  id: c4a8e1d7-2b5f-4930-8a6e-1f7d0b9c3e28
                  workspace_id: 71c4e0aa-9b2d-4d7e-8f13-5a6b0c9d2e44
                  project_id: 9a1b7c52-3a3d-4e0c-8c1e-2f6f5d0a7b10
                  title: Landing page copy, headline
                  notes: null
                  billable: null
                  tag_ids:
                  - 0b6d1f4e-7d52-4a9b-a3c1-6e8d2f9b4c20
                  task_id: null
                  started_at: 1762156800000
                  device: Mac
                  updated_at: 1762158000000
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: No timer is running.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: No timer is running.
                code: no_timer
        '409':
          description: 'The `id` you sent is not the running timer''s (`code: "timer_changed"`: another device replaced it),
            or
  
            the new project was deleted.
  
            '
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: The timer changed on another device. Refresh and try again.
                code: timer_changed
        '429':
          $ref: '#/components/responses/TooManyRequests'
    delete:
      operationId: discardTimer
      tags:
      - Timer
      summary: Discard the running timer
      description: 'Throws the running timer away and logs nothing. Answers `200` even when no timer runs. Pass `id` to discard
  
        only if the running timer is that one, so a device doesn''t discard a timer another device started. Counts
  
        toward 60 timer writes a minute.
  
        '
      parameters:
      - name: id
        in: query
        required: false
        schema:
          type: string
          format: uuid
        description: Discard only the timer with this id. Without it, whatever timer runs is discarded.
      responses:
        '200':
          description: Done. There is no timer now.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TimerDiscardResponse'
              example:
                ok: true
                timer: null
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/TooManyRequests'
  /v1/timer/start:
    post:
      operationId: startTimer
      tags:
      - Timer
      summary: Start the timer
      description: 'Starts a timer on a project. You can have one timer at a time, across all devices: if one already runs,
        this
  
        answers `409 timer_running` and returns the running timer. The timer''s `id` becomes the id of the entry when
  
        you stop it. Counts toward 60 timer writes a minute.
  
        '
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TimerStart'
            example:
              project_id: 9a1b7c52-3a3d-4e0c-8c1e-2f6f5d0a7b10
              title: Landing page copy
              device: Mac
      responses:
        '200':
          description: The new timer.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TimerResponse'
              example:
                timer:
                  id: c4a8e1d7-2b5f-4930-8a6e-1f7d0b9c3e28
                  workspace_id: 71c4e0aa-9b2d-4d7e-8f13-5a6b0c9d2e44
                  project_id: 9a1b7c52-3a3d-4e0c-8c1e-2f6f5d0a7b10
                  title: Landing page copy
                  notes: null
                  billable: null
                  tag_ids: []
                  task_id: null
                  started_at: 1762156800000
                  device: Mac
                  updated_at: 1762156800000
        '400':
          description: 'The input is invalid: no project, an unknown project, tag or task, or a `started_at` in the future or
  
            more than 7 days ago.
  
            '
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: Pick a project for the timer.
        '401':
          $ref: '#/components/responses/Unauthorized'
        '409':
          description: 'A timer already runs (`code: "timer_running"`, with the running `timer`), the project was deleted, or
            an
  
            entry with this `id` already exists.
  
            '
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TimerRunningConflict'
              example:
                error: A timer is already running. Stop it first.
                code: timer_running
                timer:
                  id: c4a8e1d7-2b5f-4930-8a6e-1f7d0b9c3e28
                  workspace_id: 71c4e0aa-9b2d-4d7e-8f13-5a6b0c9d2e44
                  project_id: 9a1b7c52-3a3d-4e0c-8c1e-2f6f5d0a7b10
                  title: Landing page copy
                  notes: null
                  billable: null
                  tag_ids: []
                  task_id: null
                  started_at: 1762156800000
                  device: Mac
                  updated_at: 1762156800000
        '429':
          $ref: '#/components/responses/TooManyRequests'
  /v1/timer/stop:
    post:
      operationId: stopTimer
      tags:
      - Timer
      summary: Stop the timer
      description: 'Stops the running timer and logs it as an entry with the timer''s id, so stopping twice can''t log twice.
        The
  
        body is optional. A timer stopped less than a minute after it started logs nothing and comes back with
  
        `too_short: true`. A timer left running for more than 7 days stops at 7 days. If the project needs tags or
  
        notes you get `422` and the timer keeps running: add them with `PATCH /v1/timer` and stop again. If the
  
        timer''s project is gone you get `409 timer_project_gone`: stop again with a `project_id`, or discard the
  
        timer. Counts toward 60 timer writes a minute.
  
        '
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TimerStop'
            example:
              ended_at: 1762164000000
      responses:
        '200':
          description: The entry that was logged, or `null` when the timer was too short.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TimerStopResponse'
              example:
                entry:
                  id: c4a8e1d7-2b5f-4930-8a6e-1f7d0b9c3e28
                  project_id: 9a1b7c52-3a3d-4e0c-8c1e-2f6f5d0a7b10
                  title: Landing page copy
                  started_at: 1762156800000
                  ended_at: 1762164000000
                  source: manual
                  client_updated_at: 1762164000500
                  deleted_at: null
                  notes: null
                  billable: null
                  tag_ids: []
                  invoice_id: null
                  billed_at: null
                  user_id: 2f7d9e18-5b6c-4f0a-9e31-7a4c8d1b3e55
                  workspace_id: 71c4e0aa-9b2d-4d7e-8f13-5a6b0c9d2e44
                timer: null
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: No timer is running.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: No timer is running.
                code: no_timer
        '409':
          description: 'The `id` you sent is not the running timer''s (`code: "timer_changed"`), or the timer''s project is
            gone
  
            (`code: "timer_project_gone"`).
  
            '
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: This timer's project is gone (deleted, or you left its workspace). Pick another project for the time,
                  or discard it.
                code: timer_project_gone
        '422':
          $ref: '#/components/responses/Unprocessable'
        '429':
          $ref: '#/components/responses/TooManyRequests'
  /v1/search:
    get:
      operationId: search
      tags:
      - Search
      summary: Search your entries and tasks
      description: 'Finds your own entries (by title or notes) and your own tasks (by title, notes or issue key) that contain
        the
  
        text. It never looks at anyone else''s rows. Newest first. Letters match without regard to case. Limited to
  
        120 searches a minute.
  
        '
      parameters:
      - name: q
        in: query
        required: true
        schema:
          type: string
          minLength: 1
          maxLength: 100
        description: The text to look for, 1 to 100 characters after trimming.
      - name: limit
        in: query
        required: false
        schema:
          type: integer
          minimum: 1
          maximum: 200
          default: 50
        description: How many entries and how many tasks to return, at most.
      responses:
        '200':
          description: The matches.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SearchResponse'
              example:
                entries:
                - id: 5c0f3a0e-6a0b-4c3a-9d55-0d8f4b7a1e01
                  project_id: 9a1b7c52-3a3d-4e0c-8c1e-2f6f5d0a7b10
                  title: Landing page copy
                  started_at: 1762156800000
                  ended_at: 1762164000000
                  source: manual
                  client_updated_at: 1762164100000
                  deleted_at: null
                  notes: null
                  billable: true
                  tag_ids: []
                  invoice_id: null
                  billed_at: null
                  user_id: 2f7d9e18-5b6c-4f0a-9e31-7a4c8d1b3e55
                  workspace_id: 71c4e0aa-9b2d-4d7e-8f13-5a6b0c9d2e44
                tasks: []
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          description: Too many searches.
          headers:
            Retry-After:
              $ref: '#/components/headers/Retry-After'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: Too many searches. Wait a moment, then try again.
                code: rate_limited
  /v1/tags/usage:
    get:
      operationId: getTagUsage
      tags:
      - Tags
      summary: Get tag usage by week
      description: 'Returns your own logged time per tag for each of the last weeks, ending with the current one. Weeks start
        on
  
        your `week_start` and run in your time zone. An entry counts in the week it starts, and an entry with several
  
        tags counts for each of them. It never includes anyone else''s time.
  
        '
      parameters:
      - name: weeks
        in: query
        required: false
        schema:
          type: integer
          minimum: 1
          maximum: 26
          default: 8
        description: How many weeks to return.
      - name: workspace_id
        in: query
        required: false
        schema:
          type: string
          format: uuid
        description: Count only your entries in this workspace.
      - name: tz
        in: query
        required: false
        schema:
          type: string
        description: An IANA time zone to use instead of yours. An unknown name is ignored.
        example: Europe/Berlin
      responses:
        '200':
          description: Seconds per tag per week.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TagUsageResponse'
              example:
                weeks:
                - 1761552000000
                - 1762156800000
                week_days:
                - '2025-10-27'
                - '2025-11-03'
                tz: UTC
                tags:
                - tag_id: 0b6d1f4e-7d52-4a9b-a3c1-6e8d2f9b4c20
                  seconds:
                  - 7200
                  - 18000
                  total_seconds: 25200
                  entries: 4
                untagged:
                  seconds:
                  - 1800
                  - 0
                  total_seconds: 1800
                  entries: 1
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
  /v1/sync:
    post:
      operationId: sync
      tags:
      - Sync
      x-internal: true
      summary: Sync clients, projects, tags and entries
      description: 'The Mac app''s sync call. In one round trip it applies the rows you send, then returns everything that
  
        changed after your `cursor` and that you may see: the clients, projects and tags of all your workspaces, and
  
        your own entries. Rows are applied in the order clients, projects, tags, entries; the last write wins per
  
        row, by `client_updated_at`. Rows that are refused go into `rejected` and don''t fail the request. Writing
  
        clients, projects and tags needs the owner or admin role. Entry changes fire webhooks like the REST calls.
  
        You can send at most 2000 rows in total. If a table has more than 5000 new rows, the response is cut short,
  
        `more` is `true`, and you call again with the new `cursor`.
  
        '
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SyncRequest'
            example:
              cursor: 1840
              clients: []
              projects: []
              tags: []
              entries:
              - id: 5c0f3a0e-6a0b-4c3a-9d55-0d8f4b7a1e01
                project_id: 9a1b7c52-3a3d-4e0c-8c1e-2f6f5d0a7b10
                title: Landing page copy
                started_at: 1762156800000
                ended_at: 1762164000000
                source: manual
                client_updated_at: 1762164100000
                deleted_at: null
      responses:
        '200':
          description: The new cursor and the rows after your cursor.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SyncResponse'
              example:
                cursor: 1847
                workspaces:
                - id: 71c4e0aa-9b2d-4d7e-8f13-5a6b0c9d2e44
                  name: Personal
                  kind: personal
                  role: owner
                  currency: USD
                  invoice:
                    company_name: ''
                    company_address: ''
                    tax_id: ''
                    prefix: INV-
                    next_number: 1
                    default_tax_rate: 0
                    payment_terms_days: 14
                    footer: ''
                    payment: {}
                clients: []
                projects: []
                tags: []
                entries:
                - id: 5c0f3a0e-6a0b-4c3a-9d55-0d8f4b7a1e01
                  project_id: 9a1b7c52-3a3d-4e0c-8c1e-2f6f5d0a7b10
                  title: Landing page copy
                  started_at: 1762156800000
                  ended_at: 1762164000000
                  source: manual
                  client_updated_at: 1762164100000
                  deleted_at: null
                  notes: null
                  billable: null
                  tag_ids: []
                  invoice_id: null
                  billed_at: null
                  user_id: 2f7d9e18-5b6c-4f0a-9e31-7a4c8d1b3e55
                  workspace_id: 71c4e0aa-9b2d-4d7e-8f13-5a6b0c9d2e44
                rejected: []
        '400':
          description: 'The body is not an object, `cursor` is not a non-negative integer or null, a list is not an array, or
  
            there are more than 2000 rows in all.
  
            '
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: At most 2000 rows can be synced per request.
        '401':
          $ref: '#/components/responses/Unauthorized'
  /v1/reports:
    get:
      operationId: getReport
      tags:
      - Reports
      summary: Get a report
      description: 'Totals and breakdowns of logged time in one workspace, filtered and grouped the way you ask. Owners and
  
        admins see everyone and money; team leads see themselves and the people they lead; members see only their
  
        own time. Everyone except owners and admins gets `null` for `amount`, `cost` and `profit`.
  
  
        Entries count by `started_at` inside `[from, to)`. At most 100,000 entries per report (otherwise `400`);
  
        at most 5,000 are listed when you ask for `include_entries`. Limited to 120 reports a minute per person.
  
        '
      parameters:
      - $ref: '#/components/parameters/WorkspaceIdQuery'
      - name: range
        in: query
        required: false
        schema:
          type: string
          enum:
          - this_week
          - last_week
          - this_month
          - last_month
          - this_year
          - last_30
          - custom
        description: 'A relative range, worked out in `tz` with the week starting on your `week_start`. Default `this_month`,
  
          or `custom` when `from` is given. `custom` needs `from` and `to`.
  
          '
      - name: from
        in: query
        required: false
        schema:
          $ref: '#/components/schemas/Timestamp'
        description: Start of a custom range (inclusive), in ms.
      - name: to
        in: query
        required: false
        schema:
          $ref: '#/components/schemas/Timestamp'
        description: End of a custom range (exclusive), in ms. Must be after `from`, at most 400 days later.
      - name: user_ids
        in: query
        required: false
        style: form
        explode: false
        schema:
          type: array
          maxItems: 200
          items:
            type: string
            format: uuid
        description: Only these people, comma separated. People you may not see are left out.
      - name: client_ids
        in: query
        required: false
        style: form
        explode: false
        schema:
          type: array
          maxItems: 200
          items:
            type: string
            format: uuid
        description: Only projects of these clients, comma separated.
      - name: project_ids
        in: query
        required: false
        style: form
        explode: false
        schema:
          type: array
          maxItems: 200
          items:
            type: string
            format: uuid
        description: Only these projects, comma separated.
      - name: tag_ids
        in: query
        required: false
        style: form
        explode: false
        schema:
          type: array
          maxItems: 200
          items:
            type: string
            format: uuid
        description: Only entries with at least one of these tags, comma separated.
      - name: billable
        in: query
        required: false
        schema:
          type: string
          enum:
          - all
          - billable
          - nonbillable
          default: all
        description: Billable is the entry's `billable`, or the project's `billable_default` when that is null.
      - name: billed
        in: query
        required: false
        schema:
          type: string
          enum:
          - all
          - billed
          - unbilled
          default: all
        description: '`billed` means an admin marked the entry billed (`billed_at` is set).'
      - name: group_by
        in: query
        required: false
        schema:
          type: string
          enum:
          - project
          - client
          - person
          - tag
          - day
          default: project
      - name: sub_group
        in: query
        required: false
        schema:
          type: string
          enum:
          - project
          - client
          - person
          - tag
          - day
        description: A second level inside each group. Ignored when it is the same as `group_by`.
      - name: tz
        in: query
        required: false
        schema:
          type: string
          examples:
          - Europe/Berlin
        description: IANA time zone for relative ranges and for grouping by day. Default your profile time zone, else UTC.
      - name: include_entries
        in: query
        required: false
        schema:
          type: string
          enum:
          - '1'
          - 'true'
        description: Also list the entries behind the numbers, as `entries`.
      responses:
        '200':
          description: The report.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ReportResponse'
              example:
                range:
                  from: 1761955200000
                  to: 1764547200000
                  name: this_month
                from: 1761955200000
                to: 1764547200000
                generated_at: 1762160400000
                currency: USD
                totals:
                  hours: 31.5
                  billable_hours: 26
                  amount: 3380
                  cost: 1260
                  profit: 2120
                  amount_by_currency:
                    USD: 3380
                groups:
                - key: 7c1d3a52-6b0e-4f6a-9d3e-2a8f5b7c9e10
                  label: Kestrel website
                  color: '#5B8DEF'
                  hours: 21
                  billable_hours: 21
                  amount: 2730
                  cost: 840
                  profit: 1890
                - key: 0e4b9a17-2d85-4c3b-b6f1-8a7d3c5e2f40
                  label: Internal
                  color: '#8FD6A5'
                  hours: 10.5
                  billable_hours: 5
                  amount: 650
                  cost: 420
                  profit: 230
                series:
                - date: '2025-11-01'
                  key: 7c1d3a52-6b0e-4f6a-9d3e-2a8f5b7c9e10
                  hours: 6
                - date: '2025-11-03'
                  key: 7c1d3a52-6b0e-4f6a-9d3e-2a8f5b7c9e10
                  hours: 7.5
                people:
                - user_id: 5a2e8c14-9b3d-4f71-a6c2-1d0e7b4f8a93
                  key: 5a2e8c14-9b3d-4f71-a6c2-1d0e7b4f8a93
                  name: Mara Lindqvist
                  hours: 31.5
                  billable_hours: 26
                  capacity_hours: 40
                  amount: 3380
                  cost: 1260
                  profit: 2120
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
  /v1/capacity:
    get:
      operationId: getCapacity
      tags:
      - Reports
      summary: Get capacity per person per week
      description: 'For each person and each week in the range: working capacity, planned hours, time off, logged hours and
  
        billable hours. Owners and admins see everyone, team leads themselves and the people they lead, members only
  
        themselves. Weeks start on your `week_start`, in `tz`. Time off is taken out of `capacity_hours` (never below
  
        0) and never counts as planned. `billable_hours` is `null` unless you are an owner or admin.
  
        '
      parameters:
      - $ref: '#/components/parameters/WorkspaceIdQuery'
      - name: from
        in: query
        required: true
        schema:
          $ref: '#/components/schemas/Timestamp'
        description: Start of the range, in ms. The first week is the one that contains it.
      - name: to
        in: query
        required: true
        schema:
          $ref: '#/components/schemas/Timestamp'
        description: End of the range (exclusive), in ms. Must be after `from`. At most 60 weeks.
      - name: team_id
        in: query
        required: false
        schema:
          type: string
          format: uuid
        description: Only people in this team.
      - name: tz
        in: query
        required: false
        schema:
          type: string
          examples:
          - Europe/Berlin
        description: IANA time zone for the weeks. Default your profile time zone, else UTC. An unknown zone is ignored.
      responses:
        '200':
          description: The week starts and one row per person per week.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CapacityResponse'
              example:
                weeks:
                - 1761523200000
                - 1762128000000
                capacity:
                - user_id: 5a2e8c14-9b3d-4f71-a6c2-1d0e7b4f8a93
                  name: Mara Lindqvist
                  email: mara@example.com
                  week_start: 1761523200000
                  capacity_hours: 40
                  time_off_hours: 0
                  planned_hours: 32
                  logged_hours: 29.5
                  billable_hours: 24
                - user_id: 5a2e8c14-9b3d-4f71-a6c2-1d0e7b4f8a93
                  name: Mara Lindqvist
                  email: mara@example.com
                  week_start: 1762128000000
                  capacity_hours: 32
                  time_off_hours: 8
                  planned_hours: 28
                  logged_hours: 2
                  billable_hours: 2
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
  /v1/budgets:
    get:
      operationId: listBudgets
      tags:
      - Reports
      summary: List project budgets
      description: 'Usage of every project in the workspace that has an hours or money budget (up to 500, by name). Usage covers
  
        the current period of each budget (this month, this week, or all time for a `total` budget) and counts
  
        everyone''s time on the project. Money fields are `null` unless you are an owner or admin.
  
        '
      parameters:
      - $ref: '#/components/parameters/WorkspaceIdQuery'
      responses:
        '200':
          description: One budget usage row per project.
          content:
            application/json:
              schema:
                type: object
                required:
                - budgets
                properties:
                  budgets:
                    type: array
                    items:
                      $ref: '#/components/schemas/BudgetUsage'
              example:
                budgets:
                - project_id: 7c1d3a52-6b0e-4f6a-9d3e-2a8f5b7c9e10
                  workspace_id: 9b1f6a4e-3c2d-4e8a-8f10-5d7c2b9a1e01
                  budget_period: monthly
                  period_start: 1761955200000
                  period_end: 1764547200000
                  budget_hours: 80
                  hours_used: 21
                  hours_pct: 26.25
                  budget_amount: 10400
                  amount_used: 2730
                  amount_pct: 26.25
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
  /v1/projects/{id}/budget:
    get:
      operationId: getProjectBudget
      tags:
      - Reports
      summary: Get a project's budget usage
      description: 'Budget usage of one project for its current period. Any member of the project''s workspace may ask. A project
  
        without a budget answers with empty budget fields. Money fields are `null` unless you are an owner or admin.
  
        '
      parameters:
      - $ref: '#/components/parameters/IdPath'
      responses:
        '200':
          description: The project's budget usage.
          content:
            application/json:
              schema:
                type: object
                required:
                - budget
                properties:
                  budget:
                    $ref: '#/components/schemas/BudgetUsage'
              example:
                budget:
                  project_id: 7c1d3a52-6b0e-4f6a-9d3e-2a8f5b7c9e10
                  workspace_id: 9b1f6a4e-3c2d-4e8a-8f10-5d7c2b9a1e01
                  budget_period: total
                  period_start: null
                  period_end: null
                  budget_hours: 200
                  hours_used: 64.5
                  hours_pct: 32.25
                  budget_amount: null
                  amount_used: null
                  amount_pct: null
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
  /v1/report-shares:
    get:
      operationId: listReportShares
      tags:
      - Shared reports
      summary: List shared reports
      description: 'The live public links of a workspace. Owners and admins see all of them; everyone else sees only the links
  
        they made. Each share carries its email `schedule`, or `null`. The link token itself is never returned here.
  
        '
      parameters:
      - $ref: '#/components/parameters/WorkspaceIdQuery'
      responses:
        '200':
          description: The shares, newest first.
          content:
            application/json:
              schema:
                type: object
                required:
                - shares
                properties:
                  shares:
                    type: array
                    items:
                      $ref: '#/components/schemas/ReportShare'
              example:
                shares:
                - id: c4a7e1b9-5d20-4a38-9f6c-0b2e8d3a7f15
                  workspace_id: 9b1f6a4e-3c2d-4e8a-8f10-5d7c2b9a1e01
                  name: Kestrel, November
                  filters:
                    range: this_month
                    billable: all
                    billed: all
                    group_by: project
                    sub_group: null
                    tz: Europe/Berlin
                  include_entries: false
                  snapshot: false
                  snapshot_at: null
                  branding:
                    title: Kestrel, November
                    accent_color: '#6A5BC2'
                    logo_url: null
                    company_name: Kestrel Inc.
                    show_amounts: false
                  created_by: 5a2e8c14-9b3d-4f71-a6c2-1d0e7b4f8a93
                  created_at: 1762156800000
                  updated_at: 1762156800000
                  schedule: null
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
    post:
      operationId: createReportShare
      tags:
      - Shared reports
      summary: Create a shared report
      description: 'Makes a public link to a report. Any member may do it. The link token is in the response once; Hourbrook
  
        keeps only its hash. A live share is worked out when someone opens it, with your permissions at that moment
  
        (a member''s link shows only their own time). A snapshot share is frozen now. Public views never carry cost,
  
        profit, rates, user ids, emails or notes. Amounts show only with `branding.show_amounts` and while the
  
        creator is an owner or admin. A workspace can have up to 100 live shares.
  
        '
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ReportShareCreate'
            example:
              workspace_id: 9b1f6a4e-3c2d-4e8a-8f10-5d7c2b9a1e01
              name: Kestrel, November
              filters:
                range: this_month
                group_by: project
                sub_group: person
              include_entries: false
              snapshot: false
              branding:
                company_name: Kestrel Inc.
                accent_color: '#6A5BC2'
                show_amounts: false
      responses:
        '200':
          description: The share, its token and the public link.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ReportShareCreated'
              example:
                share:
                  id: c4a7e1b9-5d20-4a38-9f6c-0b2e8d3a7f15
                  workspace_id: 9b1f6a4e-3c2d-4e8a-8f10-5d7c2b9a1e01
                  name: Kestrel, November
                  filters:
                    range: this_month
                    billable: all
                    billed: all
                    group_by: project
                    sub_group: person
                    tz: Europe/Berlin
                  include_entries: false
                  snapshot: false
                  snapshot_at: null
                  branding:
                    title: Kestrel, November
                    accent_color: '#6A5BC2'
                    logo_url: null
                    company_name: Kestrel Inc.
                    show_amounts: false
                  created_by: 5a2e8c14-9b3d-4f71-a6c2-1d0e7b4f8a93
                  created_at: 1762156800000
                  updated_at: 1762156800000
                token: Qm3x9T7vLk2dRz8WnYb4HcJf6AeUo1SiPg5XtVa0Ndw
                url: https://hourbrook.com/r/?t=Qm3x9T7vLk2dRz8WnYb4HcJf6AeUo1SiPg5XtVa0Ndw
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
  /v1/report-shares/{id}:
    patch:
      operationId: updateReportShare
      tags:
      - Shared reports
      summary: Update a shared report
      description: 'Changes a share''s name, filters, entries switch or branding. Owners, admins and the share''s creator may
        do it;
  
        anyone else gets `404`. `filters` replaces the whole filter set (omitted fields fall back to their defaults);
  
        `branding` changes only the keys you send. A snapshot share is recomputed, as its creator, when you change
  
        `filters`, `include_entries` or `branding`. If that creator has left the workspace the call answers `403`.
  
        '
      parameters:
      - $ref: '#/components/parameters/IdPath'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ReportSharePatch'
            example:
              name: Kestrel, November (final)
              branding:
                show_amounts: true
      responses:
        '200':
          description: The updated share.
          content:
            application/json:
              schema:
                type: object
                required:
                - share
                properties:
                  share:
                    $ref: '#/components/schemas/ReportShare'
              example:
                share:
                  id: c4a7e1b9-5d20-4a38-9f6c-0b2e8d3a7f15
                  workspace_id: 9b1f6a4e-3c2d-4e8a-8f10-5d7c2b9a1e01
                  name: Kestrel, November (final)
                  filters:
                    range: this_month
                    billable: all
                    billed: all
                    group_by: project
                    sub_group: person
                    tz: Europe/Berlin
                  include_entries: false
                  snapshot: false
                  snapshot_at: null
                  branding:
                    title: Kestrel, November
                    accent_color: '#6A5BC2'
                    logo_url: null
                    company_name: Kestrel Inc.
                    show_amounts: true
                  created_by: 5a2e8c14-9b3d-4f71-a6c2-1d0e7b4f8a93
                  created_at: 1762156800000
                  updated_at: 1762243200000
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
    delete:
      operationId: deleteReportShare
      tags:
      - Shared reports
      summary: Revoke a shared report
      description: 'Kills the public link for good and turns off its email schedule. Owners, admins and the share''s creator
        may do
  
        it; anyone else gets `404`.
  
        '
      parameters:
      - $ref: '#/components/parameters/IdPath'
      responses:
        '200':
          $ref: '#/components/responses/Ok'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
  /v1/report-shares/{id}/schedule:
    put:
      x-badges: [{ name: Session only, color: "var(--scalar-color-orange)" }]
      operationId: putReportShareSchedule
      tags:
      - Shared reports
      summary: Schedule a shared report by email
      description: 'Emails a share weekly or monthly to 1 to 10 addresses, or replaces its existing schedule (one per share).
  
        Owners, admins and the share''s creator may do it, signed in; API tokens get `403 session_required`. Whoever
  
        saves it last becomes its `created_by`. Each recipient gets their own email with a link of its own, which
  
        stops working when the schedule or the share goes. A workspace can have 25 schedules, and you can change
  
        schedules 30 times an hour. A test account can''t schedule at all (`403`, `code: test_account`).
  
        Answers `503` (`code: schedules_unavailable`) when the server has no schedule key yet.
  
        '
      security:
      - session: []
      parameters:
      - $ref: '#/components/parameters/IdPath'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ReportScheduleInput'
            example:
              frequency: weekly
              weekday: 1
              hour: 9
              timezone: Europe/Berlin
              recipients:
              - ana@example.com
              - finance@example.com
              subject: Kestrel weekly hours
      responses:
        '200':
          description: The saved schedule.
          content:
            application/json:
              schema:
                type: object
                required:
                - schedule
                properties:
                  schedule:
                    $ref: '#/components/schemas/ReportSchedule'
              example:
                schedule:
                  id: 2d9b6f03-8e41-4c75-a1b8-7f3c0e5a9d62
                  share_id: c4a7e1b9-5d20-4a38-9f6c-0b2e8d3a7f15
                  created_by: 5a2e8c14-9b3d-4f71-a6c2-1d0e7b4f8a93
                  frequency: weekly
                  weekday: 1
                  day_of_month: null
                  hour: 9
                  timezone: Europe/Berlin
                  recipients:
                  - ana@example.com
                  - finance@example.com
                  subject: Kestrel weekly hours
                  next_run_at: 1762761600000
                  last_run_at: null
                  last_status: null
                  created_at: 1762156800000
                  updated_at: 1762156800000
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/SessionRequired'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '503':
          description: The server has no schedule key yet (code schedules_unavailable).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: Scheduled emails aren't set up on this server yet.
                code: schedules_unavailable
    delete:
      x-badges: [{ name: Session only, color: "var(--scalar-color-orange)" }]
      operationId: deleteReportShareSchedule
      tags:
      - Shared reports
      summary: Stop emailing a shared report
      description: 'Turns the schedule off and kills the link that was emailed. The share''s own link keeps working. Owners,
  
        admins and the share''s creator may do it, signed in. Counts toward the 30 schedule changes an hour.
  
        '
      security:
      - session: []
      parameters:
      - $ref: '#/components/parameters/IdPath'
      responses:
        '200':
          $ref: '#/components/responses/Ok'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/SessionRequired'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
  /v1/report-shares/{id}/schedule/test:
    post:
      x-badges: [{ name: Session only, color: "var(--scalar-color-orange)" }]
      operationId: testReportShareSchedule
      tags:
      - Shared reports
      summary: Send a test report email
      description: 'Sends the scheduled email right now to your own address only, with `[Test]` in the subject. The share needs
        a
  
        saved schedule first (`404` otherwise). Signed in only. Five test sends an hour. On a dev server without a mail
  
        key the rendered email comes back as `dev` instead of being sent.
  
        '
      security:
      - session: []
      parameters:
      - $ref: '#/components/parameters/IdPath'
      responses:
        '200':
          description: Sent.
          content:
            application/json:
              schema:
                type: object
                required:
                - ok
                - sent_to
                properties:
                  ok:
                    type: boolean
                    const: true
                  sent_to:
                    type: string
                    format: email
                  dev:
                    type: object
                    description: Only on a dev server that logs mail instead of sending it.
                    properties:
                      subject:
                        type: string
                      text:
                        type: string
                      html:
                        type: string
              example:
                ok: true
                sent_to: mara@example.com
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/SessionRequired'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          description: The report can't be emailed because its link doesn't show anything any more.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: 'This report can''t be emailed: its link doesn''t show anything any more.'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '502':
          description: The email could not be sent.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: Couldn't send the email. Try again in a minute.
        '503':
          description: The server has no schedule key yet (code schedules_unavailable).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: Scheduled emails aren't set up on this server yet.
                code: schedules_unavailable
  /v1/public/reports/{token}:
    get:
      operationId: getPublicReport
      tags:
      - Shared reports
      summary: Open a shared report
      description: 'The report behind a public link. No sign-in needed: the token in the link is the key. It never carries cost,
  
        profit, rates, capacity, user ids, emails or notes; people are named by display name, else "Member N".
  
        `amount` shows only when the share allows amounts and its creator is still an owner or admin. A live share
  
        is worked out with its creator''s permissions and cached for a minute; a snapshot is the stored result.
  
        Answers `404` once the link is revoked or its creator has left the workspace. Limited to 60 requests a minute
  
        per address.
  
        '
      security: []
      parameters:
      - name: token
        in: path
        required: true
        schema:
          type: string
          pattern: ^[A-Za-z0-9_-]{43}$
        description: The token from the share's link (`/r/?t=<token>`).
      responses:
        '200':
          description: The report with its name, branding and public filters.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicReport'
              example:
                name: Kestrel, November
                branding:
                  title: Kestrel, November
                  accent_color: '#6A5BC2'
                  logo_url: null
                  company_name: Kestrel Inc.
                  show_amounts: false
                filters:
                  range: this_month
                  billable: all
                  billed: all
                  group_by: project
                  sub_group: null
                include_entries: false
                snapshot: false
                range:
                  from: 1761955200000
                  to: 1764547200000
                  name: this_month
                from: 1761955200000
                to: 1764547200000
                generated_at: 1762160400000
                currency: USD
                totals:
                  hours: 31.5
                  billable_hours: 26
                  amount: null
                  cost: null
                  profit: null
                groups:
                - key: 7c1d3a52-6b0e-4f6a-9d3e-2a8f5b7c9e10
                  label: Kestrel website
                  color: '#5B8DEF'
                  hours: 21
                  billable_hours: 21
                  amount: null
                  cost: null
                  profit: null
                series:
                - date: '2025-11-03'
                  key: 7c1d3a52-6b0e-4f6a-9d3e-2a8f5b7c9e10
                  hours: 7.5
                people:
                - key: Mara Lindqvist
                  name: Mara Lindqvist
                  hours: 31.5
                  billable_hours: 26
                  amount: null
                  cost: null
                  profit: null
                projects:
                - id: 7c1d3a52-6b0e-4f6a-9d3e-2a8f5b7c9e10
                  name: Kestrel website
                  color: '#5B8DEF'
                  client_id: 3f0c2d1e-7a64-4b95-8c2d-9e1f5a0b6d37
                clients:
                - id: 3f0c2d1e-7a64-4b95-8c2d-9e1f5a0b6d37
                  name: Kestrel Inc.
                tags: []
                budgets: []
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
  /v1/invoices:
    get:
      operationId: listInvoices
      tags:
      - Invoices
      summary: List invoices
      description: 'Every invoice of the workspace, newest first, without lines. Owners and admins only; other members get `403`
  
        and non-members `404`.
  
        '
      parameters:
      - $ref: '#/components/parameters/WorkspaceIdQuery'
      responses:
        '200':
          description: The invoices.
          content:
            application/json:
              schema:
                type: object
                required:
                - invoices
                properties:
                  invoices:
                    type: array
                    items:
                      $ref: '#/components/schemas/Invoice'
              example:
                invoices:
                - id: e8b2c5a0-1f47-4d93-b6a8-3c9d0e7f2a51
                  workspace_id: 9b1f6a4e-3c2d-4e8a-8f10-5d7c2b9a1e01
                  client_id: 3f0c2d1e-7a64-4b95-8c2d-9e1f5a0b6d37
                  number: INV-0007
                  status: sent
                  issue_date: 1761955200000
                  due_date: 1764547200000
                  currency: USD
                  tax_rate: 19
                  notes: ''
                  period_from: 1759276800000
                  period_to: 1761955200000
                  subtotal: 2730
                  tax: 518.7
                  total: 3248.7
                  created_at: 1761955200000
                  updated_at: 1762041600000
                  show_payment: true
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
    post:
      operationId: createInvoice
      tags:
      - Invoices
      summary: Create an invoice
      description: 'Makes a draft invoice for one client from logged time. It collects everyone''s billable, not-yet-invoiced,
  
        not-billed entries on that client''s projects that have a rate and that started in `[from, to)`. The rate is
  
        the author''s `bill_rate` when set, else the project''s. The tax rate, payment terms, currency and next
  
        invoice number come from the workspace''s invoice settings. Those entries are locked to the invoice and
  
        the `invoice.created` webhook fires. Owners and admins only. Answers `400` when there is nothing to
  
        invoice and `409` when the invoice number is already taken.
  
        '
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/InvoiceCreate'
            example:
              workspace_id: 9b1f6a4e-3c2d-4e8a-8f10-5d7c2b9a1e01
              client_id: 3f0c2d1e-7a64-4b95-8c2d-9e1f5a0b6d37
              from: 1759276800000
              to: 1761955200000
              group_by: project
      responses:
        '200':
          description: The new draft, with its lines.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/InvoiceResponse'
              example:
                invoice:
                  id: e8b2c5a0-1f47-4d93-b6a8-3c9d0e7f2a51
                  workspace_id: 9b1f6a4e-3c2d-4e8a-8f10-5d7c2b9a1e01
                  client_id: 3f0c2d1e-7a64-4b95-8c2d-9e1f5a0b6d37
                  number: INV-0007
                  status: draft
                  issue_date: 1762156800000
                  due_date: 1764748800000
                  currency: USD
                  tax_rate: 19
                  notes: ''
                  period_from: 1759276800000
                  period_to: 1761955200000
                  subtotal: 2730
                  tax: 518.7
                  total: 3248.7
                  created_at: 1762156800000
                  updated_at: 1762156800000
                  show_payment: true
                  lines:
                  - id: 6a0f3d82-4c19-4e57-8b2a-d1e9c7f50b34
                    position: 0
                    description: Kestrel website · Oct 1 – Oct 31, 2025
                    quantity: 21
                    rate: 130
                    amount: 2730
                    project_id: 7c1d3a52-6b0e-4f6a-9d3e-2a8f5b7c9e10
                  client:
                    id: 3f0c2d1e-7a64-4b95-8c2d-9e1f5a0b6d37
                    workspace_id: 9b1f6a4e-3c2d-4e8a-8f10-5d7c2b9a1e01
                    name: Kestrel Inc.
                    email: ap@example.com
                    address: 1 Harbour Row, Bristol
                    client_updated_at: 1761955200000
                    deleted_at: null
                  issuer:
                    company_name: Lindqvist Studio
                    company_address: 12 Elm Street, Bristol
                    tax_id: GB123456789
                    prefix: INV-
                    next_number: 8
                    default_tax_rate: 19
                    payment_terms_days: 30
                    footer: Thank you.
                    payment:
                      account_name: Lindqvist Studio
                      bank_name: Example Bank
                      iban: GB82WEST12345698765432
                      account_number: ''
                      routing: ''
                      swift: ''
                      other: ''
                    workspace_name: Lindqvist Studio
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
  /v1/invoices/{id}:
    get:
      operationId: getInvoice
      tags:
      - Invoices
      summary: Get an invoice
      description: 'One invoice with its lines, its client and the issuer details (the workspace''s invoice settings and name).
  
        Owners and admins only.
  
        '
      parameters:
      - $ref: '#/components/parameters/IdPath'
      responses:
        '200':
          description: The invoice.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/InvoiceResponse'
              example:
                invoice:
                  id: e8b2c5a0-1f47-4d93-b6a8-3c9d0e7f2a51
                  workspace_id: 9b1f6a4e-3c2d-4e8a-8f10-5d7c2b9a1e01
                  client_id: 3f0c2d1e-7a64-4b95-8c2d-9e1f5a0b6d37
                  number: INV-0007
                  status: sent
                  issue_date: 1762156800000
                  due_date: 1764748800000
                  currency: USD
                  tax_rate: 19
                  notes: Thanks for a good month.
                  period_from: 1759276800000
                  period_to: 1761955200000
                  subtotal: 2730
                  tax: 518.7
                  total: 3248.7
                  created_at: 1762156800000
                  updated_at: 1762243200000
                  show_payment: true
                  lines:
                  - id: 6a0f3d82-4c19-4e57-8b2a-d1e9c7f50b34
                    position: 0
                    description: Kestrel website · Oct 1 – Oct 31, 2025
                    quantity: 21
                    rate: 130
                    amount: 2730
                    project_id: 7c1d3a52-6b0e-4f6a-9d3e-2a8f5b7c9e10
                  client:
                    id: 3f0c2d1e-7a64-4b95-8c2d-9e1f5a0b6d37
                    workspace_id: 9b1f6a4e-3c2d-4e8a-8f10-5d7c2b9a1e01
                    name: Kestrel Inc.
                    email: ap@example.com
                    address: 1 Harbour Row, Bristol
                    client_updated_at: 1761955200000
                    deleted_at: null
                  issuer:
                    company_name: Lindqvist Studio
                    company_address: 12 Elm Street, Bristol
                    tax_id: GB123456789
                    prefix: INV-
                    next_number: 8
                    default_tax_rate: 19
                    payment_terms_days: 30
                    footer: Thank you.
                    payment:
                      account_name: Lindqvist Studio
                      bank_name: Example Bank
                      iban: GB82WEST12345698765432
                      account_number: ''
                      routing: ''
                      swift: ''
                      other: ''
                    workspace_name: Lindqvist Studio
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
    patch:
      operationId: updateInvoice
      tags:
      - Invoices
      summary: Update an invoice
      description: 'Changes an invoice''s status or content. Owners and admins only. A **draft** accepts everything; `lines`
  
        replaces all lines and the totals are recomputed. Once it is **sent** or **paid**, only `status`, `notes`
  
        and `show_payment` can change (`409` otherwise). A **void** invoice can''t change at all.
  
  
        Status moves: `draft` to `sent` or `void`; `sent` to `paid` or `void`; `paid` to `sent` or `void`. Going
  
        to `void` releases the invoice''s entries so they can be edited and invoiced again. Any other move is `409`.
  
        '
      parameters:
      - $ref: '#/components/parameters/IdPath'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/InvoicePatch'
            example:
              status: sent
              notes: Payable within 30 days.
      responses:
        '200':
          description: The updated invoice, with its lines.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/InvoiceResponse'
              example:
                invoice:
                  id: e8b2c5a0-1f47-4d93-b6a8-3c9d0e7f2a51
                  workspace_id: 9b1f6a4e-3c2d-4e8a-8f10-5d7c2b9a1e01
                  client_id: 3f0c2d1e-7a64-4b95-8c2d-9e1f5a0b6d37
                  number: INV-0007
                  status: sent
                  issue_date: 1762156800000
                  due_date: 1764748800000
                  currency: USD
                  tax_rate: 19
                  notes: Payable within 30 days.
                  period_from: 1759276800000
                  period_to: 1761955200000
                  subtotal: 2730
                  tax: 518.7
                  total: 3248.7
                  created_at: 1762156800000
                  updated_at: 1762243200000
                  show_payment: true
                  lines:
                  - id: 6a0f3d82-4c19-4e57-8b2a-d1e9c7f50b34
                    position: 0
                    description: Kestrel website · Oct 1 – Oct 31, 2025
                    quantity: 21
                    rate: 130
                    amount: 2730
                    project_id: 7c1d3a52-6b0e-4f6a-9d3e-2a8f5b7c9e10
                  client:
                    id: 3f0c2d1e-7a64-4b95-8c2d-9e1f5a0b6d37
                    workspace_id: 9b1f6a4e-3c2d-4e8a-8f10-5d7c2b9a1e01
                    name: Kestrel Inc.
                    email: ap@example.com
                    address: 1 Harbour Row, Bristol
                    client_updated_at: 1761955200000
                    deleted_at: null
                  issuer:
                    company_name: Lindqvist Studio
                    company_address: 12 Elm Street, Bristol
                    tax_id: GB123456789
                    prefix: INV-
                    next_number: 8
                    default_tax_rate: 19
                    payment_terms_days: 30
                    footer: Thank you.
                    payment:
                      account_name: Lindqvist Studio
                      bank_name: Example Bank
                      iban: GB82WEST12345698765432
                      account_number: ''
                      routing: ''
                      swift: ''
                      other: ''
                    workspace_name: Lindqvist Studio
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
    delete:
      operationId: deleteInvoice
      tags:
      - Invoices
      summary: Delete an invoice
      description: 'Deletes a draft or void invoice for good and releases its entries. A sent or paid invoice can''t be deleted:
  
        void it first (`409`). Owners and admins only.
  
        '
      parameters:
      - $ref: '#/components/parameters/IdPath'
      responses:
        '200':
          $ref: '#/components/responses/Ok'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
  /v1/plans:
    get:
      operationId: listPlans
      tags:
      - Plans
      summary: List plans
      description: 'Planned hours and time off with a `date` in `[from, to)`. Owners and admins see the whole workspace, team
  
        leads themselves and the people they lead, members only their own. `user_id` narrows it to one person; a
  
        person you may not see gives an empty list.
  
        '
      parameters:
      - $ref: '#/components/parameters/WorkspaceIdQuery'
      - $ref: '#/components/parameters/FromQuery'
      - $ref: '#/components/parameters/ToQuery'
      - name: user_id
        in: query
        required: false
        schema:
          type: string
        description: Only this person's plans.
      - $ref: '#/components/parameters/ExternalIdQuery'
      responses:
        '200':
          description: The plans, ordered by date, person and id.
          content:
            application/json:
              schema:
                type: object
                required:
                - plans
                properties:
                  plans:
                    type: array
                    items:
                      $ref: '#/components/schemas/Plan'
              example:
                plans:
                - id: b3d71e08-9a52-4f6c-8e1d-2c4a6f0b9d75
                  workspace_id: 9b1f6a4e-3c2d-4e8a-8f10-5d7c2b9a1e01
                  user_id: 5a2e8c14-9b3d-4f71-a6c2-1d0e7b4f8a93
                  project_id: 7c1d3a52-6b0e-4f6a-9d3e-2a8f5b7c9e10
                  date: 1762128000000
                  hours: 6
                  note: null
                  title: Homepage hero
                  status: planned
                  kind: work
                  updated_at: 1762041600000
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
  /v1/plans/{id}:
    put:
      operationId: putPlan
      tags:
      - Plans
      summary: Create or replace a plan
      description: 'Writes one plan under an id you choose, so a retry is safe. You can plan time for yourself and, as an
  
        owner, admin or team lead, for the people you manage (`user_id`). The plan can''t move between workspaces.
  
        Writing over a deleted plan brings it back. `kind: time_off` takes hours out of capacity and has no project;
  
        `work` needs an active project of the same workspace. The first time a work plan becomes `done`, the
  
        `task.done` webhook fires.
  
        '
      parameters:
      - $ref: '#/components/parameters/IdPath'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PlanPut'
            example:
              workspace_id: 9b1f6a4e-3c2d-4e8a-8f10-5d7c2b9a1e01
              project_id: 7c1d3a52-6b0e-4f6a-9d3e-2a8f5b7c9e10
              date: 1762128000000
              hours: 6
              title: Homepage hero
      responses:
        '200':
          description: The saved plan.
          content:
            application/json:
              schema:
                type: object
                required:
                - plan
                properties:
                  plan:
                    $ref: '#/components/schemas/Plan'
              example:
                plan:
                  id: b3d71e08-9a52-4f6c-8e1d-2c4a6f0b9d75
                  workspace_id: 9b1f6a4e-3c2d-4e8a-8f10-5d7c2b9a1e01
                  user_id: 5a2e8c14-9b3d-4f71-a6c2-1d0e7b4f8a93
                  project_id: 7c1d3a52-6b0e-4f6a-9d3e-2a8f5b7c9e10
                  date: 1762128000000
                  hours: 6
                  note: null
                  title: Homepage hero
                  status: planned
                  kind: work
                  updated_at: 1762156800000
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
    delete:
      operationId: deletePlan
      tags:
      - Plans
      summary: Delete a plan
      description: 'Deletes a plan (soft delete; deleting it twice is fine). You may delete your own plans and those of the
  
        people you manage; a plan you can''t see is `404`, one you can see but may not change is `403`.
  
        '
      parameters:
      - $ref: '#/components/parameters/IdPath'
      responses:
        '200':
          $ref: '#/components/responses/Ok'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
  /v1/webhooks:
    get:
      operationId: listWebhooks
      tags:
      - Webhooks
      summary: List webhooks
      description: 'The workspace''s webhooks, oldest first. Owners and admins only. The secret is never listed. `last_status`
  
        says only whether the latest delivery worked.
  
        '
      parameters:
      - $ref: '#/components/parameters/WorkspaceIdQuery'
      responses:
        '200':
          description: The webhooks.
          content:
            application/json:
              schema:
                type: object
                required:
                - webhooks
                properties:
                  webhooks:
                    type: array
                    items:
                      $ref: '#/components/schemas/Webhook'
              example:
                webhooks:
                - id: 4e6c1a97-0b38-4d52-a9f7-8c2d5e3b1a60
                  workspace_id: 9b1f6a4e-3c2d-4e8a-8f10-5d7c2b9a1e01
                  url: https://hooks.example.com/hourbrook
                  events:
                  - entry.created
                  - entry.updated
                  - entry.deleted
                  created_at: 1761955200000
                  last_status: ok
                  last_delivery_at: 1762156800000
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
    post:
      x-badges: [{ name: Session only, color: "var(--scalar-color-orange)" }]
      operationId: createWebhook
      tags:
      - Webhooks
      summary: Create a webhook
      description: 'Registers an https address that receives signed events. Owners and admins only, signed in: API tokens get
  
        `403 session_required`. The URL must be a public https address (default port, no credentials, a DNS name,
  
        never an IP address, `localhost` or wildcard-IP services such as `nip.io`). Without `events` you get the three
  
        entry events. The `secret` that signs deliveries is in the response once. A workspace can have 10 webhooks.
  
        Deliveries stop if the creator stops being an owner or admin. The events are described under **Webhooks** in
  
        the reference.
  
        '
      security:
      - session: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WebhookCreate'
            example:
              workspace_id: 9b1f6a4e-3c2d-4e8a-8f10-5d7c2b9a1e01
              url: https://hooks.example.com/hourbrook
              events:
              - entry.created
              - invoice.created
              - week.closed
      responses:
        '200':
          description: The webhook and its secret.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookCreated'
              example:
                webhook:
                  id: 4e6c1a97-0b38-4d52-a9f7-8c2d5e3b1a60
                  workspace_id: 9b1f6a4e-3c2d-4e8a-8f10-5d7c2b9a1e01
                  url: https://hooks.example.com/hourbrook
                  events:
                  - entry.created
                  - invoice.created
                  - week.closed
                  created_at: 1762156800000
                  last_status: null
                  last_delivery_at: null
                secret: whsec_Nk4vXb9TqLz2RdYw7HcJf1AeUo6SiPg3Mt8Va0CnEhs
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/SessionRequired'
        '404':
          $ref: '#/components/responses/NotFound'
  /v1/webhooks/{id}:
    patch:
      x-badges: [{ name: Session only, color: "var(--scalar-color-orange)" }]
      operationId: updateWebhook
      tags:
      - Webhooks
      summary: Change a webhook
      description: |
        Changes the address or the events of a webhook; the secret stays the same. Owners and admins only, signed
        in: API tokens get `403 session_required`. Send `url`, `events` or both. A new address is checked like on
        create and clears `last_status` and `last_delivery_at`, which were about the old one.
      security:
      - session: []
      parameters:
      - $ref: '#/components/parameters/IdPath'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WebhookPatch'
            example:
              events:
              - entry.created
              - entry.updated
              - entry.deleted
              - project.created
              - project.updated
              - project.deleted
      responses:
        '200':
          description: The webhook as it is now.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/SessionRequired'
        '404':
          $ref: '#/components/responses/NotFound'
    delete:
      x-badges: [{ name: Session only, color: "var(--scalar-color-orange)" }]
      operationId: deleteWebhook
      tags:
      - Webhooks
      summary: Delete a webhook
      description: 'Stops a webhook for good. Owners and admins only, signed in: API tokens get `403 session_required`. A member
  
        who isn''t an owner or admin also gets `403`.
  
        '
      security:
      - session: []
      parameters:
      - $ref: '#/components/parameters/IdPath'
      responses:
        '200':
          $ref: '#/components/responses/Ok'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/SessionRequired'
        '404':
          $ref: '#/components/responses/NotFound'
  /v1/webhooks/{id}/test:
    post:
      x-badges: [{ name: Session only, color: "var(--scalar-color-orange)" }]
      operationId: testWebhook
      tags:
      - Webhooks
      summary: Send a test event
      description: 'Sends one signed `ping` event to the webhook, built and signed like a real delivery, so you can check the
  
        address and your signature check. `ping` is not an event you subscribe to. Owners and admins only, signed in.
  
        Ten tests an hour. Answers `502` when the address doesn''t answer with a 2xx status. The result also updates
  
        `last_status`.
  
        '
      security:
      - session: []
      parameters:
      - $ref: '#/components/parameters/IdPath'
      responses:
        '200':
          $ref: '#/components/responses/Ok'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/SessionRequired'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '502':
          description: The address didn't accept the test event.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: That address didn't accept the test event. Check the address and that it answers with a 2xx status.
  /v1/tasks:
    get:
      operationId: listTasks
      tags:
      - Tasks
      summary: List tasks
      description: 'Your own live tasks with their checklist items, their entry links and the time logged on each. Ordered by
  
        day, then position. Nobody else can see your tasks, in any workspace.
  
  
        Without filters you get every task. Tasks come in pages of up to 1000: when `more` is `true`, send
  
        `next_cursor` back as `cursor` to get the next page.
  
        '
      parameters:
      - name: from
        in: query
        required: false
        schema:
          type: string
          pattern: ^\d{4}-\d{2}-\d{2}$
        description: First day to include, as `YYYY-MM-DD`. Tasks not on a day (the inbox) are left out when this is set.
        example: '2025-11-03'
      - name: to
        in: query
        required: false
        schema:
          type: string
          pattern: ^\d{4}-\d{2}-\d{2}$
        description: Last day to include (inclusive), as `YYYY-MM-DD`. Inbox tasks are left out when this is set.
        example: '2025-11-09'
      - name: inbox
        in: query
        required: false
        schema:
          type: string
          enum:
          - '1'
          - 'true'
        description: '`1` or `true`: only tasks that are not on a day yet. Any other value is ignored.'
      - name: done
        in: query
        required: false
        schema:
          type: string
          enum:
          - '0'
          - '1'
          - 'true'
          - 'false'
        description: '`1` or `true`: only finished tasks. `0` or `false`: only open ones.'
      - name: limit
        in: query
        required: false
        schema:
          type: integer
          minimum: 1
          maximum: 1000
          default: 1000
        description: The most tasks to return in this page.
      - name: cursor
        in: query
        required: false
        schema:
          type: string
        description: The `next_cursor` from the previous page.
      responses:
        '200':
          description: A page of your tasks.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TaskListResponse'
              example:
                tasks:
                - id: 3f6d2c1e-5a7b-4c8d-9e0f-1a2b3c4d5e6f
                  title: Fix the CSV export
                  notes: null
                  day: '2025-11-03'
                  project_id: 8b1d7a52-2c64-4e0a-a3f1-6d9c0e4b7f21
                  stage: doing
                  done_at: null
                  position: 1
                  estimate_minutes: 90
                  source: linear
                  external_id: 9a1c0d3e-7f42-4b6a-8c15-2e9d4f0a6b73
                  external_key: ENG-142
                  external_url: https://linear.app/example/issue/ENG-142
                  issue:
                    description: Dates in the export are off by one day.
                    status: In Progress
                    labels:
                    - bug
                    updated_at: 1762150000000
                  created_at: 1762070000000
                  client_updated_at: 1762156800000
                  deleted_at: null
                subtasks:
                - id: 5c0e9b14-3d28-4a76-b1f9-0e7a2d8c4f36
                  task_id: 3f6d2c1e-5a7b-4c8d-9e0f-1a2b3c4d5e6f
                  title: Write a failing test
                  done: true
                  position: 1
                  created_at: 1762070100000
                  client_updated_at: 1762150000000
                  deleted_at: null
                links:
                - task_id: 3f6d2c1e-5a7b-4c8d-9e0f-1a2b3c4d5e6f
                  entry_id: a7e4c2d9-6b10-4f35-8d2a-9c1e0b3f5a48
                  client_updated_at: 1762153000000
                  deleted_at: null
                time:
                  3f6d2c1e-5a7b-4c8d-9e0f-1a2b3c4d5e6f:
                    seconds: 5400
                    entries: 2
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
  /v1/tasks/{id}:
    get:
      operationId: getTask
      tags:
      - Tasks
      summary: Get a task
      description: 'One of your tasks with its checklist, its entry links, the entries linked to it (yours, not deleted, oldest
  
        first) and the time they add up to. Answers `404` for a deleted task, a task that isn''t yours, or an id
  
        that isn''t a UUID.
  
        '
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
          format: uuid
        description: The task id.
      responses:
        '200':
          description: The task and what hangs off it.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TaskDetailResponse'
              example:
                task:
                  id: 3f6d2c1e-5a7b-4c8d-9e0f-1a2b3c4d5e6f
                  title: Fix the CSV export
                  notes: Customer reported it twice.
                  day: '2025-11-03'
                  project_id: 8b1d7a52-2c64-4e0a-a3f1-6d9c0e4b7f21
                  stage: doing
                  done_at: null
                  position: 1
                  estimate_minutes: 90
                  source: null
                  external_id: null
                  external_key: null
                  external_url: null
                  issue: null
                  created_at: 1762070000000
                  client_updated_at: 1762156800000
                  deleted_at: null
                subtasks:
                - id: 5c0e9b14-3d28-4a76-b1f9-0e7a2d8c4f36
                  task_id: 3f6d2c1e-5a7b-4c8d-9e0f-1a2b3c4d5e6f
                  title: Write a failing test
                  done: false
                  position: 1
                  created_at: 1762070100000
                  client_updated_at: 1762070100000
                  deleted_at: null
                links:
                - task_id: 3f6d2c1e-5a7b-4c8d-9e0f-1a2b3c4d5e6f
                  entry_id: a7e4c2d9-6b10-4f35-8d2a-9c1e0b3f5a48
                  client_updated_at: 1762153000000
                  deleted_at: null
                entries:
                - id: a7e4c2d9-6b10-4f35-8d2a-9c1e0b3f5a48
                  project_id: 8b1d7a52-2c64-4e0a-a3f1-6d9c0e4b7f21
                  title: CSV export
                  started_at: 1762153200000
                  ended_at: 1762156800000
                  source: manual
                  client_updated_at: 1762156800000
                  deleted_at: null
                  notes: null
                  billable: null
                  tag_ids: []
                  invoice_id: null
                  billed_at: null
                  user_id: u_4d8e1f
                  workspace_id: ws_8f2c71
                time:
                  seconds: 3600
                  entries: 1
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
    put:
      operationId: putTask
      tags:
      - Tasks
      summary: Create or update a task
      description: 'Creates your task with the id in the URL, or updates it if it exists (a deleted task comes back to life).
  
        The id must be a UUID you make. Only `title` is required, and only when the task is new. Fields you leave
  
        out keep their stored value, so a partial body is a safe update.
  
  
        The server stamps the change itself; you can''t send `client_updated_at` or `deleted_at` here. Counts as
  
        one of 300 changes a minute across your tasks, checklist items, links and planned blocks.
  
        '
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
          format: uuid
        description: The task id, a UUID you make.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TaskWrite'
            example:
              title: Fix the CSV export
              day: '2025-11-03'
              project_id: 8b1d7a52-2c64-4e0a-a3f1-6d9c0e4b7f21
              stage: doing
              estimate_minutes: 90
              source: linear
              external_id: 9a1c0d3e-7f42-4b6a-8c15-2e9d4f0a6b73
              external_key: ENG-142
              external_url: https://linear.app/example/issue/ENG-142
              issue:
                description: Dates in the export are off by one day.
                status: In Progress
                labels:
                - bug
      responses:
        '200':
          description: The task as stored.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TaskResponse'
              example:
                task:
                  id: 3f6d2c1e-5a7b-4c8d-9e0f-1a2b3c4d5e6f
                  title: Fix the CSV export
                  notes: null
                  day: '2025-11-03'
                  project_id: 8b1d7a52-2c64-4e0a-a3f1-6d9c0e4b7f21
                  stage: doing
                  done_at: null
                  position: 0
                  estimate_minutes: 90
                  source: linear
                  external_id: 9a1c0d3e-7f42-4b6a-8c15-2e9d4f0a6b73
                  external_key: ENG-142
                  external_url: https://linear.app/example/issue/ENG-142
                  issue:
                    description: Dates in the export are off by one day.
                    status: In Progress
                    labels:
                    - bug
                    updated_at: null
                  created_at: 1762156800000
                  client_updated_at: 1762156800000
                  deleted_at: null
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '409':
          description: You already have the most tasks allowed (50,000 live ones).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: Too many tasks.
        '429':
          $ref: '#/components/responses/TooManyRequests'
    delete:
      operationId: deleteTask
      tags:
      - Tasks
      summary: Delete a task
      description: 'Deletes your task together with its checklist items and entry links. Planned blocks that pointed at it stay,
  
        with no task. Deleting a task that is already deleted answers `ok` again. Your entries are never touched.
  
        '
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
          format: uuid
        description: The task id.
      responses:
        '200':
          $ref: '#/components/responses/Ok'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
  /v1/tasks/{id}/entries/{entryId}:
    put:
      operationId: linkEntryToTask
      tags:
      - Tasks
      summary: Link an entry to a task
      description: 'Counts one of your own entries toward one of your tasks. Both must exist and not be deleted. Linking a
  
        pair that is already linked changes nothing. Takes no body.
  
        '
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
          format: uuid
        description: The task id.
      - name: entryId
        in: path
        required: true
        schema:
          type: string
          format: uuid
        description: The id of one of your own entries.
      responses:
        '200':
          description: The link.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TaskLinkResponse'
              example:
                link:
                  task_id: 3f6d2c1e-5a7b-4c8d-9e0f-1a2b3c4d5e6f
                  entry_id: a7e4c2d9-6b10-4f35-8d2a-9c1e0b3f5a48
                  client_updated_at: 1762156800000
                  deleted_at: null
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          description: You already have the most task links allowed (200,000 live ones).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: Too many task links.
        '429':
          $ref: '#/components/responses/TooManyRequests'
    delete:
      operationId: unlinkEntryFromTask
      tags:
      - Tasks
      summary: Unlink an entry from a task
      description: 'Removes the link between one of your tasks and one of your entries. The entry stays. Answers `404` if
  
        the two were never linked, and `ok` if the link is already removed.
  
        '
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
          format: uuid
        description: The task id.
      - name: entryId
        in: path
        required: true
        schema:
          type: string
          format: uuid
        description: The entry id.
      responses:
        '200':
          $ref: '#/components/responses/Ok'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
  /v1/subtasks/{id}:
    put:
      operationId: putSubtask
      tags:
      - Tasks
      summary: Create or update a checklist item
      description: 'Creates a checklist item of one of your tasks with the id in the URL, or updates it. The id must be a UUID
  
        you make. A new item needs `task_id` and `title`; for an existing one, fields you leave out keep their
  
        value. The task must be yours, otherwise the answer is `400`.
  
        '
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
          format: uuid
        description: The checklist item id, a UUID you make.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SubtaskWrite'
            example:
              task_id: 3f6d2c1e-5a7b-4c8d-9e0f-1a2b3c4d5e6f
              title: Write a failing test
              done: false
              position: 1
      responses:
        '200':
          description: The checklist item as stored.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SubtaskResponse'
              example:
                subtask:
                  id: 5c0e9b14-3d28-4a76-b1f9-0e7a2d8c4f36
                  task_id: 3f6d2c1e-5a7b-4c8d-9e0f-1a2b3c4d5e6f
                  title: Write a failing test
                  done: false
                  position: 1
                  created_at: 1762156800000
                  client_updated_at: 1762156800000
                  deleted_at: null
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '409':
          description: You already have the most checklist items allowed (100,000 live ones).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: Too many checklist items.
        '429':
          $ref: '#/components/responses/TooManyRequests'
    delete:
      operationId: deleteSubtask
      tags:
      - Tasks
      summary: Delete a checklist item
      description: Deletes one checklist item of yours. Deleting one that is already deleted answers `ok` again.
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
          format: uuid
        description: The checklist item id.
      responses:
        '200':
          $ref: '#/components/responses/Ok'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
  /v1/plan-blocks:
    get:
      operationId: listPlanBlocks
      tags:
      - Tasks
      summary: List planned blocks
      description: 'Your own planned blocks that overlap the range from `from` (inclusive) to `to` (exclusive), oldest first.
  
        These are private to you and are never logged time. The range may span at most 400 days.
  
        '
      parameters:
      - $ref: '#/components/parameters/FromQuery'
      - $ref: '#/components/parameters/ToQuery'
      responses:
        '200':
          description: Planned blocks in the range.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PlanBlockListResponse'
              example:
                plan_blocks:
                - id: c2d5e8f1-4a37-4b90-8e6c-7f1a3b5d9e02
                  project_id: 8b1d7a52-2c64-4e0a-a3f1-6d9c0e4b7f21
                  task_id: 3f6d2c1e-5a7b-4c8d-9e0f-1a2b3c4d5e6f
                  title: Fix the CSV export
                  started_at: 1762160400000
                  ended_at: 1762167600000
                  created_at: 1762156800000
                  client_updated_at: 1762156800000
                  deleted_at: null
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
  /v1/plan-blocks/{id}:
    put:
      operationId: putPlanBlock
      tags:
      - Tasks
      summary: Create or update a planned block
      description: 'Creates a private planned block on your calendar with the id in the URL, or updates it. The id must be a
  
        UUID you make. A new block needs `started_at` and `ended_at`; the end must be after the start and at most 7
  
        days later. For an existing block, fields you leave out keep their value.
  
        '
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
          format: uuid
        description: The planned block id, a UUID you make.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PlanBlockWrite'
            example:
              title: Fix the CSV export
              project_id: 8b1d7a52-2c64-4e0a-a3f1-6d9c0e4b7f21
              task_id: 3f6d2c1e-5a7b-4c8d-9e0f-1a2b3c4d5e6f
              started_at: 1762160400000
              ended_at: 1762167600000
      responses:
        '200':
          description: The planned block as stored.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PlanBlockResponse'
              example:
                plan_block:
                  id: c2d5e8f1-4a37-4b90-8e6c-7f1a3b5d9e02
                  project_id: 8b1d7a52-2c64-4e0a-a3f1-6d9c0e4b7f21
                  task_id: 3f6d2c1e-5a7b-4c8d-9e0f-1a2b3c4d5e6f
                  title: Fix the CSV export
                  started_at: 1762160400000
                  ended_at: 1762167600000
                  created_at: 1762156800000
                  client_updated_at: 1762156800000
                  deleted_at: null
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '409':
          description: You already have the most planned blocks allowed (100,000 live ones).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: Too many planned blocks.
        '429':
          $ref: '#/components/responses/TooManyRequests'
    delete:
      operationId: deletePlanBlock
      tags:
      - Tasks
      summary: Delete a planned block
      description: Deletes one of your planned blocks. Deleting one that is already deleted answers `ok` again.
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
          format: uuid
        description: The planned block id.
      responses:
        '200':
          $ref: '#/components/responses/Ok'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
  /v1/sync/personal:
    post:
      operationId: syncPersonal
      tags:
      - Sync
      x-internal: true
      summary: Sync tasks, checklists, links and planned blocks
      description: 'The Mac app''s round trip for your private data. It applies the rows you send (tasks, then checklist
  
        items, links and planned blocks, so an item may point at a task from the same request), then returns every
  
        row of yours that changed after `cursor`, plus your running timer. Start with no cursor, and keep the
  
        `cursor` you get back. This cursor is separate from the one for `POST /v1/sync`.
  
  
        A write is last-write-wins on `client_updated_at`; a tie goes to the deletion. A time more than 5 minutes
  
        ahead of the server clock counts as now. Rows may leave out any field except the key and
  
        `client_updated_at`; a new live row still needs its required fields. Rows the server refuses come back in
  
        `rejected` and don''t fail the request.
  
  
        When `more` is `true`, call again with the new cursor. When `reset` is `true`, your cursor was older than
  
        deletions the server has since forgotten, so this answer starts from the beginning: after the last page,
  
        drop any row you hold that no page brought.
  
  
        Limits: 2,000 rows per request; 60 requests and 1,200 rows per minute.
  
        '
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PersonalSyncRequest'
            example:
              cursor: 48210
              tasks:
              - id: 3f6d2c1e-5a7b-4c8d-9e0f-1a2b3c4d5e6f
                title: Fix the CSV export
                day: '2025-11-03'
                stage: doing
                client_updated_at: 1762156800000
              subtasks:
              - id: 5c0e9b14-3d28-4a76-b1f9-0e7a2d8c4f36
                task_id: 3f6d2c1e-5a7b-4c8d-9e0f-1a2b3c4d5e6f
                title: Write a failing test
                client_updated_at: 1762156800000
              task_links: []
              plan_blocks: []
      responses:
        '200':
          description: Your rows after the cursor, the running timer and any rows that were refused.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PersonalSyncResponse'
              example:
                cursor: 48233
                tasks:
                - id: 3f6d2c1e-5a7b-4c8d-9e0f-1a2b3c4d5e6f
                  title: Fix the CSV export
                  notes: null
                  day: '2025-11-03'
                  project_id: null
                  stage: doing
                  done_at: null
                  position: 0
                  estimate_minutes: null
                  source: null
                  external_id: null
                  external_key: null
                  external_url: null
                  issue: null
                  created_at: 1762156800000
                  client_updated_at: 1762156800000
                  deleted_at: null
                subtasks:
                - id: 5c0e9b14-3d28-4a76-b1f9-0e7a2d8c4f36
                  task_id: 3f6d2c1e-5a7b-4c8d-9e0f-1a2b3c4d5e6f
                  title: Write a failing test
                  done: false
                  position: 0
                  created_at: 1762156800000
                  client_updated_at: 1762156800000
                  deleted_at: null
                task_links: []
                plan_blocks: []
                timer: null
                rejected: []
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/TooManyRequests'
  /v1/projects:
    get:
      operationId: listProjects
      tags:
      - Projects
      summary: List projects
      description: 'Projects in every workspace you belong to, or in one workspace with `workspace_id`. Deleted projects are
  
        left out. In team workspaces, plain members get `rate` and `budget_amount` as `null`. Ordered by creation time.
  
        '
      parameters:
      - $ref: '#/components/parameters/WorkspaceIdQueryOptional'
      - $ref: '#/components/parameters/ExternalIdQuery'
      responses:
        '200':
          description: The projects.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProjectList'
              example:
                projects:
                - id: 3b0c7f4e-5d1a-4e0b-9a52-6f1c2d8e9a10
                  workspace_id: 9e1f2a34-7b6c-4d5e-8f90-a1b2c3d4e5f6
                  client_id: 5a7d9c20-1e3f-4a6b-8c4d-0f2e1a3b5c7d
                  name: Website redesign
                  color: '#5B8DEF'
                  rate: 95
                  budget_hours: 120
                  archived: false
                  created_at: 1759276800000
                  client_updated_at: 1762156800000
                  deleted_at: null
                  billable_default: true
                  budget_amount: null
                  budget_period: null
                  currency: null
                  requires_tags: false
                  requires_notes: false
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
  /v1/projects/{id}:
    put:
      operationId: putProject
      tags:
      - Projects
      summary: Create or replace a project
      description: 'Creates the project, or replaces it if the id exists. You make the id (a UUID), so a retry is safe.
  
        Owners and admins of the workspace only. Fields you leave out take these values: `client_id`, `rate` and
  
        `budget_hours` become `null`, `archived` becomes `false`, `deleted_at` becomes `null` (so a PUT brings a
  
        deleted project back), and `created_at` keeps the stored value or becomes now. The v2 fields
  
        (`billable_default`, `budget_amount`, `budget_period`, `currency`, `requires_tags`, `requires_notes`) keep
  
        their stored values, or the defaults for a new project. `client_updated_at` is set by the server and ignored
  
        if you send it. A project can''t move to another workspace.
  
        '
      parameters:
      - $ref: '#/components/parameters/IdPath'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ProjectPut'
            example:
              workspace_id: 9e1f2a34-7b6c-4d5e-8f90-a1b2c3d4e5f6
              client_id: 5a7d9c20-1e3f-4a6b-8c4d-0f2e1a3b5c7d
              name: Website redesign
              color: '#5B8DEF'
              rate: 95
              budget_hours: 120
              requires_tags: false
      responses:
        '200':
          description: The project as stored.
          content:
            application/json:
              schema:
                type: object
                required:
                - project
                properties:
                  project:
                    $ref: '#/components/schemas/Project'
              example:
                project:
                  id: 3b0c7f4e-5d1a-4e0b-9a52-6f1c2d8e9a10
                  workspace_id: 9e1f2a34-7b6c-4d5e-8f90-a1b2c3d4e5f6
                  client_id: 5a7d9c20-1e3f-4a6b-8c4d-0f2e1a3b5c7d
                  name: Website redesign
                  color: '#5B8DEF'
                  rate: 95
                  budget_hours: 120
                  archived: false
                  created_at: 1762156800000
                  client_updated_at: 1762156800123
                  deleted_at: null
                  billable_default: true
                  budget_amount: null
                  budget_period: null
                  currency: null
                  requires_tags: false
                  requires_notes: false
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
    delete:
      operationId: deleteProject
      tags:
      - Projects
      summary: Delete a project
      description: 'Soft-deletes the project: the row stays with `deleted_at` set. Owners and admins only. Deleting a project
  
        that is already deleted is not an error.
  
        '
      parameters:
      - $ref: '#/components/parameters/IdPath'
      responses:
        '200':
          $ref: '#/components/responses/Ok'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
  /v1/clients:
    get:
      operationId: listClients
      tags:
      - Clients
      summary: List clients
      description: 'Clients in every workspace you belong to, or in one workspace with `workspace_id`. Deleted clients are
  
        left out. Ordered by name.
  
        '
      parameters:
      - $ref: '#/components/parameters/WorkspaceIdQueryOptional'
      - $ref: '#/components/parameters/ExternalIdQuery'
      responses:
        '200':
          description: The clients.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientList'
              example:
                clients:
                - id: 5a7d9c20-1e3f-4a6b-8c4d-0f2e1a3b5c7d
                  workspace_id: 9e1f2a34-7b6c-4d5e-8f90-a1b2c3d4e5f6
                  name: Northwind Traders
                  email: billing@northwind.example.com
                  address: '12 Harbour Road
  
                    Lisbon'
                  client_updated_at: 1762156800000
                  deleted_at: null
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
  /v1/clients/{id}:
    put:
      operationId: putClient
      tags:
      - Clients
      summary: Create or replace a client
      description: 'Creates the client, or replaces it if the id exists. You make the id (a UUID), so a retry is safe.
  
        Owners and admins of the workspace only. `email` and `address` become `null` when left out, and so does
  
        `deleted_at` (a PUT brings a deleted client back). `client_updated_at` is set by the server. A client can''t
  
        move to another workspace.
  
        '
      parameters:
      - $ref: '#/components/parameters/IdPath'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ClientPut'
            example:
              workspace_id: 9e1f2a34-7b6c-4d5e-8f90-a1b2c3d4e5f6
              name: Northwind Traders
              email: billing@northwind.example.com
              address: '12 Harbour Road
  
                Lisbon'
      responses:
        '200':
          description: The client as stored.
          content:
            application/json:
              schema:
                type: object
                required:
                - client
                properties:
                  client:
                    $ref: '#/components/schemas/Client'
              example:
                client:
                  id: 5a7d9c20-1e3f-4a6b-8c4d-0f2e1a3b5c7d
                  workspace_id: 9e1f2a34-7b6c-4d5e-8f90-a1b2c3d4e5f6
                  name: Northwind Traders
                  email: billing@northwind.example.com
                  address: '12 Harbour Road
  
                    Lisbon'
                  client_updated_at: 1762156800123
                  deleted_at: null
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
    delete:
      operationId: deleteClient
      tags:
      - Clients
      summary: Delete a client
      description: 'Soft-deletes the client. Its projects stay but lose their client (`client_id` becomes `null`). Owners and
  
        admins only. Deleting a client that is already deleted is not an error.
  
        '
      parameters:
      - $ref: '#/components/parameters/IdPath'
      responses:
        '200':
          $ref: '#/components/responses/Ok'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
  /v1/tags:
    get:
      operationId: listTags
      tags:
      - Tags
      summary: List tags
      description: 'Tags in every workspace you belong to, or in one workspace with `workspace_id`. Deleted tags are left out.
  
        Ordered by name. Any member can read them.
  
        '
      parameters:
      - $ref: '#/components/parameters/WorkspaceIdQueryOptional'
      - $ref: '#/components/parameters/ExternalIdQuery'
      responses:
        '200':
          description: The tags.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TagList'
              example:
                tags:
                - id: 7c2e4a60-3b1d-4f8a-9e5c-2d4f6a8b0c1e
                  workspace_id: 9e1f2a34-7b6c-4d5e-8f90-a1b2c3d4e5f6
                  name: Meeting
                  color: '#8A8F98'
                  archived: false
                  client_updated_at: 1762156800000
                  deleted_at: null
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
  /v1/tags/{id}:
    put:
      operationId: putTag
      tags:
      - Tags
      summary: Create or replace a tag
      description: 'Creates the tag, or replaces it if the id exists. The id must be a UUID you make, and is stored in lower
  
        case. Owners and admins of the workspace only. `color` defaults to `#8A8F98`, `archived` to `false`, and
  
        `deleted_at` to `null` (a PUT brings a deleted tag back). `client_updated_at` is set by the server. A tag
  
        can''t move to another workspace.
  
        '
      parameters:
      - $ref: '#/components/parameters/IdPath'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TagPut'
            example:
              workspace_id: 9e1f2a34-7b6c-4d5e-8f90-a1b2c3d4e5f6
              name: Meeting
              color: '#8A8F98'
      responses:
        '200':
          description: The tag as stored.
          content:
            application/json:
              schema:
                type: object
                required:
                - tag
                properties:
                  tag:
                    $ref: '#/components/schemas/Tag'
              example:
                tag:
                  id: 7c2e4a60-3b1d-4f8a-9e5c-2d4f6a8b0c1e
                  workspace_id: 9e1f2a34-7b6c-4d5e-8f90-a1b2c3d4e5f6
                  name: Meeting
                  color: '#8A8F98'
                  archived: false
                  client_updated_at: 1762156800123
                  deleted_at: null
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
    delete:
      operationId: deleteTag
      tags:
      - Tags
      summary: Delete a tag
      description: 'Soft-deletes the tag. Owners and admins only. Deleting a tag that is already deleted is not an error.
  
        '
      parameters:
      - $ref: '#/components/parameters/IdPath'
      responses:
        '200':
          $ref: '#/components/responses/Ok'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
  /v1/workspaces:
    post:
      operationId: createWorkspace
      tags:
      - Workspaces
      summary: Create a team workspace
      description: 'Makes a new team workspace (`kind: "team"`) in your currency, and makes you its owner. The server makes
  
        the id.
  
        '
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WorkspaceCreate'
            example:
              name: Northwind Studio
      responses:
        '200':
          description: The new workspace.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WorkspaceResponse'
              example:
                workspace:
                  id: 9e1f2a34-7b6c-4d5e-8f90-a1b2c3d4e5f6
                  name: Northwind Studio
                  kind: team
                  role: owner
                  currency: USD
                  invoice:
                    company_name: ''
                    company_address: ''
                    tax_id: ''
                    prefix: INV-
                    next_number: 1
                    default_tax_rate: 0
                    payment_terms_days: 14
                    footer: ''
                    payment: {}
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
  /v1/workspaces/{id}:
    patch:
      operationId: patchWorkspace
      tags:
      - Workspaces
      summary: Update a workspace
      description: 'Changes the name, currency or invoice settings. Owners and admins only; send only what changes. A personal
  
        workspace can''t be renamed, and changing its currency changes yours. Payment details (`invoice.payment`)
  
        are stricter: only an owner may change them, and only from a signed-in session. A token gets `403` with
  
        `code: "session_required"`, and an admin gets `403` with `code: "owner_only"`. When a payment detail really
  
        changes, the change is recorded and the owners get an email.
  
        '
      parameters:
      - $ref: '#/components/parameters/WorkspaceIdPath'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WorkspacePatch'
            example:
              name: Northwind Studio
              currency: EUR
              invoice:
                company_name: Northwind Studio GmbH
                prefix: NW-
                default_tax_rate: 19
                payment_terms_days: 14
                payment:
                  account_name: Northwind Studio GmbH
                  iban: DE89 3704 0044 0532 0130 00
      responses:
        '200':
          description: The workspace after the change.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WorkspaceResponse'
              example:
                workspace:
                  id: 9e1f2a34-7b6c-4d5e-8f90-a1b2c3d4e5f6
                  name: Northwind Studio
                  kind: team
                  role: owner
                  currency: EUR
                  invoice:
                    company_name: Northwind Studio GmbH
                    company_address: ''
                    tax_id: ''
                    prefix: NW-
                    next_number: 1
                    default_tax_rate: 19
                    payment_terms_days: 14
                    footer: ''
                    payment:
                      account_name: Northwind Studio GmbH
                      iban: DE89370400440532013000
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: You are not an owner or admin, or you tried to change payment details without being an owner on a signed-in
            session.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                notAdmin:
                  summary: A plain member
                  value:
                    error: Only workspace admins can do that.
                sessionRequired:
                  summary: Payment details from an API token
                  value:
                    error: API tokens can't be used here. Sign in instead.
                    code: session_required
                ownerOnly:
                  summary: Payment details from an admin
                  value:
                    error: Only a workspace owner can change payment details.
                    code: owner_only
        '404':
          $ref: '#/components/responses/NotFound'
  /v1/workspaces/{id}/payment-history:
    get:
      operationId: getPaymentHistory
      tags:
      - Workspaces
      summary: List payment detail changes
      description: 'Who changed the payment details and when: the last 20 changes, newest first. It names the fields, never
  
        their values. Owners only.
  
        '
      parameters:
      - $ref: '#/components/parameters/WorkspaceIdPath'
      responses:
        '200':
          description: The changes.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaymentHistoryResponse'
              example:
                changes:
                - at: 1762156800000
                  fields:
                  - iban
                  - bank_name
                  by:
                    name: Ana Costa
                    email: ana@example.com
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: You are not an owner of this workspace.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: Only a workspace owner can see this.
                code: owner_only
        '404':
          $ref: '#/components/responses/NotFound'
  /v1/workspaces/{id}/members:
    get:
      operationId: listMembers
      tags:
      - Workspaces
      summary: List members and invites
      description: 'Everyone in the workspace, oldest first. Any member can call it. Owners and admins also get the open
  
        invites (not yet expired) and each member''s `cost_rate` and `bill_rate`; for everyone else `invites` is
  
        empty and the two rates are left out.
  
        '
      parameters:
      - $ref: '#/components/parameters/WorkspaceIdPath'
      responses:
        '200':
          description: The members and open invites.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MembersResponse'
              example:
                members:
                - user_id: 2d4f6a8b-0c1e-4a3b-9c5d-7e9f1a2b3c4d
                  email: ana@example.com
                  name: Ana Costa
                  role: owner
                  joined_at: 1759276800000
                  capacity_hours: 40
                  cost_rate: 45
                  bill_rate: null
                invites:
                - id: 8a9b0c1d-2e3f-4a5b-8c6d-7e8f9a0b1c2d
                  email: sam@example.com
                  role: member
                  created_at: 1762156800000
                  expires_at: 1763366400000
                  capacity_hours: 32
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
  /v1/workspaces/{id}/members/{userId}:
    patch:
      operationId: patchMember
      tags:
      - Workspaces
      summary: Update a member
      description: 'Changes a member''s role, weekly capacity, cost rate or bill rate. Owners and admins only; send at least
  
        one field. Changing the **role** needs a signed-in session: a token gets `403` with
  
        `code: "session_required"`. Admins can''t make anyone an owner or change an owner''s role, and a workspace
  
        always keeps at least one owner. Roles in a personal workspace can''t be changed. Setting a rate to `null`
  
        clears it.
  
        '
      parameters:
      - $ref: '#/components/parameters/WorkspaceIdPath'
      - name: userId
        in: path
        required: true
        schema:
          type: string
        description: The member's user id.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/MemberPatch'
            example:
              role: admin
              capacity_hours: 32
              cost_rate: 45
              bill_rate: 90
      responses:
        '200':
          description: The member after the change.
          content:
            application/json:
              schema:
                type: object
                required:
                - member
                properties:
                  member:
                    $ref: '#/components/schemas/Member'
              example:
                member:
                  user_id: 6e8f0a1b-2c3d-4e5f-8a7b-9c0d1e2f3a4b
                  email: sam@example.com
                  name: Sam Okafor
                  role: admin
                  joined_at: 1760486400000
                  capacity_hours: 32
                  cost_rate: 45
                  bill_rate: 90
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: You are not an owner or admin, an admin touched an owner, or the role change came from an API token.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                notAdmin:
                  summary: A plain member
                  value:
                    error: Only workspace admins can do that.
                ownerOnly:
                  summary: An admin changing an owner
                  value:
                    error: Only a workspace owner can do that.
                sessionRequired:
                  summary: A role change from an API token
                  value:
                    error: API tokens can't be used here. Sign in instead.
                    code: session_required
        '404':
          $ref: '#/components/responses/NotFound'
    delete:
      x-badges: [{ name: Session only, color: "var(--scalar-color-orange)" }]
      operationId: removeMember
      tags:
      - Workspaces
      summary: Remove a member
      description: 'Removes a member from the workspace. Owners and admins can remove others, and anyone can remove
  
        themselves (leave). Admins can''t remove an owner, and the last owner can''t leave. The person''s entries
  
        stay. They also leave the workspace''s teams, and the webhooks, report links and report emails they made
  
        there stop. You can''t leave a personal workspace. Needs a signed-in session.
  
        '
      security:
      - session: []
      parameters:
      - $ref: '#/components/parameters/WorkspaceIdPath'
      - name: userId
        in: path
        required: true
        schema:
          type: string
        description: The member's user id. Use your own to leave.
      responses:
        '200':
          $ref: '#/components/responses/Ok'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: You need a signed-in session, or you may not remove this person.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                sessionRequired:
                  summary: An API token
                  value:
                    error: API tokens can't be used here. Sign in instead.
                    code: session_required
                notAdmin:
                  summary: A plain member removing someone else
                  value:
                    error: Only workspace admins can do that.
                ownerOnly:
                  summary: An admin removing an owner
                  value:
                    error: Only a workspace owner can do that.
        '404':
          $ref: '#/components/responses/NotFound'
  /v1/workspaces/{id}/invites:
    post:
      x-badges: [{ name: Session only, color: "var(--scalar-color-orange)" }]
      operationId: createInvite
      tags:
      - Invites
      summary: Invite someone
      description: 'Emails an invitation to join a team workspace. Owners and admins only, and it needs a signed-in session.
  
        The link works for 14 days and only for the invited address. Inviting an address again replaces the earlier
  
        invite. Limits: 50 invites a day per workspace, 100 a day per person, 5 a day per recipient (`429`, resets
  
        at midnight UTC). You can''t invite people to a personal workspace or someone who is already a member.
  
        '
      security:
      - session: []
      parameters:
      - $ref: '#/components/parameters/WorkspaceIdPath'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/InviteCreate'
            example:
              email: sam@example.com
              role: member
              capacity_hours: 32
      responses:
        '200':
          description: The invite. The token is only in the email.
          content:
            application/json:
              schema:
                type: object
                required:
                - invite
                properties:
                  invite:
                    $ref: '#/components/schemas/Invite'
              example:
                invite:
                  id: 8a9b0c1d-2e3f-4a5b-8c6d-7e8f9a0b1c2d
                  email: sam@example.com
                  role: member
                  created_at: 1762156800000
                  expires_at: 1763366400000
                  capacity_hours: 32
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: You need a signed-in session, you are not an owner or admin, or a test account broke the test account
            rules.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                sessionRequired:
                  summary: An API token
                  value:
                    error: API tokens can't be used here. Sign in instead.
                    code: session_required
                notAdmin:
                  summary: A plain member
                  value:
                    error: Only workspace admins can do that.
                testAccount:
                  summary: A test account (they can't invite, share, schedule, add hooks or delete)
                  value:
                    error: 'Test accounts can''t do that: they''re shared, so links, invites, hooks and deletes are off.'
                    code: test_account
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '502':
          description: The email could not be sent. The invite is not kept.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: Couldn't send the email. Try again in a minute.
        '503':
          description: This server can't send email yet.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: Email invites open soon. Until then this invitation can't be sent.
                code: email_unavailable
  /v1/workspaces/{id}/invites/{inviteId}:
    patch:
      operationId: patchInvite
      tags:
      - Invites
      summary: Update an invite
      description: 'Changes the weekly hours an invited person will have once they join. Owners and admins only. It works
  
        with an API token. Only invites that haven''t expired can be changed.
  
        '
      parameters:
      - $ref: '#/components/parameters/WorkspaceIdPath'
      - name: inviteId
        in: path
        required: true
        schema:
          type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/InvitePatch'
            example:
              capacity_hours: 24
      responses:
        '200':
          description: The invite after the change.
          content:
            application/json:
              schema:
                type: object
                required:
                - invite
                properties:
                  invite:
                    $ref: '#/components/schemas/Invite'
              example:
                invite:
                  id: 8a9b0c1d-2e3f-4a5b-8c6d-7e8f9a0b1c2d
                  email: sam@example.com
                  role: member
                  created_at: 1762156800000
                  expires_at: 1763366400000
                  capacity_hours: 24
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
    delete:
      x-badges: [{ name: Session only, color: "var(--scalar-color-orange)" }]
      operationId: deleteInvite
      tags:
      - Invites
      summary: Cancel an invite
      description: 'Deletes an open invite, so its link stops working. Owners and admins only, and it needs a signed-in
  
        session.
  
        '
      security:
      - session: []
      parameters:
      - $ref: '#/components/parameters/WorkspaceIdPath'
      - name: inviteId
        in: path
        required: true
        schema:
          type: string
      responses:
        '200':
          $ref: '#/components/responses/Ok'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: You need a signed-in session, or you are not an owner or admin.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                sessionRequired:
                  summary: An API token
                  value:
                    error: API tokens can't be used here. Sign in instead.
                    code: session_required
                notAdmin:
                  summary: A plain member
                  value:
                    error: Only workspace admins can do that.
        '404':
          $ref: '#/components/responses/NotFound'
  /v1/invites/accept:
    post:
      operationId: acceptInvite
      tags:
      - Invites
      summary: Accept an invite
      description: 'Joins the workspace the invite was sent for. You must be signed in as the invited email address. The token
  
        comes from the link in the invitation email (`/join?token=…`). It works with an API token. The invite is
  
        used up.
  
        '
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/InviteAccept'
            example:
              token: Zm9vYmFyYmF6cXV4MTIzNDU2Nzg5MGFiY2RlZmdoaWo
      responses:
        '200':
          description: The workspace you joined.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WorkspaceResponse'
              example:
                workspace:
                  id: 9e1f2a34-7b6c-4d5e-8f90-a1b2c3d4e5f6
                  name: Northwind Studio
                  kind: team
                  role: member
                  currency: USD
                  invoice: null
        '400':
          description: The token is wrong, used or expired.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: This invite link is invalid or has expired.
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: The invite was sent to a different email address.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: This invite was sent to a different email address. Sign in with that address to accept it.
  /v1/workspaces/{id}/teams:
    get:
      operationId: listTeams
      tags:
      - Teams
      summary: List teams
      description: 'The teams in a workspace, ordered by name, each with its members and leads. Any member can call it.
  
        Only people who are still members of the workspace are listed.
  
        '
      parameters:
      - $ref: '#/components/parameters/WorkspaceIdPath'
      responses:
        '200':
          description: The teams.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TeamList'
              example:
                teams:
                - id: 4c6e8a0b-1d2f-4a3b-9c5d-6e7f8a9b0c1d
                  workspace_id: 9e1f2a34-7b6c-4d5e-8f90-a1b2c3d4e5f6
                  name: Design
                  color: '#6A5BC2'
                  created_at: 1759276800000
                  members:
                  - user_id: 2d4f6a8b-0c1e-4a3b-9c5d-7e9f1a2b3c4d
                    is_lead: true
                  - user_id: 6e8f0a1b-2c3d-4e5f-8a7b-9c0d1e2f3a4b
                    is_lead: false
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
  /v1/workspaces/{id}/teams/{teamId}:
    put:
      operationId: putTeam
      tags:
      - Teams
      summary: Create or rename a team
      description: 'Creates the team, or changes its name and colour if the id exists. You make the id (a UUID). Owners and
  
        admins only. It leaves the member list alone (use the members endpoint), and it brings a deleted team
  
        back. Only team workspaces have teams, at most 200 each. A new team has no members.
  
        '
      parameters:
      - $ref: '#/components/parameters/WorkspaceIdPath'
      - name: teamId
        in: path
        required: true
        schema:
          type: string
          format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TeamPut'
            example:
              name: Design
              color: '#6A5BC2'
      responses:
        '200':
          description: The team.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TeamResponse'
              example:
                team:
                  id: 4c6e8a0b-1d2f-4a3b-9c5d-6e7f8a9b0c1d
                  workspace_id: 9e1f2a34-7b6c-4d5e-8f90-a1b2c3d4e5f6
                  name: Design
                  color: '#6A5BC2'
                  created_at: 1762156800000
                  members: []
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
    delete:
      operationId: deleteTeam
      tags:
      - Teams
      summary: Delete a team
      description: Deletes the team and empties its member list. Owners and admins only. The people stay in the workspace.
      parameters:
      - $ref: '#/components/parameters/WorkspaceIdPath'
      - name: teamId
        in: path
        required: true
        schema:
          type: string
          format: uuid
      responses:
        '200':
          $ref: '#/components/responses/Ok'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
  /v1/workspaces/{id}/teams/{teamId}/members:
    put:
      operationId: putTeamMembers
      tags:
      - Teams
      summary: Set a team's members
      description: 'Replaces the whole member list of the team. Owners and admins only. Everyone must already be a member of
  
        the workspace; at most 500. Send a list of `{ user_id, is_lead }`, or an object with that list under
  
        `members`. Send an empty list to empty the team. A person listed twice is a lead if any entry says so.
  
        '
      parameters:
      - $ref: '#/components/parameters/WorkspaceIdPath'
      - name: teamId
        in: path
        required: true
        schema:
          type: string
          format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              oneOf:
              - $ref: '#/components/schemas/TeamMemberList'
              - type: object
                required:
                - members
                properties:
                  members:
                    $ref: '#/components/schemas/TeamMemberList'
            example:
            - user_id: 2d4f6a8b-0c1e-4a3b-9c5d-7e9f1a2b3c4d
              is_lead: true
            - user_id: 6e8f0a1b-2c3d-4e5f-8a7b-9c0d1e2f3a4b
      responses:
        '200':
          description: The team with its new members.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TeamResponse'
              example:
                team:
                  id: 4c6e8a0b-1d2f-4a3b-9c5d-6e7f8a9b0c1d
                  workspace_id: 9e1f2a34-7b6c-4d5e-8f90-a1b2c3d4e5f6
                  name: Design
                  color: '#6A5BC2'
                  created_at: 1759276800000
                  members:
                  - user_id: 2d4f6a8b-0c1e-4a3b-9c5d-7e9f1a2b3c4d
                    is_lead: true
                  - user_id: 6e8f0a1b-2c3d-4e5f-8a7b-9c0d1e2f3a4b
                    is_lead: false
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
  /v1/workspaces/{id}/slack:
    get:
      x-badges: [{ name: Session only, color: "var(--scalar-color-orange)" }]
      operationId: getSlack
      tags:
      - Integrations
      summary: Get the Slack connection
      description: 'Whether the workspace posts its weekly summary to Slack. Owners and admins only, from a signed-in session.
  
        The webhook URL is a secret and is never returned: you get its last 4 characters as a hint.
  
        '
      security:
      - session: []
      parameters:
      - $ref: '#/components/parameters/WorkspaceIdPath'
      responses:
        '200':
          description: The connection. When Slack isn't connected, `connected` is false and the rest is empty.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SlackConnection'
              example:
                connected: true
                hint: aB3d
                last_status: ok
                last_sent_at: 1762156800000
                include_people: false
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: You need a signed-in session, or you are not an owner or admin.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                sessionRequired:
                  summary: An API token
                  value:
                    error: API tokens can't be used here. Sign in instead.
                    code: session_required
                notAdmin:
                  summary: A plain member
                  value:
                    error: Only workspace admins can do that.
        '404':
          $ref: '#/components/responses/NotFound'
    put:
      x-badges: [{ name: Session only, color: "var(--scalar-color-orange)" }]
      operationId: putSlack
      tags:
      - Integrations
      summary: Connect or change Slack
      description: 'Send a `webhook_url` to connect Slack, or to replace the URL (the people setting is kept unless you send
  
        `include_people` too). Send only `include_people` to change that setting on an existing connection; with
  
        no connection yet that answers `404`. The URL must be a Slack incoming webhook:
  
        `https://hooks.slack.com/services/…`. Owners and admins only, from a signed-in session. Limited to 20
  
        changes an hour per person.
  
        '
      security:
      - session: []
      parameters:
      - $ref: '#/components/parameters/WorkspaceIdPath'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SlackPut'
            example:
              webhook_url: https://hooks.slack.com/services/T0000000/B0000000/XXXXXXXXXXXXXXXXXXXXaB3d
              include_people: false
      responses:
        '200':
          description: The connection after the change.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SlackConnection'
              example:
                connected: true
                hint: aB3d
                last_status: null
                last_sent_at: null
                include_people: false
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: You need a signed-in session, or you are not an owner or admin.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                sessionRequired:
                  summary: An API token
                  value:
                    error: API tokens can't be used here. Sign in instead.
                    code: session_required
                notAdmin:
                  summary: A plain member
                  value:
                    error: Only workspace admins can do that.
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
    delete:
      x-badges: [{ name: Session only, color: "var(--scalar-color-orange)" }]
      operationId: deleteSlack
      tags:
      - Integrations
      summary: Disconnect Slack
      description: Removes the stored webhook URL, so nothing more is posted. Owners and admins only, from a signed-in session.
        Disconnecting when nothing is connected is not an error.
      security:
      - session: []
      parameters:
      - $ref: '#/components/parameters/WorkspaceIdPath'
      responses:
        '200':
          $ref: '#/components/responses/Ok'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: You need a signed-in session, or you are not an owner or admin.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                sessionRequired:
                  summary: An API token
                  value:
                    error: API tokens can't be used here. Sign in instead.
                    code: session_required
                notAdmin:
                  summary: A plain member
                  value:
                    error: Only workspace admins can do that.
        '404':
          $ref: '#/components/responses/NotFound'
  /v1/workspaces/{id}/slack/test:
    post:
      x-badges: [{ name: Session only, color: "var(--scalar-color-orange)" }]
      operationId: testSlack
      tags:
      - Integrations
      summary: Send a test message to Slack
      description: 'Posts one short test message to the connected channel and records whether Slack accepted it. Owners and
  
        admins only, from a signed-in session. Limited to 5 tests an hour per person. Answers `404` if Slack isn''t
  
        connected, and `502` if Slack refused the message.
  
        '
      security:
      - session: []
      parameters:
      - $ref: '#/components/parameters/WorkspaceIdPath'
      responses:
        '200':
          $ref: '#/components/responses/Ok'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: You need a signed-in session, or you are not an owner or admin.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                sessionRequired:
                  summary: An API token
                  value:
                    error: API tokens can't be used here. Sign in instead.
                    code: session_required
                notAdmin:
                  summary: A plain member
                  value:
                    error: Only workspace admins can do that.
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '502':
          description: Slack didn't accept the message.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: Slack didn't accept that message. Check the webhook URL and that the app is still in the channel.
webhooks:
  entry.created:
    post:
      operationId: webhookEntryCreated
      tags:
      - Webhooks
      summary: Entry created
      description: 'Sent when a new entry is saved, whether from the app, the API or a sync.
  
        The body holds the entry''s synced fields only: the same object the entries endpoints return.
  
        The request is `application/json` with `User-Agent: Hourbrook-Webhooks/1`. Delivery is best effort: a 5 second
  
        timeout, no redirects, no retries.
  
        '
      security: []
      parameters:
      - name: X-Hourbrook-Event
        in: header
        required: true
        schema:
          type: string
          const: entry.created
        description: The event type, same as `type` in the body.
      - name: X-Hourbrook-Delivery
        in: header
        required: true
        schema:
          type: string
          format: uuid
        description: A fresh id for this delivery, same as `id` in the body.
      - name: X-Hourbrook-Signature
        in: header
        required: true
        schema:
          type: string
          pattern: ^sha256=[0-9a-f]{64}$
        description: '`sha256=` and the hex HMAC-SHA256 of the raw request body, keyed with the webhook''s secret. Compare in
  
          constant time. Reject old `created_at` values to stop replays.
  
          '
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WebhookEntryEvent'
            example:
              id: 6f1c8e2a-3b54-4d97-a0c1-5e9d7b2f4a38
              type: entry.created
              created_at: 1762160400000
              workspace_id: 9b1f6a4e-3c2d-4e8a-8f10-5d7c2b9a1e01
              data:
                entry:
                  id: a17e5c93-2b60-4d48-9e1f-6c3b8d0f4a27
                  project_id: 7c1d3a52-6b0e-4f6a-9d3e-2a8f5b7c9e10
                  title: Homepage hero
                  started_at: 1762160400000
                  ended_at: 1762165800000
                  source: manual
                  client_updated_at: 1762165801000
                  deleted_at: null
                  notes: null
                  billable: true
                  tag_ids: []
                  invoice_id: null
                  billed_at: null
                  user_id: 5a2e8c14-9b3d-4f71-a6c2-1d0e7b4f8a93
                  workspace_id: 9b1f6a4e-3c2d-4e8a-8f10-5d7c2b9a1e01
      responses:
        '200':
          description: Any 2xx marks the delivery ok.
  entry.updated:
    post:
      operationId: webhookEntryUpdated
      tags:
      - Webhooks
      summary: Entry updated
      description: 'Sent when an existing entry changes, including when it is marked billed or put on an invoice.
  
        The body holds the entry''s synced fields only: the same object the entries endpoints return.
  
        The request is `application/json` with `User-Agent: Hourbrook-Webhooks/1`. Delivery is best effort: a 5 second
  
        timeout, no redirects, no retries.
  
        '
      security: []
      parameters:
      - name: X-Hourbrook-Event
        in: header
        required: true
        schema:
          type: string
          const: entry.updated
        description: The event type, same as `type` in the body.
      - name: X-Hourbrook-Delivery
        in: header
        required: true
        schema:
          type: string
          format: uuid
        description: A fresh id for this delivery, same as `id` in the body.
      - name: X-Hourbrook-Signature
        in: header
        required: true
        schema:
          type: string
          pattern: ^sha256=[0-9a-f]{64}$
        description: '`sha256=` and the hex HMAC-SHA256 of the raw request body, keyed with the webhook''s secret. Compare in
  
          constant time. Reject old `created_at` values to stop replays.
  
          '
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WebhookEntryEvent'
            example:
              id: 0d4a7b19-8c36-4e52-b1f8-3a6c9e5d2b70
              type: entry.updated
              created_at: 1762160400000
              workspace_id: 9b1f6a4e-3c2d-4e8a-8f10-5d7c2b9a1e01
              data:
                entry:
                  id: a17e5c93-2b60-4d48-9e1f-6c3b8d0f4a27
                  project_id: 7c1d3a52-6b0e-4f6a-9d3e-2a8f5b7c9e10
                  title: Homepage hero
                  started_at: 1762160400000
                  ended_at: 1762165800000
                  source: manual
                  client_updated_at: 1762165801000
                  deleted_at: null
                  notes: null
                  billable: true
                  tag_ids: []
                  invoice_id: null
                  billed_at: null
                  user_id: 5a2e8c14-9b3d-4f71-a6c2-1d0e7b4f8a93
                  workspace_id: 9b1f6a4e-3c2d-4e8a-8f10-5d7c2b9a1e01
      responses:
        '200':
          description: Any 2xx marks the delivery ok.
  entry.deleted:
    post:
      operationId: webhookEntryDeleted
      tags:
      - Webhooks
      summary: Entry deleted
      description: 'Sent when an entry is deleted. The entry comes with `deleted_at` set.
  
        The body holds the entry''s synced fields only: the same object the entries endpoints return.
  
        The request is `application/json` with `User-Agent: Hourbrook-Webhooks/1`. Delivery is best effort: a 5 second
  
        timeout, no redirects, no retries.
  
        '
      security: []
      parameters:
      - name: X-Hourbrook-Event
        in: header
        required: true
        schema:
          type: string
          const: entry.deleted
        description: The event type, same as `type` in the body.
      - name: X-Hourbrook-Delivery
        in: header
        required: true
        schema:
          type: string
          format: uuid
        description: A fresh id for this delivery, same as `id` in the body.
      - name: X-Hourbrook-Signature
        in: header
        required: true
        schema:
          type: string
          pattern: ^sha256=[0-9a-f]{64}$
        description: '`sha256=` and the hex HMAC-SHA256 of the raw request body, keyed with the webhook''s secret. Compare in
  
          constant time. Reject old `created_at` values to stop replays.
  
          '
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WebhookEntryEvent'
            example:
              id: b92e6d05-1a47-4c83-8f3e-7d0b5a9c2e14
              type: entry.deleted
              created_at: 1762160400000
              workspace_id: 9b1f6a4e-3c2d-4e8a-8f10-5d7c2b9a1e01
              data:
                entry:
                  id: a17e5c93-2b60-4d48-9e1f-6c3b8d0f4a27
                  project_id: 7c1d3a52-6b0e-4f6a-9d3e-2a8f5b7c9e10
                  title: Homepage hero
                  started_at: 1762160400000
                  ended_at: 1762165800000
                  source: manual
                  client_updated_at: 1762165801000
                  deleted_at: 1762170000000
                  notes: null
                  billable: true
                  tag_ids: []
                  invoice_id: null
                  billed_at: null
                  user_id: 5a2e8c14-9b3d-4f71-a6c2-1d0e7b4f8a93
                  workspace_id: 9b1f6a4e-3c2d-4e8a-8f10-5d7c2b9a1e01
      responses:
        '200':
          description: Any 2xx marks the delivery ok.
  client.created:
    post:
      operationId: webhookClientCreated
      tags:
      - Webhooks
      summary: Client created
      description: |
        Sent when a new client is saved, from the app, the API or a sync.
        The body holds the client as the API returns it to owners and admins. Subscribe to it when you create or
        change the webhook; it isn't in the default list. Same headers, signature and best-effort delivery as the
        entry events.
      security: []
      parameters:
      - name: X-Hourbrook-Event
        in: header
        required: true
        schema:
          type: string
          const: client.created
        description: The event type, same as `type` in the body.
      - name: X-Hourbrook-Delivery
        in: header
        required: true
        schema:
          type: string
          format: uuid
        description: A fresh id for this delivery, same as `id` in the body.
      - name: X-Hourbrook-Signature
        in: header
        required: true
        schema:
          type: string
          pattern: ^sha256=[0-9a-f]{64}$
        description: "`sha256=` and the hex HMAC-SHA256 of the raw request body, keyed with the webhook's secret."
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WebhookClientEvent'
            example:
              id: 1a7e3c90-5d24-4b8f-9e61-0c3f8a2d7b45
              type: client.created
              created_at: 1762160400000
              workspace_id: 9b1f6a4e-3c2d-4e8a-8f10-5d7c2b9a1e01
              data:
                client:
                  id: 3d8b6f21-4c70-4a9e-b5d2-8e1f0c7a9b34
                  workspace_id: 9b1f6a4e-3c2d-4e8a-8f10-5d7c2b9a1e01
                  name: Fernhill Coffee
                  email: accounts@fernhill.example
                  address: null
                  client_updated_at: 1762160400000
                  deleted_at: null
                  external_id: linear-team-FERN
      responses:
        '200':
          description: Any 2xx marks the delivery ok.
  client.updated:
    post:
      operationId: webhookClientUpdated
      tags:
      - Webhooks
      summary: Client updated
      description: |
        Sent when a client changes.
        The body holds the client as the API returns it to owners and admins. Subscribe to it when you create or
        change the webhook; it isn't in the default list. Same headers, signature and best-effort delivery as the
        entry events.
      security: []
      parameters:
      - name: X-Hourbrook-Event
        in: header
        required: true
        schema:
          type: string
          const: client.updated
        description: The event type, same as `type` in the body.
      - name: X-Hourbrook-Delivery
        in: header
        required: true
        schema:
          type: string
          format: uuid
        description: A fresh id for this delivery, same as `id` in the body.
      - name: X-Hourbrook-Signature
        in: header
        required: true
        schema:
          type: string
          pattern: ^sha256=[0-9a-f]{64}$
        description: "`sha256=` and the hex HMAC-SHA256 of the raw request body, keyed with the webhook's secret."
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WebhookClientEvent'
            example:
              id: 2b8f4d01-6e35-4c90-af72-1d409b3e8c56
              type: client.updated
              created_at: 1762160400000
              workspace_id: 9b1f6a4e-3c2d-4e8a-8f10-5d7c2b9a1e01
              data:
                client:
                  id: 3d8b6f21-4c70-4a9e-b5d2-8e1f0c7a9b34
                  workspace_id: 9b1f6a4e-3c2d-4e8a-8f10-5d7c2b9a1e01
                  name: Fernhill Coffee
                  email: accounts@fernhill.example
                  address: null
                  client_updated_at: 1762160400000
                  deleted_at: null
                  external_id: linear-team-FERN
      responses:
        '200':
          description: Any 2xx marks the delivery ok.
  client.deleted:
    post:
      operationId: webhookClientDeleted
      tags:
      - Webhooks
      summary: Client deleted
      description: |
        Sent when a client is deleted. The client comes with `deleted_at` set; its projects lose their client and each gets a `project.updated`.
        The body holds the client as the API returns it to owners and admins. Subscribe to it when you create or
        change the webhook; it isn't in the default list. Same headers, signature and best-effort delivery as the
        entry events.
      security: []
      parameters:
      - name: X-Hourbrook-Event
        in: header
        required: true
        schema:
          type: string
          const: client.deleted
        description: The event type, same as `type` in the body.
      - name: X-Hourbrook-Delivery
        in: header
        required: true
        schema:
          type: string
          format: uuid
        description: A fresh id for this delivery, same as `id` in the body.
      - name: X-Hourbrook-Signature
        in: header
        required: true
        schema:
          type: string
          pattern: ^sha256=[0-9a-f]{64}$
        description: "`sha256=` and the hex HMAC-SHA256 of the raw request body, keyed with the webhook's secret."
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WebhookClientEvent'
            example:
              id: 3c905e12-7f46-4da1-b083-2e51ac4f9d67
              type: client.deleted
              created_at: 1762160400000
              workspace_id: 9b1f6a4e-3c2d-4e8a-8f10-5d7c2b9a1e01
              data:
                client:
                  id: 3d8b6f21-4c70-4a9e-b5d2-8e1f0c7a9b34
                  workspace_id: 9b1f6a4e-3c2d-4e8a-8f10-5d7c2b9a1e01
                  name: Fernhill Coffee
                  email: accounts@fernhill.example
                  address: null
                  client_updated_at: 1762160400000
                  deleted_at: 1762164000000
                  external_id: linear-team-FERN
      responses:
        '200':
          description: Any 2xx marks the delivery ok.
  project.created:
    post:
      operationId: webhookProjectCreated
      tags:
      - Webhooks
      summary: Project created
      description: |
        Sent when a new project is saved, from the app, the API or a sync.
        The body holds the project as the API returns it to owners and admins. Subscribe to it when you create or
        change the webhook; it isn't in the default list. Same headers, signature and best-effort delivery as the
        entry events.
      security: []
      parameters:
      - name: X-Hourbrook-Event
        in: header
        required: true
        schema:
          type: string
          const: project.created
        description: The event type, same as `type` in the body.
      - name: X-Hourbrook-Delivery
        in: header
        required: true
        schema:
          type: string
          format: uuid
        description: A fresh id for this delivery, same as `id` in the body.
      - name: X-Hourbrook-Signature
        in: header
        required: true
        schema:
          type: string
          pattern: ^sha256=[0-9a-f]{64}$
        description: "`sha256=` and the hex HMAC-SHA256 of the raw request body, keyed with the webhook's secret."
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WebhookProjectEvent'
            example:
              id: 4da16f23-8057-4eb2-81c4-3f62bd50ae78
              type: project.created
              created_at: 1762160400000
              workspace_id: 9b1f6a4e-3c2d-4e8a-8f10-5d7c2b9a1e01
              data:
                project:
                  id: 7c1d3a52-6b0e-4f6a-9d3e-2a8f5b7c9e10
                  workspace_id: 9b1f6a4e-3c2d-4e8a-8f10-5d7c2b9a1e01
                  client_id: 3d8b6f21-4c70-4a9e-b5d2-8e1f0c7a9b34
                  name: Website redesign
                  color: '#3366FF'
                  rate: 90
                  budget_hours: 120
                  archived: false
                  created_at: 1762156800000
                  client_updated_at: 1762160400000
                  deleted_at: null
                  billable_default: true
                  budget_amount: null
                  budget_period: null
                  currency: null
                  requires_tags: false
                  requires_notes: false
                  external_id: jira-project-10042
      responses:
        '200':
          description: Any 2xx marks the delivery ok.
  project.updated:
    post:
      operationId: webhookProjectUpdated
      tags:
      - Webhooks
      summary: Project updated
      description: |
        Sent when a project changes, archiving included, and when it loses its client because the client was deleted.
        The body holds the project as the API returns it to owners and admins. Subscribe to it when you create or
        change the webhook; it isn't in the default list. Same headers, signature and best-effort delivery as the
        entry events.
      security: []
      parameters:
      - name: X-Hourbrook-Event
        in: header
        required: true
        schema:
          type: string
          const: project.updated
        description: The event type, same as `type` in the body.
      - name: X-Hourbrook-Delivery
        in: header
        required: true
        schema:
          type: string
          format: uuid
        description: A fresh id for this delivery, same as `id` in the body.
      - name: X-Hourbrook-Signature
        in: header
        required: true
        schema:
          type: string
          pattern: ^sha256=[0-9a-f]{64}$
        description: "`sha256=` and the hex HMAC-SHA256 of the raw request body, keyed with the webhook's secret."
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WebhookProjectEvent'
            example:
              id: 5eb27034-9168-4fc3-92d5-4073ce61bf89
              type: project.updated
              created_at: 1762160400000
              workspace_id: 9b1f6a4e-3c2d-4e8a-8f10-5d7c2b9a1e01
              data:
                project:
                  id: 7c1d3a52-6b0e-4f6a-9d3e-2a8f5b7c9e10
                  workspace_id: 9b1f6a4e-3c2d-4e8a-8f10-5d7c2b9a1e01
                  client_id: 3d8b6f21-4c70-4a9e-b5d2-8e1f0c7a9b34
                  name: Website redesign
                  color: '#3366FF'
                  rate: 90
                  budget_hours: 120
                  archived: false
                  created_at: 1762156800000
                  client_updated_at: 1762160400000
                  deleted_at: null
                  billable_default: true
                  budget_amount: null
                  budget_period: null
                  currency: null
                  requires_tags: false
                  requires_notes: false
                  external_id: jira-project-10042
      responses:
        '200':
          description: Any 2xx marks the delivery ok.
  project.deleted:
    post:
      operationId: webhookProjectDeleted
      tags:
      - Webhooks
      summary: Project deleted
      description: |
        Sent when a project is deleted. The project comes with `deleted_at` set.
        The body holds the project as the API returns it to owners and admins. Subscribe to it when you create or
        change the webhook; it isn't in the default list. Same headers, signature and best-effort delivery as the
        entry events.
      security: []
      parameters:
      - name: X-Hourbrook-Event
        in: header
        required: true
        schema:
          type: string
          const: project.deleted
        description: The event type, same as `type` in the body.
      - name: X-Hourbrook-Delivery
        in: header
        required: true
        schema:
          type: string
          format: uuid
        description: A fresh id for this delivery, same as `id` in the body.
      - name: X-Hourbrook-Signature
        in: header
        required: true
        schema:
          type: string
          pattern: ^sha256=[0-9a-f]{64}$
        description: "`sha256=` and the hex HMAC-SHA256 of the raw request body, keyed with the webhook's secret."
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WebhookProjectEvent'
            example:
              id: 6fc38145-a279-40d4-a3e6-5184df72c09a
              type: project.deleted
              created_at: 1762160400000
              workspace_id: 9b1f6a4e-3c2d-4e8a-8f10-5d7c2b9a1e01
              data:
                project:
                  id: 7c1d3a52-6b0e-4f6a-9d3e-2a8f5b7c9e10
                  workspace_id: 9b1f6a4e-3c2d-4e8a-8f10-5d7c2b9a1e01
                  client_id: 3d8b6f21-4c70-4a9e-b5d2-8e1f0c7a9b34
                  name: Website redesign
                  color: '#3366FF'
                  rate: 90
                  budget_hours: 120
                  archived: false
                  created_at: 1762156800000
                  client_updated_at: 1762160400000
                  deleted_at: 1762164000000
                  billable_default: true
                  budget_amount: null
                  budget_period: null
                  currency: null
                  requires_tags: false
                  requires_notes: false
                  external_id: jira-project-10042
      responses:
        '200':
          description: Any 2xx marks the delivery ok.
  tag.created:
    post:
      operationId: webhookTagCreated
      tags:
      - Webhooks
      summary: Tag created
      description: |
        Sent when a new tag is saved, from the app, the API or a sync.
        The body holds the tag as the API returns it to owners and admins. Subscribe to it when you create or
        change the webhook; it isn't in the default list. Same headers, signature and best-effort delivery as the
        entry events.
      security: []
      parameters:
      - name: X-Hourbrook-Event
        in: header
        required: true
        schema:
          type: string
          const: tag.created
        description: The event type, same as `type` in the body.
      - name: X-Hourbrook-Delivery
        in: header
        required: true
        schema:
          type: string
          format: uuid
        description: A fresh id for this delivery, same as `id` in the body.
      - name: X-Hourbrook-Signature
        in: header
        required: true
        schema:
          type: string
          pattern: ^sha256=[0-9a-f]{64}$
        description: "`sha256=` and the hex HMAC-SHA256 of the raw request body, keyed with the webhook's secret."
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WebhookTagEvent'
            example:
              id: 70d49256-b38a-41e5-b4f7-6295e083d1ab
              type: tag.created
              created_at: 1762160400000
              workspace_id: 9b1f6a4e-3c2d-4e8a-8f10-5d7c2b9a1e01
              data:
                tag:
                  id: 0b6d1f4e-7d52-4a9b-a3c1-6e8d2f9b4c20
                  workspace_id: 9b1f6a4e-3c2d-4e8a-8f10-5d7c2b9a1e01
                  name: Design
                  color: '#22AA66'
                  archived: false
                  client_updated_at: 1762160400000
                  deleted_at: null
                  external_id: null
      responses:
        '200':
          description: Any 2xx marks the delivery ok.
  tag.updated:
    post:
      operationId: webhookTagUpdated
      tags:
      - Webhooks
      summary: Tag updated
      description: |
        Sent when a tag changes, archiving included.
        The body holds the tag as the API returns it to owners and admins. Subscribe to it when you create or
        change the webhook; it isn't in the default list. Same headers, signature and best-effort delivery as the
        entry events.
      security: []
      parameters:
      - name: X-Hourbrook-Event
        in: header
        required: true
        schema:
          type: string
          const: tag.updated
        description: The event type, same as `type` in the body.
      - name: X-Hourbrook-Delivery
        in: header
        required: true
        schema:
          type: string
          format: uuid
        description: A fresh id for this delivery, same as `id` in the body.
      - name: X-Hourbrook-Signature
        in: header
        required: true
        schema:
          type: string
          pattern: ^sha256=[0-9a-f]{64}$
        description: "`sha256=` and the hex HMAC-SHA256 of the raw request body, keyed with the webhook's secret."
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WebhookTagEvent'
            example:
              id: 81e5a367-c49b-42f6-8508-73a6f194e2bc
              type: tag.updated
              created_at: 1762160400000
              workspace_id: 9b1f6a4e-3c2d-4e8a-8f10-5d7c2b9a1e01
              data:
                tag:
                  id: 0b6d1f4e-7d52-4a9b-a3c1-6e8d2f9b4c20
                  workspace_id: 9b1f6a4e-3c2d-4e8a-8f10-5d7c2b9a1e01
                  name: Design
                  color: '#22AA66'
                  archived: false
                  client_updated_at: 1762160400000
                  deleted_at: null
                  external_id: null
      responses:
        '200':
          description: Any 2xx marks the delivery ok.
  tag.deleted:
    post:
      operationId: webhookTagDeleted
      tags:
      - Webhooks
      summary: Tag deleted
      description: |
        Sent when a tag is deleted. The tag comes with `deleted_at` set.
        The body holds the tag as the API returns it to owners and admins. Subscribe to it when you create or
        change the webhook; it isn't in the default list. Same headers, signature and best-effort delivery as the
        entry events.
      security: []
      parameters:
      - name: X-Hourbrook-Event
        in: header
        required: true
        schema:
          type: string
          const: tag.deleted
        description: The event type, same as `type` in the body.
      - name: X-Hourbrook-Delivery
        in: header
        required: true
        schema:
          type: string
          format: uuid
        description: A fresh id for this delivery, same as `id` in the body.
      - name: X-Hourbrook-Signature
        in: header
        required: true
        schema:
          type: string
          pattern: ^sha256=[0-9a-f]{64}$
        description: "`sha256=` and the hex HMAC-SHA256 of the raw request body, keyed with the webhook's secret."
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WebhookTagEvent'
            example:
              id: 92f6b478-d5ac-4307-9619-84b70a25f3cd
              type: tag.deleted
              created_at: 1762160400000
              workspace_id: 9b1f6a4e-3c2d-4e8a-8f10-5d7c2b9a1e01
              data:
                tag:
                  id: 0b6d1f4e-7d52-4a9b-a3c1-6e8d2f9b4c20
                  workspace_id: 9b1f6a4e-3c2d-4e8a-8f10-5d7c2b9a1e01
                  name: Design
                  color: '#22AA66'
                  archived: false
                  client_updated_at: 1762160400000
                  deleted_at: 1762164000000
                  external_id: null
      responses:
        '200':
          description: Any 2xx marks the delivery ok.
  plan.created:
    post:
      operationId: webhookPlanCreated
      tags:
      - Webhooks
      summary: Plan created
      description: |
        Sent when planned time or time off is saved for a day (`PUT /v1/plans/{id}` with a new id, or one that was deleted).
        The body holds the plan as the API returns it to owners and admins. Subscribe to it when you create or
        change the webhook; it isn't in the default list. Same headers, signature and best-effort delivery as the
        entry events.
      security: []
      parameters:
      - name: X-Hourbrook-Event
        in: header
        required: true
        schema:
          type: string
          const: plan.created
        description: The event type, same as `type` in the body.
      - name: X-Hourbrook-Delivery
        in: header
        required: true
        schema:
          type: string
          format: uuid
        description: A fresh id for this delivery, same as `id` in the body.
      - name: X-Hourbrook-Signature
        in: header
        required: true
        schema:
          type: string
          pattern: ^sha256=[0-9a-f]{64}$
        description: "`sha256=` and the hex HMAC-SHA256 of the raw request body, keyed with the webhook's secret."
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WebhookPlanEvent'
            example:
              id: a307c589-e6bd-4418-a72a-95c81b3604de
              type: plan.created
              created_at: 1762160400000
              workspace_id: 9b1f6a4e-3c2d-4e8a-8f10-5d7c2b9a1e01
              data:
                plan:
                  id: c2a95e17-8d43-4b6f-a0e9-1f7c3d5b8a62
                  workspace_id: 9b1f6a4e-3c2d-4e8a-8f10-5d7c2b9a1e01
                  user_id: 5a2e8c14-9b3d-4f71-a6c2-1d0e7b4f8a93
                  project_id: 7c1d3a52-6b0e-4f6a-9d3e-2a8f5b7c9e10
                  date: 1762128000000
                  hours: 3
                  note: null
                  title: Homepage hero
                  status: planned
                  kind: work
                  updated_at: 1762160400000
                  external_id: LIN-482
      responses:
        '200':
          description: Any 2xx marks the delivery ok.
  plan.updated:
    post:
      operationId: webhookPlanUpdated
      tags:
      - Webhooks
      summary: Plan updated
      description: |
        Sent when a plan is saved again, for example marked `done`. A plan turning done also sends `task.done`.
        The body holds the plan as the API returns it to owners and admins. Subscribe to it when you create or
        change the webhook; it isn't in the default list. Same headers, signature and best-effort delivery as the
        entry events.
      security: []
      parameters:
      - name: X-Hourbrook-Event
        in: header
        required: true
        schema:
          type: string
          const: plan.updated
        description: The event type, same as `type` in the body.
      - name: X-Hourbrook-Delivery
        in: header
        required: true
        schema:
          type: string
          format: uuid
        description: A fresh id for this delivery, same as `id` in the body.
      - name: X-Hourbrook-Signature
        in: header
        required: true
        schema:
          type: string
          pattern: ^sha256=[0-9a-f]{64}$
        description: "`sha256=` and the hex HMAC-SHA256 of the raw request body, keyed with the webhook's secret."
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WebhookPlanEvent'
            example:
              id: b418d69a-f7ce-4529-b83b-a6d92c4715ef
              type: plan.updated
              created_at: 1762160400000
              workspace_id: 9b1f6a4e-3c2d-4e8a-8f10-5d7c2b9a1e01
              data:
                plan:
                  id: c2a95e17-8d43-4b6f-a0e9-1f7c3d5b8a62
                  workspace_id: 9b1f6a4e-3c2d-4e8a-8f10-5d7c2b9a1e01
                  user_id: 5a2e8c14-9b3d-4f71-a6c2-1d0e7b4f8a93
                  project_id: 7c1d3a52-6b0e-4f6a-9d3e-2a8f5b7c9e10
                  date: 1762128000000
                  hours: 3
                  note: null
                  title: Homepage hero
                  status: planned
                  kind: work
                  updated_at: 1762160400000
                  external_id: LIN-482
      responses:
        '200':
          description: Any 2xx marks the delivery ok.
  plan.deleted:
    post:
      operationId: webhookPlanDeleted
      tags:
      - Webhooks
      summary: Plan deleted
      description: |
        Sent when a plan is deleted. The plan is the last version before it was deleted.
        The body holds the plan as the API returns it to owners and admins. Subscribe to it when you create or
        change the webhook; it isn't in the default list. Same headers, signature and best-effort delivery as the
        entry events.
      security: []
      parameters:
      - name: X-Hourbrook-Event
        in: header
        required: true
        schema:
          type: string
          const: plan.deleted
        description: The event type, same as `type` in the body.
      - name: X-Hourbrook-Delivery
        in: header
        required: true
        schema:
          type: string
          format: uuid
        description: A fresh id for this delivery, same as `id` in the body.
      - name: X-Hourbrook-Signature
        in: header
        required: true
        schema:
          type: string
          pattern: ^sha256=[0-9a-f]{64}$
        description: "`sha256=` and the hex HMAC-SHA256 of the raw request body, keyed with the webhook's secret."
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WebhookPlanEvent'
            example:
              id: c529e7ab-08df-463a-894c-b7ea3d5826f0
              type: plan.deleted
              created_at: 1762160400000
              workspace_id: 9b1f6a4e-3c2d-4e8a-8f10-5d7c2b9a1e01
              data:
                plan:
                  id: c2a95e17-8d43-4b6f-a0e9-1f7c3d5b8a62
                  workspace_id: 9b1f6a4e-3c2d-4e8a-8f10-5d7c2b9a1e01
                  user_id: 5a2e8c14-9b3d-4f71-a6c2-1d0e7b4f8a93
                  project_id: 7c1d3a52-6b0e-4f6a-9d3e-2a8f5b7c9e10
                  date: 1762128000000
                  hours: 3
                  note: null
                  title: Homepage hero
                  status: planned
                  kind: work
                  updated_at: 1762160400000
                  external_id: LIN-482
      responses:
        '200':
          description: Any 2xx marks the delivery ok.
  task.done:
    post:
      operationId: webhookTaskDone
      tags:
      - Webhooks
      summary: Planned work done
      description: 'Sent the first time a planned work block (a plan, not time off) is marked `done`. Marking it planned again
        and
  
        done again sends nothing more. Titles and hours only: no notes.
  
        The request is `application/json` with `User-Agent: Hourbrook-Webhooks/1`. Delivery is best effort: a 5 second
  
        timeout, no redirects, no retries.
  
        '
      security: []
      parameters:
      - name: X-Hourbrook-Event
        in: header
        required: true
        schema:
          type: string
          const: task.done
        description: The event type, same as `type` in the body.
      - name: X-Hourbrook-Delivery
        in: header
        required: true
        schema:
          type: string
          format: uuid
        description: A fresh id for this delivery, same as `id` in the body.
      - name: X-Hourbrook-Signature
        in: header
        required: true
        schema:
          type: string
          pattern: ^sha256=[0-9a-f]{64}$
        description: '`sha256=` and the hex HMAC-SHA256 of the raw request body, keyed with the webhook''s secret. Compare in
  
          constant time. Reject old `created_at` values to stop replays.
  
          '
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WebhookTaskDoneEvent'
            example:
              id: 3c8f1e64-7a29-4b50-9d02-e4a6b1c7f853
              type: task.done
              created_at: 1762160400000
              workspace_id: 9b1f6a4e-3c2d-4e8a-8f10-5d7c2b9a1e01
              data:
                task:
                  id: b3d71e08-9a52-4f6c-8e1d-2c4a6f0b9d75
                  title: Homepage hero
                  date: 1762128000000
                  hours_planned: 6
                  user:
                    id: 5a2e8c14-9b3d-4f71-a6c2-1d0e7b4f8a93
                    name: Mara Lindqvist
                  project:
                    id: 7c1d3a52-6b0e-4f6a-9d3e-2a8f5b7c9e10
                    name: Kestrel website
      responses:
        '200':
          description: Any 2xx marks the delivery ok.
  invoice.created:
    post:
      operationId: webhookInvoiceCreated
      tags:
      - Webhooks
      summary: Invoice created
      description: 'Sent when an invoice is created with `POST /v1/invoices`. It carries the number, client, total, currency
        and
  
        period: no lines and no notes.
  
        The request is `application/json` with `User-Agent: Hourbrook-Webhooks/1`. Delivery is best effort: a 5 second
  
        timeout, no redirects, no retries.
  
        '
      security: []
      parameters:
      - name: X-Hourbrook-Event
        in: header
        required: true
        schema:
          type: string
          const: invoice.created
        description: The event type, same as `type` in the body.
      - name: X-Hourbrook-Delivery
        in: header
        required: true
        schema:
          type: string
          format: uuid
        description: A fresh id for this delivery, same as `id` in the body.
      - name: X-Hourbrook-Signature
        in: header
        required: true
        schema:
          type: string
          pattern: ^sha256=[0-9a-f]{64}$
        description: '`sha256=` and the hex HMAC-SHA256 of the raw request body, keyed with the webhook''s secret. Compare in
  
          constant time. Reject old `created_at` values to stop replays.
  
          '
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WebhookInvoiceCreatedEvent'
            example:
              id: 91a5d3e7-0f28-4b64-8c19-2e7f6a0b4d36
              type: invoice.created
              created_at: 1762156800000
              workspace_id: 9b1f6a4e-3c2d-4e8a-8f10-5d7c2b9a1e01
              data:
                invoice:
                  id: e8b2c5a0-1f47-4d93-b6a8-3c9d0e7f2a51
                  number: INV-0007
                  client:
                    id: 3f0c2d1e-7a64-4b95-8c2d-9e1f5a0b6d37
                    name: Kestrel Inc.
                  total: 3248.7
                  currency: USD
                  period_from: 1759276800000
                  period_to: 1761955200000
      responses:
        '200':
          description: Any 2xx marks the delivery ok.
  week.closed:
    post:
      operationId: webhookWeekClosed
      tags:
      - Webhooks
      summary: Week closed
      description: 'Sent on Monday morning (from 06:00 UTC) for the week that just ended, Monday to Monday in UTC, once per
  
        week and workspace. A week with no logged time sends nothing. It carries hours per project and per person;
  
        people are named by name, else "Teammate" plus an initial, never by email.
  
        The request is `application/json` with `User-Agent: Hourbrook-Webhooks/1`. Delivery is best effort: a 5 second
  
        timeout, no redirects, no retries.
  
        '
      security: []
      parameters:
      - name: X-Hourbrook-Event
        in: header
        required: true
        schema:
          type: string
          const: week.closed
        description: The event type, same as `type` in the body.
      - name: X-Hourbrook-Delivery
        in: header
        required: true
        schema:
          type: string
          format: uuid
        description: A fresh id for this delivery, same as `id` in the body.
      - name: X-Hourbrook-Signature
        in: header
        required: true
        schema:
          type: string
          pattern: ^sha256=[0-9a-f]{64}$
        description: '`sha256=` and the hex HMAC-SHA256 of the raw request body, keyed with the webhook''s secret. Compare in
  
          constant time. Reject old `created_at` values to stop replays.
  
          '
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WebhookWeekClosedEvent'
            example:
              id: d5b08c2f-6e13-4a79-b4e0-8f1a3c7d9e62
              type: week.closed
              created_at: 1762235400000
              workspace_id: 9b1f6a4e-3c2d-4e8a-8f10-5d7c2b9a1e01
              data:
                week:
                  from: 1761523200000
                  to: 1762128000000
                total_hours: 62.5
                projects:
                - id: 7c1d3a52-6b0e-4f6a-9d3e-2a8f5b7c9e10
                  name: Kestrel website
                  hours: 41
                - id: 0e4b9a17-2d85-4c3b-b6f1-8a7d3c5e2f40
                  name: Internal
                  hours: 21.5
                people:
                - id: 5a2e8c14-9b3d-4f71-a6c2-1d0e7b4f8a93
                  name: Mara Lindqvist
                  hours: 38
                - id: 8d3f2a61-5c07-4e94-a1b8-9e6d0c4f7b25
                  name: Teammate J.
                  hours: 24.5
      responses:
        '200':
          description: Any 2xx marks the delivery ok.
components:
  securitySchemes:
    apiToken:
      type: http
      scheme: bearer
      description: A personal API token (`hb_pat_…`) or a signed-in session token.
    session:
      type: http
      scheme: bearer
      description: A signed-in session token only (from `POST /v1/auth/verify`). Personal API tokens get `403 session_required`.
  parameters:
    WorkspaceIdQuery:
      name: workspace_id
      in: query
      required: true
      schema: { type: string }
      description: The workspace to read.
    WorkspaceIdQueryOptional:
      name: workspace_id
      in: query
      required: false
      schema: { type: string }
      description: Only this workspace. Without it, every workspace you belong to.
    ExternalIdQuery:
      name: external_id
      in: query
      required: false
      schema: { type: string, maxLength: 200 }
      description: Only rows with exactly this `external_id`, to find what an integration already imported.
    WorkspaceIdPath:
      name: id
      in: path
      required: true
      schema: { type: string }
      description: The workspace id.
    IdPath:
      name: id
      in: path
      required: true
      schema: { type: string }
    FromQuery:
      name: from
      in: query
      required: true
      schema: { $ref: "#/components/schemas/Timestamp" }
      description: Start of the range (inclusive), in ms.
    ToQuery:
      name: to
      in: query
      required: true
      schema: { $ref: "#/components/schemas/Timestamp" }
      description: End of the range (exclusive), in ms. At most 400 days after `from`.
    EntryAuthorQuery:
      name: user_id
      in: query
      required: false
      schema: { type: string }
      description: |
        Whose entry this is. Without it, your own entry with that id wins; otherwise the one colleague's entry you
        manage. Several matches answer `409 ambiguous`.
    OAuthConnectProvider:
      name: provider
      in: path
      required: true
      schema:
        type: string
        enum:
        - linear
        - jira
      description: The issue tracker to connect.
  headers:
    Retry-After:
      description: "Seconds until the limit frees. `0` with `code: \"new_code_required\"` means waiting won't help."
      schema: { type: integer, minimum: 0 }
  responses:
    Ok:
      description: Done.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Ok" }
    BadRequest:
      description: The input is invalid.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          example: { error: '"name" must be 1 to 200 characters.' }
    Unauthorized:
      description: No token, or it is unknown, revoked or expired.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          example: { error: "Sign in again." }
    Forbidden:
      description: You may not do this.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          example: { error: "Only workspace admins can do that." }
    SessionRequired:
      description: This needs a signed-in session; personal API tokens are refused.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          example: { error: Sign in to do this; API tokens can't., code: session_required }
    NotFound:
      description: No such thing, or nothing you can see by that id.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          example: { error: "Not found." }
    Conflict:
      description: The current state doesn't allow this.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          example: { error: "This entry is marked as billed. Unmark it to edit it.", code: billed }
    Unprocessable:
      description: The project needs tags or notes on every entry.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          example: { error: "This project needs at least one tag on every entry.", code: tags_required }
    TooManyRequests:
      description: Rate limited.
      headers:
        Retry-After: { $ref: "#/components/headers/Retry-After" }
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          example: { error: "Too many requests. Try again in a minute." }
    HtmlPage:
      description: A small HTML page (`text/html`) that says what went wrong. Nothing in it is JSON.
      content:
        text/html:
          schema:
            type: string
    ProviderUnknown:
      description: No such provider.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error: Unknown provider.
            code: provider_unknown
    ProviderNotConfigured:
      description: This provider isn't set up on this server.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error: This provider isn't set up on this server yet.
            code: provider_not_configured
    NotConfigured:
      description: Connecting apps isn't set up on this server.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error: Connecting apps isn't set up on this server yet.
            code: not_configured
    OAuthErrorResponse:
      description: An OAuth error in the RFC 6749 shape, not the usual `{ error, code }`.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/OAuthError'
          example:
            error: invalid_grant
            error_description: The code or refresh token is invalid, expired or was already used.
    ProtectedResourceMetadata:
      description: The metadata document.
      content:
        application/json:
          schema:
            type: object
            additionalProperties: true
          example:
            resource: https://hourbrook-api.mlazarev.workers.dev/mcp
            authorization_servers:
            - https://hourbrook-api.mlazarev.workers.dev
            scopes_supported:
            - time:read
            - time:write
            bearer_methods_supported:
            - header
            resource_name: Hourbrook
    JsonRpcError:
      description: A JSON-RPC error. A `401` carries a `WWW-Authenticate` header pointing at the protected resource metadata.
      content:
        application/json:
          schema:
            type: object
            additionalProperties: true
          example:
            jsonrpc: '2.0'
            id: null
            error:
              code: -32001
              message: Sign in to Hourbrook first.
    MethodNotAllowed:
      description: Only POST is allowed.
      headers:
        Allow:
          schema:
            type: string
            examples:
            - POST
      content:
        application/json:
          schema:
            type: object
            additionalProperties: true
          example:
            jsonrpc: '2.0'
            id: null
            error:
              code: -32000
              message: Method not allowed. POST JSON-RPC messages to this endpoint.
  schemas:
    Timestamp:
      type: integer
      format: int64
      description: Milliseconds since the Unix epoch (UTC).
      examples: [1762156800000]
    Color:
      type: string
      pattern: "^#[0-9A-Fa-f]{6}$"
      examples: ["#5B8DEF"]
    Currency:
      type: string
      description: ISO 4217 code.
      examples: [USD]
    Ok:
      type: object
      required: [ok]
      properties:
        ok: { type: boolean, const: true }
    Error:
      type: object
      required: [error]
      properties:
        error: { type: string, description: A sentence for people. }
        code:
          type: string
          description: A stable code for programs, when there is one.
          examples: [session_required, billed, tags_required, notes_required, ambiguous, transfer_ownership, rate_limited, new_code_required]
    Rejected:
      type: object
      description: A row that was refused, with the reason. Refused rows don't fail the request.
      required: [id, reason]
      properties:
        id: { type: string }
        reason:
          type: string
          examples: [forbidden, unknown project, invoiced, billed, tags_required]
    Role:
      type: string
      enum: [owner, admin, member]
    User:
      type: object
      required: [id, email, currency, name, timezone, week_start]
      properties:
        id: { type: string }
        email: { type: string, format: email }
        currency: { $ref: "#/components/schemas/Currency" }
        name: { type: [string, "null"], description: Display name; null until set. }
        timezone:
          type: [string, "null"]
          description: IANA time zone. Null means unknown; the server then uses UTC.
          examples: [Europe/Berlin]
        week_start:
          type: integer
          minimum: 0
          maximum: 6
          description: First day of the week. 0 = Sunday, 1 = Monday (default) … 6 = Saturday.
    PaymentDetails:
      type: object
      description: How clients pay the workspace. Every field is optional; `""` means not set.
      properties:
        account_name: { type: string }
        bank_name: { type: string }
        iban: { type: string, description: "Compact, upper case, checksum verified." }
        account_number: { type: string }
        routing: { type: string, description: "Routing number, sort code, BSB or similar." }
        swift: { type: string }
        other: { type: string, description: "Free text, such as a PayPal or Wise link." }
    InvoiceSettings:
      type: object
      required: [company_name, company_address, tax_id, prefix, next_number, default_tax_rate, payment_terms_days, footer, payment]
      properties:
        company_name: { type: string }
        company_address: { type: string }
        tax_id: { type: string }
        prefix: { type: string, examples: [INV-] }
        next_number: { type: integer, minimum: 1 }
        default_tax_rate: { type: number, description: Percent. `19` means 19%. }
        payment_terms_days: { type: integer, minimum: 0 }
        footer: { type: string }
        payment: { $ref: "#/components/schemas/PaymentDetails" }
    Workspace:
      type: object
      required: [id, name, kind, role, currency, invoice]
      properties:
        id: { type: string }
        name: { type: string }
        kind: { type: string, enum: [personal, team] }
        role: { $ref: "#/components/schemas/Role" }
        currency: { $ref: "#/components/schemas/Currency" }
        invoice:
          description: Invoice settings. Owners and admins only; null for everyone else.
          oneOf:
            - $ref: "#/components/schemas/InvoiceSettings"
            - type: "null"
    ExternalId:
      type: [string, "null"]
      maxLength: 200
      description: |
        Your own id for this row in another system (a Jira issue, a Linear project, a payroll id), so a sync can find
        what it already imported instead of importing it twice. One line, at most 200 characters; an empty string is
        stored as `null`. Hourbrook never reads it and doesn't require it to be unique. Writes that leave it out keep
        the stored value.
    Client:
      type: object
      required: [id, workspace_id, name, email, address, client_updated_at, deleted_at, external_id]
      properties:
        id: { type: string }
        workspace_id: { type: string }
        name: { type: string, minLength: 1, maxLength: 200 }
        email: { type: [string, "null"] }
        address: { type: [string, "null"] }
        website:
          type: [string, "null"]
          description: A bare domain, like `lumen.health`. Left out of a write keeps the stored value; null clears it.
        logo:
          type: [string, "null"]
          description: A data URL of a PNG, JPEG or WebP image of at most 128 by 128 pixels and 24 KB. Never SVG. Left out keeps it.
        tax_id:
          type: [string, "null"]
          maxLength: 100
          description: >-
            The client's VAT or tax number, printed under "Bill to" on invoices when set. Trimmed; blank or null clears it,
            left out of a write keeps the stored value.
          example: DE123456789
        client_updated_at:
          $ref: "#/components/schemas/Timestamp"
          description: When the row last changed. Writes with an older value than the stored one are ignored.
        deleted_at:
          oneOf: [{ $ref: "#/components/schemas/Timestamp" }, { type: "null" }]
        external_id: { $ref: "#/components/schemas/ExternalId" }
    Project:
      type: object
      required: [id, workspace_id, client_id, name, color, rate, budget_hours, archived, created_at, client_updated_at,
        deleted_at, billable_default, budget_amount, budget_period, currency, requires_tags, requires_notes, external_id]
      properties:
        id: { type: string }
        workspace_id: { type: string }
        client_id: { type: [string, "null"] }
        name: { type: string, minLength: 1, maxLength: 200 }
        color: { $ref: "#/components/schemas/Color" }
        rate:
          type: [number, "null"]
          minimum: 0
          maximum: 100000
          description: Hourly rate. Null for plain members of team workspaces.
        budget_hours: { type: [number, "null"], minimum: 0, maximum: 1000000 }
        archived: { type: boolean }
        created_at: { $ref: "#/components/schemas/Timestamp" }
        client_updated_at: { $ref: "#/components/schemas/Timestamp" }
        deleted_at:
          oneOf: [{ $ref: "#/components/schemas/Timestamp" }, { type: "null" }]
        billable_default:
          type: boolean
          description: Whether entries on this project are billable unless they say otherwise.
        budget_amount:
          type: [number, "null"]
          description: Money budget. Null for plain members of team workspaces.
        budget_period:
          type: [string, "null"]
          enum: [total, monthly, weekly, null]
        currency:
          type: [string, "null"]
          description: Null means the workspace currency.
        requires_tags: { type: boolean, description: Entries need at least one tag. }
        requires_notes: { type: boolean, description: Entries need notes. }
        external_id: { $ref: "#/components/schemas/ExternalId" }
    Tag:
      type: object
      required: [id, workspace_id, name, color, archived, client_updated_at, deleted_at, external_id]
      properties:
        id: { type: string, description: Stored lower case. }
        workspace_id: { type: string }
        name: { type: string, minLength: 1, maxLength: 100 }
        color: { $ref: "#/components/schemas/Color" }
        archived: { type: boolean }
        client_updated_at: { $ref: "#/components/schemas/Timestamp" }
        deleted_at:
          oneOf: [{ $ref: "#/components/schemas/Timestamp" }, { type: "null" }]
        external_id: { $ref: "#/components/schemas/ExternalId" }
    Entry:
      type: object
      required: [id, project_id, title, started_at, ended_at, source, client_updated_at, deleted_at, notes, billable,
        tag_ids, external_id, invoice_id, billed_at, user_id, workspace_id]
      properties:
        id: { type: string, description: Unique per author. }
        project_id: { type: string }
        title: { type: string, maxLength: 500 }
        started_at: { $ref: "#/components/schemas/Timestamp" }
        ended_at:
          $ref: "#/components/schemas/Timestamp"
          description: Not before `started_at`, at most 7 days after it.
        source:
          type: string
          enum: [manual, suggestion]
          description: "`suggestion`: kept from what the Mac app recorded. `manual`: typed in."
        client_updated_at: { $ref: "#/components/schemas/Timestamp" }
        deleted_at:
          oneOf: [{ $ref: "#/components/schemas/Timestamp" }, { type: "null" }]
        notes: { type: [string, "null"] }
        billable:
          type: [boolean, "null"]
          description: Null means the project's `billable_default`.
        tag_ids:
          type: array
          items: { type: string }
        external_id: { $ref: "#/components/schemas/ExternalId" }
        invoice_id:
          type: [string, "null"]
          readOnly: true
          description: The invoice this entry is on. Locks the entry.
        billed_at:
          oneOf: [{ $ref: "#/components/schemas/Timestamp" }, { type: "null" }]
          readOnly: true
          description: When an admin marked it billed. Locks the entry.
        user_id: { type: string, readOnly: true, description: The author. }
        workspace_id: { type: string, readOnly: true, description: The project's workspace. }
    Member:
      type: object
      required: [user_id, email, name, role, joined_at, capacity_hours]
      properties:
        user_id: { type: string }
        email: { type: string, format: email }
        name: { type: [string, "null"] }
        role: { $ref: "#/components/schemas/Role" }
        joined_at: { $ref: "#/components/schemas/Timestamp" }
        capacity_hours: { type: number, description: Weekly working hours. }
        cost_rate:
          type: [number, "null"]
          description: What an hour of their time costs. Owners and admins only; absent for everyone else.
        bill_rate:
          type: [number, "null"]
          description: Their hourly rate in reports, over the project's. Owners and admins only.
        external_id:
          $ref: "#/components/schemas/ExternalId"
          description: Their id in another system (a Jira account, a payroll id). Owners and admins only, like the rates.
    Invite:
      type: object
      required: [id, email, role, created_at, expires_at]
      properties:
        id: { type: string }
        email: { type: string, format: email }
        role: { type: string, enum: [admin, member] }
        created_at: { $ref: "#/components/schemas/Timestamp" }
        expires_at: { $ref: "#/components/schemas/Timestamp" }
        capacity_hours:
          type: [number, "null"]
          description: Weekly hours they'll have once they join. Null means the default, 40.
    Team:
      type: object
      required: [id, workspace_id, name, color, created_at, members]
      properties:
        id: { type: string }
        workspace_id: { type: string }
        name: { type: string }
        color: { $ref: "#/components/schemas/Color" }
        created_at: { $ref: "#/components/schemas/Timestamp" }
        members:
          type: array
          items:
            type: object
            required: [user_id, is_lead]
            properties:
              user_id: { type: string }
              is_lead: { type: boolean }
    Plan:
      type: object
      required: [id, workspace_id, user_id, project_id, date, hours, note, title, status, kind, updated_at, external_id]
      properties:
        id: { type: string }
        workspace_id: { type: string }
        user_id: { type: string }
        project_id: { type: [string, "null"], description: Null for time off. }
        date:
          $ref: "#/components/schemas/Timestamp"
          description: The planned day, as local midnight in ms.
        hours: { type: number, exclusiveMinimum: 0, maximum: 24 }
        note: { type: [string, "null"], maxLength: 500 }
        title: { type: [string, "null"], maxLength: 200, description: "The planned work, one line." }
        status: { type: string, enum: [planned, done] }
        kind:
          type: string
          enum: [work, time_off]
          description: "`time_off` is subtracted from capacity and never counted as planned."
        updated_at: { $ref: "#/components/schemas/Timestamp" }
        external_id: { $ref: "#/components/schemas/ExternalId" }
    InvoiceLine:
      type: object
      required: [id, position, description, quantity, rate, amount, project_id]
      properties:
        id: { type: string }
        position: { type: integer }
        description: { type: string }
        quantity: { type: number, description: Hours. }
        rate: { type: number }
        amount: { type: number, description: "`quantity × rate`, rounded to 2 decimals." }
        project_id: { type: [string, "null"] }
    Invoice:
      type: object
      required: [id, workspace_id, client_id, number, status, issue_date, due_date, currency, tax_rate, notes, period_from,
        period_to, subtotal, tax, total, created_at, updated_at, show_payment]
      properties:
        id: { type: string }
        workspace_id: { type: string }
        client_id: { type: string }
        number: { type: string, examples: [INV-0001] }
        status: { type: string, enum: [draft, sent, paid, void] }
        issue_date: { $ref: "#/components/schemas/Timestamp" }
        due_date: { $ref: "#/components/schemas/Timestamp" }
        currency: { $ref: "#/components/schemas/Currency" }
        tax_rate: { type: number, description: Percent. }
        notes: { type: string }
        period_from: { $ref: "#/components/schemas/Timestamp" }
        period_to: { $ref: "#/components/schemas/Timestamp" }
        subtotal: { type: number }
        tax: { type: number }
        total: { type: number }
        created_at: { $ref: "#/components/schemas/Timestamp" }
        updated_at: { $ref: "#/components/schemas/Timestamp" }
        show_payment: { type: boolean, description: False hides the "How to pay" box on this invoice. }
        lines:
          type: array
          items: { $ref: "#/components/schemas/InvoiceLine" }
          description: Only from `GET /v1/invoices/{id}` and writes.
        client: { $ref: "#/components/schemas/Client" }
        issuer:
          description: The workspace's invoice settings at the time, with its name.
          allOf:
            - $ref: "#/components/schemas/InvoiceSettings"
            - type: object
              properties:
                workspace_name: { type: string }
    IssueDetails:
      type: object
      description: Cached details of an issue imported from the person's own tracker.
      properties:
        description: { type: [string, "null"] }
        status: { type: [string, "null"] }
        labels: { type: array, items: { type: string } }
        updated_at:
          oneOf: [{ $ref: "#/components/schemas/Timestamp" }, { type: "null" }]
    Task:
      type: object
      required: [id, title, notes, day, project_id, stage, done_at, position, estimate_minutes, source, external_id,
        external_key, external_url, issue, created_at, client_updated_at, deleted_at]
      properties:
        id: { type: string }
        title: { type: string }
        notes: { type: [string, "null"] }
        day: { type: string, description: "`YYYY-MM-DD` (your local day), or `\"\"` for not on a day yet." }
        project_id: { type: [string, "null"] }
        stage: { type: [string, "null"], enum: [todo, doing, null] }
        done_at:
          oneOf: [{ $ref: "#/components/schemas/Timestamp" }, { type: "null" }]
        position: { type: number }
        estimate_minutes: { type: [number, "null"], minimum: 0, maximum: 1000000 }
        source: { type: [string, "null"], description: "Where the task came from, such as `linear` or `jira`." }
        external_id: { type: [string, "null"], description: The id in that tool. }
        external_key: { type: [string, "null"], examples: [ENG-142] }
        external_url: { type: [string, "null"] }
        issue:
          oneOf: [{ $ref: "#/components/schemas/IssueDetails" }, { type: "null" }]
        created_at: { $ref: "#/components/schemas/Timestamp" }
        client_updated_at: { $ref: "#/components/schemas/Timestamp" }
        deleted_at:
          oneOf: [{ $ref: "#/components/schemas/Timestamp" }, { type: "null" }]
    Subtask:
      type: object
      required: [id, task_id, title, done, position, created_at, client_updated_at, deleted_at]
      properties:
        id: { type: string }
        task_id: { type: string }
        title: { type: string }
        done: { type: boolean }
        position: { type: number }
        created_at: { $ref: "#/components/schemas/Timestamp" }
        client_updated_at: { $ref: "#/components/schemas/Timestamp" }
        deleted_at:
          oneOf: [{ $ref: "#/components/schemas/Timestamp" }, { type: "null" }]
    TaskLink:
      type: object
      required: [task_id, entry_id, client_updated_at, deleted_at]
      properties:
        task_id: { type: string }
        entry_id: { type: string }
        client_updated_at: { $ref: "#/components/schemas/Timestamp" }
        deleted_at:
          oneOf: [{ $ref: "#/components/schemas/Timestamp" }, { type: "null" }]
    PlanBlock:
      type: object
      description: A block of time you planned on your calendar. Private to you, never logged time.
      required: [id, project_id, task_id, title, started_at, ended_at, created_at, client_updated_at, deleted_at]
      properties:
        id: { type: string }
        project_id: { type: [string, "null"] }
        task_id: { type: [string, "null"] }
        title: { type: string }
        started_at: { $ref: "#/components/schemas/Timestamp" }
        ended_at: { $ref: "#/components/schemas/Timestamp" }
        created_at: { $ref: "#/components/schemas/Timestamp" }
        client_updated_at: { $ref: "#/components/schemas/Timestamp" }
        deleted_at:
          oneOf: [{ $ref: "#/components/schemas/Timestamp" }, { type: "null" }]
    Timer:
      type: object
      required: [id, workspace_id, project_id, title, notes, billable, tag_ids, task_id, started_at, device, updated_at]
      properties:
        id: { type: string, description: The id of the entry the timer becomes when stopped. }
        workspace_id: { type: string }
        project_id: { type: string }
        title: { type: string }
        notes: { type: [string, "null"] }
        billable: { type: [boolean, "null"] }
        tag_ids: { type: array, items: { type: string } }
        task_id: { type: [string, "null"] }
        started_at: { $ref: "#/components/schemas/Timestamp" }
        device: { type: [string, "null"], description: Which device started it. }
        updated_at: { $ref: "#/components/schemas/Timestamp" }
    MeResponse:
      type: object
      required:
      - user
      - workspaces
      properties:
        user:
          $ref: '#/components/schemas/User'
        workspaces:
          type: array
          items:
            $ref: '#/components/schemas/Workspace'
    MePatch:
      type: object
      description: At least one field. Unknown fields are ignored.
      properties:
        currency:
          $ref: '#/components/schemas/Currency'
        name:
          type:
          - string
          - 'null'
          maxLength: 100
          description: Display name on one line. An empty string or null clears it.
        timezone:
          type:
          - string
          - 'null'
          description: An IANA time zone name such as `Europe/Berlin`, or null for unknown.
        week_start:
          type: integer
          minimum: 0
          maximum: 6
          description: 0 is Sunday, 1 is Monday, up to 6 for Saturday.
    MeDelete:
      type: object
      required:
      - confirm
      properties:
        confirm:
          type: string
          format: email
          description: Your own email address, to confirm.
    Session:
      type: object
      required:
      - id
      - name
      - created_at
      - last_used_at
      - expires_at
      - current
      properties:
        id:
          type: string
        name:
          type:
          - string
          - 'null'
          description: The device, such as `Mac`, `Web - Safari` or `Mac - GitHub`.
        created_at:
          oneOf:
          - $ref: '#/components/schemas/Timestamp'
          - type: 'null'
        last_used_at:
          oneOf:
          - $ref: '#/components/schemas/Timestamp'
          - type: 'null'
        expires_at:
          $ref: '#/components/schemas/Timestamp'
          description: Sessions last 180 days and renew when used.
        current:
          type: boolean
          description: True for the session making this request.
    ApiToken:
      type: object
      required:
      - id
      - name
      - hint
      - created_at
      - last_used_at
      properties:
        id:
          type: string
        name:
          type: string
        hint:
          type: string
          description: The last 4 characters of the token, so you can tell tokens apart.
        created_at:
          $ref: '#/components/schemas/Timestamp'
        last_used_at:
          description: Null until first used. Updated at most every 10 minutes.
          oneOf:
          - $ref: '#/components/schemas/Timestamp'
          - type: 'null'
    ApiTokenCreate:
      type: object
      required:
      - name
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 100
          description: What the token is for. One line.
    ApiTokenCreated:
      type: object
      required:
      - token
      - secret
      properties:
        token:
          $ref: '#/components/schemas/ApiToken'
        secret:
          type: string
          description: The token itself, `hb_pat_` plus 43 characters. Shown only in this response.
          examples:
          - hb_pat_k3J9xQvT2mWc8RzY1nL5aHdE7uBfPoGs4iVtXyN0wCq
    ConnectedApp:
      type: object
      required:
      - id
      - name
      - redirect_host
      - scopes
      - created_at
      - last_used_at
      properties:
        id:
          type: string
          description: The grant id. Use it to disconnect the app.
        name:
          type: string
          description: The name the app gave itself.
        redirect_host:
          type: string
          description: The host the app sends you back to.
        scopes:
          type: array
          items:
            type: string
            enum:
            - time:read
            - time:write
        created_at:
          $ref: '#/components/schemas/Timestamp'
        last_used_at:
          oneOf:
          - $ref: '#/components/schemas/Timestamp'
          - type: 'null'
    LogoutAllRequest:
      type: object
      properties:
        revoke_tokens:
          type: boolean
          description: Also revoke every personal API token. Default false.
    LogoutAllResponse:
      type: object
      required:
      - ok
      - ended
      properties:
        ok:
          type: boolean
          const: true
        ended:
          type: integer
          description: Sessions ended.
        revoked_tokens:
          type: integer
          description: API tokens revoked. Only present when you asked for it.
    AuthStatus:
      type: object
      required:
      - email_sign_in
      - oauth
      - new_accounts
      properties:
        email_sign_in:
          type: boolean
          description: Sign-in by emailed code is open.
        oauth:
          type: object
          required:
          - github
          - google
          properties:
            github:
              type: boolean
            google:
              type: boolean
        new_accounts:
          type: boolean
          description: GitHub and Google sign-in may create new accounts.
    AuthCodeRequest:
      type: object
      required:
      - email
      properties:
        email:
          type: string
          format: email
          maxLength: 254
    AuthCodeResponse:
      type: object
      required:
      - ok
      properties:
        ok:
          type: boolean
          const: true
        devCode:
          type: string
          description: The code itself. Only on local dev servers.
        devLink:
          type: string
          description: The one-click link. Only on local dev servers.
    AuthVerifyRequest:
      type: object
      required:
      - email
      - code
      properties:
        email:
          type: string
          format: email
        code:
          type: string
          pattern: ^\d{6}$
        device:
          type: string
          maxLength: 80
          description: Name for this session. Guessed from the User-Agent when missing.
    AuthSession:
      type: object
      required:
      - token
      - user
      - session_id
      properties:
        token:
          type: string
          description: The session token. Send it as a bearer token. Shown only once.
        user:
          $ref: '#/components/schemas/User'
        session_id:
          type: string
    OAuthSigninRedeem:
      type: object
      required:
      - ticket
      - client_verifier
      properties:
        ticket:
          type: string
          pattern: ^[A-Za-z0-9_-]{43}$
          description: The one-time ticket from the callback.
        client_verifier:
          type: string
          pattern: ^[A-Za-z0-9._~-]{43,128}$
          description: The secret whose SHA-256 (base64url) was sent as the challenge.
    OAuthProviders:
      type: object
      required:
      - connect
      - signin
      - github_device_client_id
      properties:
        connect:
          type: object
          required:
          - linear
          - jira
          properties:
            linear:
              type: boolean
            jira:
              type: boolean
        signin:
          type: object
          required:
          - github
          - google
          properties:
            github:
              type: boolean
            google:
              type: boolean
        github_device_client_id:
          type:
          - string
          - 'null'
          description: The GitHub app id for device-flow sign-in, when set.
    OAuthConnectStart:
      type: object
      required:
      - client
      - client_challenge
      properties:
        client:
          type: string
          enum:
          - mac
        client_challenge:
          type: string
          pattern: ^[A-Za-z0-9_-]{43}$
          description: base64url(sha256(verifier)).
    OAuthConnectStarted:
      type: object
      required:
      - authorize_url
      - expires_at
      properties:
        authorize_url:
          type: string
          format: uri
        expires_at:
          $ref: '#/components/schemas/Timestamp'
    OAuthConnectTokens:
      type: object
      required:
      - provider
      - access_token
      - refresh_token
      - expires_at
      - scope
      - account
      properties:
        provider:
          type: string
          enum:
          - linear
          - jira
        access_token:
          type: string
        refresh_token:
          type:
          - string
          - 'null'
        expires_at:
          description: When the access token expires. Null when the provider did not say.
          oneOf:
          - $ref: '#/components/schemas/Timestamp'
          - type: 'null'
        scope:
          type:
          - string
          - 'null'
        account:
          type: object
          properties:
            name:
              type:
              - string
              - 'null'
            email:
              type:
              - string
              - 'null'
        sites:
          type: array
          description: Jira only. The sites the token can reach.
          items:
            type: object
            required:
            - id
            - url
            - name
            properties:
              id:
                type: string
              url:
                type: string
                format: uri
              name:
                type: string
    OAuthRefreshRequest:
      type: object
      required:
      - refresh_token
      properties:
        refresh_token:
          type: string
          maxLength: 4096
    OAuthRefreshResponse:
      type: object
      required:
      - provider
      - access_token
      - refresh_token
      - expires_at
      - scope
      properties:
        provider:
          type: string
          enum:
          - linear
          - jira
        access_token:
          type: string
        refresh_token:
          type:
          - string
          - 'null'
        expires_at:
          oneOf:
          - $ref: '#/components/schemas/Timestamp'
          - type: 'null'
        scope:
          type:
          - string
          - 'null'
    OAuthError:
      type: object
      description: An OAuth error as RFC 6749 defines it.
      required:
      - error
      properties:
        error:
          type: string
          examples:
          - invalid_request
          - invalid_client
          - invalid_grant
          - invalid_scope
          - invalid_target
          - unsupported_grant_type
          - invalid_redirect_uri
          - invalid_client_metadata
          - slow_down
        error_description:
          type: string
    McpClientRegistration:
      type: object
      required:
      - redirect_uris
      properties:
        client_name:
          type: string
          description: Shown to the person on the consent page. Cut to 80 characters.
        redirect_uris:
          type: array
          minItems: 1
          maxItems: 5
          items:
            type: string
            format: uri
            maxLength: 512
        grant_types:
          type: array
          items:
            type: string
            enum:
            - authorization_code
            - refresh_token
        response_types:
          type: array
          items:
            type: string
            enum:
            - code
        token_endpoint_auth_method:
          type: string
          enum:
          - none
          - client_secret_post
          - client_secret_basic
          description: Defaults to `client_secret_basic`.
    McpClientRegistered:
      type: object
      required:
      - client_id
      - client_id_issued_at
      - client_name
      - redirect_uris
      - grant_types
      - response_types
      - token_endpoint_auth_method
      properties:
        client_id:
          type: string
        client_id_issued_at:
          type: integer
          description: Seconds since the epoch.
        client_name:
          type: string
        redirect_uris:
          type: array
          items:
            type: string
        grant_types:
          type: array
          items:
            type: string
        response_types:
          type: array
          items:
            type: string
        token_endpoint_auth_method:
          type: string
        client_secret:
          type: string
          description: Only for clients that use a secret. Shown once.
        client_secret_expires_at:
          type: integer
          description: Always 0 (never).
    McpTokenRequest:
      type: object
      required:
      - grant_type
      properties:
        grant_type:
          type: string
          enum:
          - authorization_code
          - refresh_token
        client_id:
          type: string
        client_secret:
          type: string
        code:
          type: string
          description: For `authorization_code`.
        code_verifier:
          type: string
          description: For `authorization_code`. The PKCE verifier.
        redirect_uri:
          type: string
        refresh_token:
          type: string
          description: For `refresh_token`.
        scope:
          type: string
          description: For `refresh_token`. Can only narrow the grant.
        resource:
          type: string
    McpTokenResponse:
      type: object
      required:
      - access_token
      - token_type
      - expires_in
      - refresh_token
      - scope
      properties:
        access_token:
          type: string
        token_type:
          type: string
          const: Bearer
        expires_in:
          type: integer
          description: Seconds. 3600.
        refresh_token:
          type: string
        scope:
          type: string
    McpRevokeRequest:
      type: object
      required:
      - token
      properties:
        token:
          type: string
        client_id:
          type: string
        client_secret:
          type: string
    McpConsentRequest:
      type: object
      required:
      - request
      properties:
        request:
          type: string
          pattern: ^[A-Za-z0-9_-]{43}$
          description: The handle from the consent page URL fragment.
    McpConsentDecision:
      type: object
      required:
      - request
      - allow
      properties:
        request:
          type: string
          pattern: ^[A-Za-z0-9_-]{43}$
        allow:
          type: boolean
    McpConsentInfo:
      type: object
      required:
      - client
      - scopes
      - account
      - expires_at
      properties:
        client:
          type: object
          required:
          - name
          - redirect_host
          - redirect_uri
          properties:
            name:
              type: string
            redirect_host:
              type: string
            redirect_uri:
              type: string
        scopes:
          type: array
          items:
            type: string
        account:
          type: object
          required:
          - email
          - name
          properties:
            email:
              type: string
            name:
              type:
              - string
              - 'null'
        expires_at:
          $ref: '#/components/schemas/Timestamp'
    EntryListResponse:
      type: object
      required:
      - entries
      properties:
        entries:
          type: array
          items:
            $ref: '#/components/schemas/Entry'
        next_cursor:
          type: [string, "null"]
          description: |
            Only when you sent `limit`. Pass it as `cursor` to get the next page; `null` on the last page. Absent
            without `limit`.
    EntryResponse:
      type: object
      required:
      - entry
      properties:
        entry:
          $ref: '#/components/schemas/Entry'
    EntryCreate:
      type: object
      required:
      - project_id
      - title
      - started_at
      - ended_at
      properties:
        id:
          type: string
          format: uuid
          description: Your own id, so a retry is safe. The server makes one if you leave it out. Must be a UUID.
        user_id:
          type:
          - string
          - 'null'
          format: uuid
          description: 'Log the time for this person. Owners and admins may use any member of the project''s workspace; team
    
            leads the people they lead. Without it the entry is yours.
    
            '
        project_id:
          type: string
          format: uuid
          description: A live project in a workspace the author belongs to.
        title:
          type: string
          maxLength: 500
          description: May be empty.
        started_at:
          $ref: '#/components/schemas/Timestamp'
        ended_at:
          $ref: '#/components/schemas/Timestamp'
          description: Not before `started_at`, at most 7 days after it.
        source:
          type: string
          enum:
          - manual
          - suggestion
          default: manual
        notes:
          type:
          - string
          - 'null'
          maxLength: 5000
          description: An empty string is stored as `null`. Required when the project has `requires_notes`.
        billable:
          type:
          - boolean
          - 'null'
          description: Null means the project's `billable_default`.
        tag_ids:
          type:
          - array
          - 'null'
          maxItems: 50
          items:
            type: string
            format: uuid
          description: Tags of the project's workspace. Required when the project has `requires_tags`. Duplicates are dropped.
        external_id:
          $ref: '#/components/schemas/ExternalId'
    EntryPatch:
      type: object
      description: Send only what changes. A field left out, or sent as `null` for `project_id`, `title`, `started_at` or `ended_at`,
        stays as it is.
      properties:
        user_id:
          type: string
          format: uuid
          description: Whose entry this is. Same as the `user_id` query parameter, which wins if both are sent.
        project_id:
          type: string
          format: uuid
        title:
          type: string
          maxLength: 500
        started_at:
          $ref: '#/components/schemas/Timestamp'
        ended_at:
          $ref: '#/components/schemas/Timestamp'
          description: Not before `started_at`, at most 7 days after it.
        notes:
          type:
          - string
          - 'null'
          maxLength: 5000
          description: '`null` or an empty string clears the notes.'
        billable:
          type:
          - boolean
          - 'null'
        tag_ids:
          type:
          - array
          - 'null'
          maxItems: 50
          items:
            type: string
            format: uuid
          description: Replaces the entry's tags. `null` clears them.
        external_id:
          $ref: '#/components/schemas/ExternalId'
          description: "`null` clears it."
    BulkEntriesRequest:
      type: object
      required:
      - ids
      - set
      properties:
        ids:
          type: array
          minItems: 1
          maxItems: 500
          description: The entries to change, from at most 25 different people. Repeated pairs count once.
          items:
            type: object
            required:
            - user_id
            - id
            properties:
              user_id:
                type: string
                format: uuid
                description: The author.
              id:
                type: string
                format: uuid
        set:
          $ref: '#/components/schemas/BulkEntriesSet'
    BulkEntriesSet:
      type: object
      description: 'What to change. Send at least one field. `billed` and `delete` can''t be combined with each other or with
        the
    
        other fields.
    
        '
      properties:
        project_id:
          type: string
          format: uuid
          description: Move the entries to this project. Someone else's entries can only move within their workspace.
        tag_ids:
          type: array
          maxItems: 50
          items:
            type: string
            format: uuid
          description: Replace the tags. Not together with `add_tag_ids`.
        add_tag_ids:
          type: array
          maxItems: 50
          items:
            type: string
            format: uuid
          description: Add these tags to the ones each entry has. Not together with `tag_ids`.
        billable:
          type:
          - boolean
          - 'null'
        billed:
          type: boolean
          description: '`true` marks the entries billed, `false` unmarks them. Owners and admins of the entry''s workspace only.'
        delete:
          type: boolean
          const: true
          description: Delete the entries. Only `true` is accepted.
    BulkEntriesResponse:
      type: object
      required:
      - entries
      - rejected
      properties:
        entries:
          type: array
          description: The current state of every entry in scope that was not rejected.
          items:
            $ref: '#/components/schemas/Entry'
        rejected:
          type: array
          items:
            $ref: '#/components/schemas/BulkRejected'
    BulkRejected:
      type: object
      description: An entry the bulk change skipped.
      required:
      - user_id
      - id
      - reason
      properties:
        user_id:
          type: string
        id:
          type: string
        reason:
          type: string
          description: 'Why it was skipped. Fixed values: `not found` (also for entries in workspaces you aren''t in),
    
            `forbidden`, `deleted`, `invoiced`, `billed`. Also the validation messages: `unknown project`,
    
            `deleted project`, `unknown tag`, `tags_required`, `notes_required`,
    
            `entries can only move to projects in the same workspace`, `not a member of the entry''s workspace`.
    
            '
          examples:
          - invoiced
    EntryHistoryResponse:
      type: object
      required:
      - history
      properties:
        history:
          type: array
          items:
            $ref: '#/components/schemas/EntryHistoryItem'
    EntryHistoryItem:
      type: object
      required:
      - id
      - action
      - changes
      - at
      - actor
      properties:
        id:
          type: string
        action:
          type: string
          enum:
          - create
          - update
          - delete
          - bill
          description: '`create` also marks an entry that moved into a workspace. `bill` is an owner or admin marking it billed
    
            or unbilled.
    
            '
        changes:
          type: object
          description: 'Each changed field with `[old, new]`. Fields: `project_id`, `title`, `notes`, `started_at`, `ended_at`,
    
            `billable`, `tag_ids`, `deleted_at`, and for `bill` only `billed_at`. A `create` has `null` as every old
    
            value; a `delete` has only `deleted_at`.
    
            '
          additionalProperties:
            type: array
            minItems: 2
            maxItems: 2
        at:
          $ref: '#/components/schemas/Timestamp'
        actor:
          type: object
          description: Who made the change.
          required:
          - user_id
          - name
          - email
          properties:
            user_id:
              type: string
            name:
              type:
              - string
              - 'null'
            email:
              type:
              - string
              - 'null'
    TimerGetResponse:
      type: object
      required:
      - timer
      properties:
        timer:
          oneOf:
          - $ref: '#/components/schemas/Timer'
          - type: 'null'
        project_gone:
          type: boolean
          const: true
          description: Present when the timer's project is deleted or you left its workspace. Stop with another `project_id`,
            or discard.
    TimerResponse:
      type: object
      required:
      - timer
      properties:
        timer:
          $ref: '#/components/schemas/Timer'
    TimerRunningConflict:
      type: object
      required:
      - error
      - code
      - timer
      properties:
        error:
          type: string
        code:
          type: string
          const: timer_running
        timer:
          $ref: '#/components/schemas/Timer'
    TimerStart:
      type: object
      required:
      - project_id
      properties:
        id:
          type: string
          format: uuid
          description: The id for the entry this timer becomes. The server makes one if you leave it out.
        project_id:
          type: string
          format: uuid
          description: A live project in one of your workspaces.
        title:
          type: string
          maxLength: 500
          default: ''
        notes:
          type:
          - string
          - 'null'
          maxLength: 5000
        billable:
          type:
          - boolean
          - 'null'
        tag_ids:
          type:
          - array
          - 'null'
          maxItems: 50
          items:
            type: string
            format: uuid
          description: Tags of the project's workspace.
        task_id:
          type:
          - string
          - 'null'
          format: uuid
          description: One of your own tasks. The entry is linked to it when you stop.
        started_at:
          $ref: '#/components/schemas/Timestamp'
          description: When it started. Default now. Not more than a minute ahead, not more than 7 days back.
        device:
          type:
          - string
          - 'null'
          maxLength: 100
          description: A short label for the device, one line.
    TimerPatch:
      type: object
      description: Send only what changes. `title` can't be `null`.
      properties:
        id:
          type: string
          format: uuid
          description: The timer you think is running. A different running timer answers `409 timer_changed`.
        project_id:
          type: string
          format: uuid
        title:
          type: string
          maxLength: 500
        notes:
          type:
          - string
          - 'null'
          maxLength: 5000
        billable:
          type:
          - boolean
          - 'null'
        tag_ids:
          type:
          - array
          - 'null'
          maxItems: 50
          items:
            type: string
            format: uuid
        task_id:
          type:
          - string
          - 'null'
          format: uuid
        started_at:
          $ref: '#/components/schemas/Timestamp'
          description: Not more than a minute ahead, not more than 7 days back.
    TimerStop:
      type: object
      description: Every field is optional, and so is the whole body.
      properties:
        id:
          type: string
          format: uuid
          description: The timer you think is running. A different running timer answers `409 timer_changed`.
        ended_at:
          oneOf:
          - $ref: '#/components/schemas/Timestamp'
          - type: 'null'
          description: When it ended. Default now. Not before the start, not more than a minute ahead.
        project_id:
          type: string
          format: uuid
          description: Log the time to this project instead of the timer's. Needed when the timer's project is gone.
        tag_ids:
          type:
          - array
          - 'null'
          maxItems: 50
          items:
            type: string
            format: uuid
          description: The tags to use with a different `project_id`. Without them the entry gets none. Ignored when the project
            stays the same.
    TimerStopResponse:
      type: object
      required:
      - entry
      - timer
      properties:
        entry:
          oneOf:
          - $ref: '#/components/schemas/Entry'
          - type: 'null'
          description: The logged entry, or `null` when the timer ran under a minute.
        timer:
          type: 'null'
        too_short:
          type: boolean
          const: true
          description: Present when nothing was logged because the timer ran under a minute.
    TimerDiscardResponse:
      type: object
      required:
      - ok
      - timer
      properties:
        ok:
          type: boolean
          const: true
        timer:
          type: 'null'
    SearchResponse:
      type: object
      required:
      - entries
      - tasks
      properties:
        entries:
          type: array
          description: Your live entries, newest first.
          items:
            $ref: '#/components/schemas/Entry'
        tasks:
          type: array
          description: Your live tasks, most recently changed first.
          items:
            $ref: '#/components/schemas/Task'
    TagUsageResponse:
      type: object
      required:
      - weeks
      - week_days
      - tz
      - tags
      - untagged
      properties:
        weeks:
          type: array
          description: The start of each week, oldest first, as local midnight in ms.
          items:
            $ref: '#/components/schemas/Timestamp'
        week_days:
          type: array
          description: The same weeks as `YYYY-MM-DD`.
          items:
            type: string
        tz:
          type: string
          description: The time zone used.
        tags:
          type: array
          description: Tags with time, most time first.
          items:
            type: object
            required:
            - tag_id
            - seconds
            - total_seconds
            - entries
            properties:
              tag_id:
                type: string
              seconds:
                type: array
                description: Seconds in each week, in the order of `weeks`.
                items:
                  type: integer
              total_seconds:
                type: integer
              entries:
                type: integer
                description: How many entries carry this tag.
        untagged:
          type: object
          description: Entries with no tag.
          required:
          - seconds
          - total_seconds
          - entries
          properties:
            seconds:
              type: array
              items:
                type: integer
            total_seconds:
              type: integer
            entries:
              type: integer
    EntryWrite:
      type: object
      description: An entry as the sync call takes it. `invoice_id`, `billed_at`, `user_id` and `workspace_id` are ignored.
      required:
      - id
      - project_id
      - title
      - started_at
      - ended_at
      - source
      - client_updated_at
      - deleted_at
      properties:
        id:
          type: string
          format: uuid
        project_id:
          type: string
          format: uuid
        title:
          type: string
          maxLength: 500
        started_at:
          $ref: '#/components/schemas/Timestamp'
        ended_at:
          $ref: '#/components/schemas/Timestamp'
          description: Not before `started_at`, at most 7 days after it.
        source:
          type: string
          enum:
          - manual
          - suggestion
        client_updated_at:
          $ref: '#/components/schemas/Timestamp'
        deleted_at:
          oneOf:
          - $ref: '#/components/schemas/Timestamp'
          - type: 'null'
          description: Set it to delete the entry. It must be present, `null` for a live entry.
        notes:
          type:
          - string
          - 'null'
          maxLength: 5000
          description: Left out keeps the stored value.
        billable:
          type:
          - boolean
          - 'null'
          description: Left out keeps the stored value.
        tag_ids:
          type:
          - array
          - 'null'
          maxItems: 50
          items:
            type: string
            format: uuid
          description: Left out keeps the stored value.
        external_id:
          $ref: '#/components/schemas/ExternalId'
          description: Left out keeps the stored value; `null` clears it.
    SyncRequest:
      type: object
      properties:
        cursor:
          type:
          - integer
          - 'null'
          minimum: 0
          description: The `cursor` of your last response. `null` or `0` returns everything.
        clients:
          type: array
          items:
            $ref: '#/components/schemas/Client'
          description: Owners and admins only. `email`, `address` and `deleted_at` must be present (`null` is fine); `external_id` may be left out.
        projects:
          type: array
          items:
            $ref: '#/components/schemas/Project'
          description: 'Owners and admins only. `deleted_at` must be present. The later fields (`billable_default`,
    
            `budget_amount`, `budget_period`, `currency`, `requires_tags`, `requires_notes`, `external_id`) may be left out and keep
    
            their stored values.
    
            '
        tags:
          type: array
          items:
            $ref: '#/components/schemas/Tag'
          description: Owners and admins only. `color`, `archived` and `external_id` may be left out. `deleted_at` must be present.
        entries:
          type: array
          items:
            $ref: '#/components/schemas/EntryWrite'
          description: Your own entries.
    SyncResponse:
      type: object
      required:
      - cursor
      - workspaces
      - clients
      - projects
      - tags
      - entries
      - rejected
      properties:
        cursor:
          type: integer
          description: Send this back next time. When `more` is `true` it stops at the last row returned.
        workspaces:
          type: array
          description: Always the full current list. When it gains or loses a workspace, sync again from a `null` cursor.
          items:
            $ref: '#/components/schemas/Workspace'
        clients:
          type: array
          description: Changed clients of all your workspaces, deleted ones included.
          items:
            $ref: '#/components/schemas/Client'
        projects:
          type: array
          items:
            $ref: '#/components/schemas/Project'
        tags:
          type: array
          items:
            $ref: '#/components/schemas/Tag'
        entries:
          type: array
          description: Your own changed entries, deleted ones included.
          items:
            $ref: '#/components/schemas/Entry'
        rejected:
          type: array
          description: Rows that were refused. The rest of the request still went through.
          items:
            $ref: '#/components/schemas/Rejected'
        more:
          type: boolean
          const: true
          description: Present when the response was cut at 5000 rows per table. Call again with `cursor`.
    ReportGroup:
      type: object
      required:
      - key
      - label
      - color
      - hours
      - billable_hours
      - amount
      - cost
      - profit
      properties:
        key:
          type: string
          description: 'The project, client, tag or person id, or the day (`YYYY-MM-DD`). `none` for entries without a client
            or
    
            tag. On public reports a person''s key is their label.
    
            '
        label:
          type: string
        color:
          type:
          - string
          - 'null'
          description: Set for projects and tags.
        hours:
          type: number
        billable_hours:
          type: number
        amount:
          type:
          - number
          - 'null'
          description: Billable money in the workspace currency. Null unless you may see money.
        cost:
          type:
          - number
          - 'null'
          description: Hours times the members' cost rates. Owners and admins only.
        profit:
          type:
          - number
          - 'null'
          description: Amount less cost. Owners and admins only; work billed in another currency is left out.
        sub:
          type: array
          description: The second level, when `sub_group` is set. Entries here have no `sub` of their own.
          items:
            $ref: '#/components/schemas/ReportGroup'
    ReportEntry:
      type: object
      description: One entry behind a report. On public reports `id`, `user_id` and `notes` are left out.
      required:
      - date
      - started_at
      - ended_at
      - title
      - project_id
      - project
      - client
      - person
      - project_name
      - project_color
      - client_name
      - user_name
      - tag_ids
      - hours
      - billable
      - billed
      - amount
      properties:
        id:
          type: string
        user_id:
          type: string
        date:
          type: string
          description: The day it started, `YYYY-MM-DD` in the report's time zone.
        started_at:
          $ref: '#/components/schemas/Timestamp'
        ended_at:
          $ref: '#/components/schemas/Timestamp'
        title:
          type: string
        notes:
          type:
          - string
          - 'null'
        project_id:
          type: string
        project:
          type: string
          description: The project's name.
        client:
          type:
          - string
          - 'null'
          description: The client's name.
        person:
          type: string
          description: The author's name, else their email (a label like "Member 123" on public reports).
        project_name:
          type: string
          description: Same as `project`.
        project_color:
          $ref: '#/components/schemas/Color'
        client_name:
          type:
          - string
          - 'null'
          description: Same as `client`.
        user_name:
          type: string
          description: Same as `person`.
        tag_ids:
          type: array
          items:
            type: string
        hours:
          type: number
        billable:
          type: boolean
        billed:
          type: boolean
        amount:
          type:
          - number
          - 'null'
    ReportPerson:
      type: object
      required:
      - key
      - name
      - hours
      - billable_hours
      - amount
      - cost
      - profit
      properties:
        user_id:
          type: string
          description: Left out on public reports.
        key:
          type: string
          description: The user id, or the person's label on public reports.
        name:
          type: string
        hours:
          type: number
        billable_hours:
          type: number
        capacity_hours:
          type:
          - number
          - 'null'
          description: Weekly capacity. Left out on public reports.
        amount:
          type:
          - number
          - 'null'
        cost:
          type:
          - number
          - 'null'
        profit:
          type:
          - number
          - 'null'
    ReportTotals:
      type: object
      required:
      - hours
      - billable_hours
      - amount
      - cost
      - profit
      properties:
        hours:
          type: number
        billable_hours:
          type: number
        amount:
          type:
          - number
          - 'null'
          description: Billable money in the workspace currency. Null unless you may see money.
        cost:
          type:
          - number
          - 'null'
        profit:
          type:
          - number
          - 'null'
        amount_by_currency:
          type: object
          description: Owners and admins only, when anything billable was priced. Billable money per currency, for workspaces
            whose projects use several.
          additionalProperties:
            type: number
    ReportResponse:
      type: object
      required:
      - range
      - from
      - to
      - generated_at
      - currency
      - totals
      - groups
      - series
      - people
      properties:
        range:
          type: object
          required:
          - from
          - to
          - name
          properties:
            from:
              $ref: '#/components/schemas/Timestamp'
            to:
              $ref: '#/components/schemas/Timestamp'
            name:
              type: string
              enum:
              - this_week
              - last_week
              - this_month
              - last_month
              - this_year
              - last_30
              - custom
        from:
          $ref: '#/components/schemas/Timestamp'
        to:
          $ref: '#/components/schemas/Timestamp'
        generated_at:
          $ref: '#/components/schemas/Timestamp'
        currency:
          $ref: '#/components/schemas/Currency'
        totals:
          $ref: '#/components/schemas/ReportTotals'
        groups:
          type: array
          description: Sorted by hours, biggest first (by date when grouped by day). An entry with several tags counts under each
            tag.
          items:
            $ref: '#/components/schemas/ReportGroup'
        series:
          type: array
          description: Hours per day and group key, for charts.
          items:
            type: object
            required:
            - date
            - key
            - hours
            properties:
              date:
                type: string
                description: '`YYYY-MM-DD` in the report''s time zone.'
              key:
                type: string
              hours:
                type: number
        people:
          type: array
          description: Per-person totals, biggest first.
          items:
            $ref: '#/components/schemas/ReportPerson'
        entries:
          type: array
          description: Only with `include_entries`. At most 5,000.
          items:
            $ref: '#/components/schemas/ReportEntry'
        truncated:
          type: boolean
          const: true
          description: Present when entries or rows were left out for size.
    CapacityRow:
      type: object
      required:
      - user_id
      - name
      - email
      - week_start
      - capacity_hours
      - time_off_hours
      - planned_hours
      - logged_hours
      - billable_hours
      properties:
        user_id:
          type: string
        name:
          type:
          - string
          - 'null'
        email:
          type: string
          format: email
        week_start:
          $ref: '#/components/schemas/Timestamp'
        capacity_hours:
          type: number
          description: Weekly capacity less time off, never below 0.
        time_off_hours:
          type: number
        planned_hours:
          type: number
          description: Planned work. Time off is not counted.
        logged_hours:
          type: number
        billable_hours:
          type:
          - number
          - 'null'
          description: Owners and admins only.
    CapacityResponse:
      type: object
      required:
      - weeks
      - capacity
      properties:
        weeks:
          type: array
          description: The start of each week in the range, in ms.
          items:
            $ref: '#/components/schemas/Timestamp'
        capacity:
          type: array
          description: One row per person per week.
          items:
            $ref: '#/components/schemas/CapacityRow'
    BudgetUsage:
      type: object
      required:
      - project_id
      - workspace_id
      - budget_period
      - period_start
      - period_end
      - budget_hours
      - hours_used
      - hours_pct
      - budget_amount
      - amount_used
      - amount_pct
      properties:
        project_id:
          type: string
        workspace_id:
          type: string
        budget_period:
          type: string
          enum:
          - total
          - monthly
          - weekly
        period_start:
          type:
          - integer
          - 'null'
          description: Start of the current period, in ms. Null for a `total` budget.
        period_end:
          type:
          - integer
          - 'null'
          description: End of the current period (exclusive), in ms. Null for a `total` budget.
        budget_hours:
          type:
          - number
          - 'null'
        hours_used:
          type: number
          description: Everyone's logged hours on the project in the period.
        hours_pct:
          type:
          - number
          - 'null'
          description: Percent of the hours budget used. Null without an hours budget.
        budget_amount:
          type:
          - number
          - 'null'
          description: Null for anyone but owners and admins.
        amount_used:
          type:
          - number
          - 'null'
          description: Billable money in the period. Null for anyone but owners and admins.
        amount_pct:
          type:
          - number
          - 'null'
          description: Null for anyone but owners and admins, or without a money budget.
    ReportFilters:
      type: object
      description: 'The filters of a shared report. They match the query parameters of `GET /v1/reports`, with lists sent as
    
        arrays. Everything is optional on input; stored filters always carry `range`, `billable`, `billed`,
    
        `group_by` and `sub_group`.
    
        '
      properties:
        range:
          type: string
          enum:
          - this_week
          - last_week
          - this_month
          - last_month
          - this_year
          - last_30
          - custom
          description: Default `this_month`, or `custom` when `from` is given. `custom` needs `from` and `to`.
        from:
          $ref: '#/components/schemas/Timestamp'
        to:
          $ref: '#/components/schemas/Timestamp'
        user_ids:
          type: array
          maxItems: 200
          items:
            type: string
            format: uuid
        client_ids:
          type: array
          maxItems: 200
          items:
            type: string
            format: uuid
        project_ids:
          type: array
          maxItems: 200
          items:
            type: string
            format: uuid
        tag_ids:
          type: array
          maxItems: 200
          items:
            type: string
            format: uuid
        billable:
          type: string
          enum:
          - all
          - billable
          - nonbillable
          default: all
        billed:
          type: string
          enum:
          - all
          - billed
          - unbilled
          default: all
        group_by:
          type: string
          enum:
          - project
          - client
          - person
          - tag
          - day
          default: project
        sub_group:
          type:
          - string
          - 'null'
          enum:
          - project
          - client
          - person
          - tag
          - day
          - null
        tz:
          type: string
          description: IANA time zone for relative ranges and day grouping. A new share gets your profile zone when you send none.
          examples:
          - Europe/Berlin
    ReportShareBranding:
      type: object
      required:
      - title
      - accent_color
      - logo_url
      - company_name
      - show_amounts
      properties:
        title:
          type: string
          maxLength: 200
          description: Defaults to the share's name.
        accent_color:
          $ref: '#/components/schemas/Color'
        logo_url:
          type:
          - string
          - 'null'
          maxLength: 500
          description: An https address.
        company_name:
          type:
          - string
          - 'null'
          maxLength: 200
        show_amounts:
          type: boolean
          description: Show money next to hours. This reveals effective rates to everyone with the link.
    ReportShareBrandingInput:
      type: object
      description: Keys you leave out keep their value (the default on create).
      properties:
        title:
          type: string
          maxLength: 200
        accent_color:
          $ref: '#/components/schemas/Color'
        logo_url:
          type:
          - string
          - 'null'
          maxLength: 500
          description: An https address with no credentials, or null or `""` to clear it.
        company_name:
          type:
          - string
          - 'null'
          maxLength: 200
        show_amounts:
          type: boolean
    ReportSchedule:
      type: object
      required:
      - id
      - share_id
      - created_by
      - frequency
      - weekday
      - day_of_month
      - hour
      - timezone
      - recipients
      - subject
      - next_run_at
      - last_run_at
      - last_status
      - created_at
      - updated_at
      properties:
        id:
          type: string
        share_id:
          type: string
        created_by:
          type: string
          description: Whoever saved the schedule last.
        frequency:
          type: string
          enum:
          - weekly
          - monthly
        weekday:
          type:
          - integer
          - 'null'
          minimum: 1
          maximum: 7
          description: 1 = Monday to 7 = Sunday. Weekly only.
        day_of_month:
          type:
          - integer
          - 'null'
          minimum: 1
          maximum: 28
          description: Monthly only.
        hour:
          type: integer
          minimum: 0
          maximum: 23
          description: Hour of the day on the clock of `timezone`.
        timezone:
          type: string
          examples:
          - Europe/Berlin
        recipients:
          type: array
          items:
            type: string
            format: email
        subject:
          type:
          - string
          - 'null'
          description: Null means the default subject.
        next_run_at:
          $ref: '#/components/schemas/Timestamp'
        last_run_at:
          oneOf:
          - $ref: '#/components/schemas/Timestamp'
          - type: 'null'
        last_status:
          type:
          - string
          - 'null'
          enum:
          - sent
          - partial
          - failed
          - logged
          - sending
          - unavailable
          - null
          description: Null before the first run. `logged` is a dev server that logs mail. `unavailable` means the schedule was
            turned off because the report can't be shown any more.
        created_at:
          $ref: '#/components/schemas/Timestamp'
        updated_at:
          $ref: '#/components/schemas/Timestamp'
    ReportScheduleInput:
      type: object
      required:
      - frequency
      - hour
      - recipients
      properties:
        frequency:
          type: string
          enum:
          - weekly
          - monthly
        weekday:
          type: integer
          minimum: 1
          maximum: 7
          description: 1 = Monday to 7 = Sunday. Required when weekly.
        day_of_month:
          type: integer
          minimum: 1
          maximum: 28
          description: Required when monthly.
        hour:
          type: integer
          minimum: 0
          maximum: 23
          description: Hour of the day on the clock of `timezone`.
        timezone:
          type: string
          description: IANA time zone. Default your profile time zone, else UTC.
          examples:
          - Europe/Berlin
        recipients:
          type: array
          minItems: 1
          maxItems: 10
          description: Plain addresses only, no display names. They are lower-cased and duplicates dropped.
          items:
            type: string
            format: email
        subject:
          type:
          - string
          - 'null'
          maxLength: 150
          description: One line. Empty or null means the default subject.
    ReportShare:
      type: object
      required:
      - id
      - workspace_id
      - name
      - filters
      - include_entries
      - snapshot
      - snapshot_at
      - branding
      - created_by
      - created_at
      - updated_at
      properties:
        id:
          type: string
        workspace_id:
          type: string
        name:
          type: string
        filters:
          $ref: '#/components/schemas/ReportFilters'
        include_entries:
          type: boolean
          description: Whether the public page lists entries (without notes and ids).
        snapshot:
          type: boolean
          description: True when the report is frozen.
        snapshot_at:
          oneOf:
          - $ref: '#/components/schemas/Timestamp'
          - type: 'null'
          description: When the snapshot was last computed.
        branding:
          $ref: '#/components/schemas/ReportShareBranding'
        created_by:
          type: string
        created_at:
          $ref: '#/components/schemas/Timestamp'
        updated_at:
          $ref: '#/components/schemas/Timestamp'
        schedule:
          description: Only in the list. Null when the share isn't emailed.
          oneOf:
          - $ref: '#/components/schemas/ReportSchedule'
          - type: 'null'
    ReportShareCreate:
      type: object
      required:
      - workspace_id
      - name
      properties:
        workspace_id:
          type: string
        name:
          type: string
          minLength: 1
          maxLength: 200
        filters:
          $ref: '#/components/schemas/ReportFilters'
        include_entries:
          type: boolean
          default: false
        snapshot:
          type: boolean
          default: false
          description: Freeze the report now instead of computing it on every visit.
        branding:
          $ref: '#/components/schemas/ReportShareBrandingInput'
        snapshot_data:
          type: object
          description: Accepted for old clients (at most 512 KB) and thrown away. Snapshots are always computed by the server.
          additionalProperties: true
    ReportSharePatch:
      type: object
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 200
        filters:
          $ref: '#/components/schemas/ReportFilters'
        include_entries:
          type: boolean
        branding:
          $ref: '#/components/schemas/ReportShareBrandingInput'
    ReportShareCreated:
      type: object
      required:
      - share
      - token
      - url
      properties:
        share:
          $ref: '#/components/schemas/ReportShare'
        token:
          type: string
          description: The link token. Shown once; Hourbrook stores only its hash.
        url:
          type: string
          format: uri
          description: The public link.
    PublicReport:
      description: 'A report as the public sees it: everything in `ReportResponse` plus the share''s name and branding, the names
    
        behind the ids, and project budgets that lie wholly inside what the share shows. Never any cost or profit
    
        (always null), user ids, capacity, emails or notes. Amounts are null unless the share shows them.
    
        '
      allOf:
      - $ref: '#/components/schemas/ReportResponse'
      - type: object
        required:
        - name
        - branding
        - filters
        - include_entries
        - snapshot
        properties:
          name:
            type: string
          branding:
            $ref: '#/components/schemas/ReportShareBranding'
          filters:
            type: object
            description: Only what the public may see. No people, ids or time zone.
            properties:
              range:
                type: string
              from:
                $ref: '#/components/schemas/Timestamp'
              to:
                $ref: '#/components/schemas/Timestamp'
              billable:
                type: string
                enum:
                - all
                - billable
                - nonbillable
              billed:
                type: string
                enum:
                - all
                - billed
                - unbilled
              group_by:
                type: string
                enum:
                - project
                - client
                - person
                - tag
                - day
              sub_group:
                type:
                - string
                - 'null'
          include_entries:
            type: boolean
          snapshot:
            type: boolean
          projects:
            type: array
            items:
              type: object
              required:
              - id
              - name
              - color
              - client_id
              properties:
                id:
                  type: string
                name:
                  type: string
                color:
                  $ref: '#/components/schemas/Color'
                client_id:
                  type:
                  - string
                  - 'null'
          clients:
            type: array
            items:
              type: object
              required:
              - id
              - name
              properties:
                id:
                  type: string
                name:
                  type: string
          tags:
            type: array
            items:
              type: object
              required:
              - id
              - name
              - color
              properties:
                id:
                  type: string
                name:
                  type: string
                color:
                  $ref: '#/components/schemas/Color'
          budgets:
            type: array
            description: Absent on snapshots frozen before budgets were added. Empty when none qualifies.
            items:
              type: object
              description: A `BudgetUsage` without `workspace_id`. Money fields are null unless the share shows amounts.
              properties:
                project_id:
                  type: string
                budget_period:
                  type: string
                  enum:
                  - total
                  - monthly
                  - weekly
                period_start:
                  type:
                  - integer
                  - 'null'
                period_end:
                  type:
                  - integer
                  - 'null'
                budget_hours:
                  type:
                  - number
                  - 'null'
                hours_used:
                  type: number
                hours_pct:
                  type:
                  - number
                  - 'null'
                budget_amount:
                  type:
                  - number
                  - 'null'
                amount_used:
                  type:
                  - number
                  - 'null'
                amount_pct:
                  type:
                  - number
                  - 'null'
    InvoiceCreate:
      type: object
      required:
      - workspace_id
      - client_id
      - from
      - to
      - group_by
      properties:
        workspace_id:
          type: string
        client_id:
          type: string
          format: uuid
        from:
          $ref: '#/components/schemas/Timestamp'
        to:
          $ref: '#/components/schemas/Timestamp'
        group_by:
          type: string
          enum:
          - project
          - entry
          description: '`project`: one line per project and rate. `entry`: one line per entry.'
        issue_date:
          $ref: '#/components/schemas/Timestamp'
          description: Default now. The due date is this plus the workspace's payment terms.
    InvoiceLineInput:
      type: object
      required:
      - description
      - quantity
      - rate
      properties:
        description:
          type: string
          maxLength: 500
        quantity:
          type: number
          minimum: 0
          maximum: 1000000
          description: Hours. Rounded to 2 decimals.
        rate:
          type: number
          minimum: 0
          maximum: 1000000
        project_id:
          type:
          - string
          - 'null'
          format: uuid
          description: A project of this workspace.
    InvoicePatch:
      type: object
      properties:
        status:
          type: string
          enum:
          - draft
          - sent
          - paid
          - void
        issue_date:
          $ref: '#/components/schemas/Timestamp'
        due_date:
          $ref: '#/components/schemas/Timestamp'
        tax_rate:
          type: number
          minimum: 0
          maximum: 100
          description: Percent. Totals are recomputed.
        notes:
          type: string
          maxLength: 5000
        show_payment:
          type: boolean
        lines:
          type: array
          maxItems: 500
          description: Replaces all lines. Drafts only.
          items:
            $ref: '#/components/schemas/InvoiceLineInput'
    InvoiceResponse:
      type: object
      required:
      - invoice
      properties:
        invoice:
          $ref: '#/components/schemas/Invoice'
    PlanPut:
      type: object
      required:
      - workspace_id
      - date
      - hours
      properties:
        id:
          type: string
          description: If sent, it must equal the id in the path.
        workspace_id:
          type: string
        user_id:
          type: string
          format: uuid
          description: Whose plan this is. Default you. Must be a member of the workspace.
        kind:
          type: string
          enum:
          - work
          - time_off
          default: work
        project_id:
          type:
          - string
          - 'null'
          format: uuid
          description: Required for `work`, an active project of this workspace. Leave out for `time_off`.
        date:
          $ref: '#/components/schemas/Timestamp'
        hours:
          type: number
          exclusiveMinimum: 0
          maximum: 24
          description: Rounded to 2 decimals.
        note:
          type:
          - string
          - 'null'
          maxLength: 500
        title:
          type:
          - string
          - 'null'
          maxLength: 200
          description: One line, no control characters.
        status:
          type: string
          enum:
          - planned
          - done
          default: planned
        external_id:
          $ref: '#/components/schemas/ExternalId'
          description: Left out keeps the stored value; `null` clears it.
    Webhook:
      type: object
      required:
      - id
      - workspace_id
      - url
      - events
      - created_at
      - last_status
      - last_delivery_at
      properties:
        id:
          type: string
        workspace_id:
          type: string
        url:
          type: string
          format: uri
        events:
          type: array
          items:
            $ref: '#/components/schemas/WebhookEventName'
        created_at:
          $ref: '#/components/schemas/Timestamp'
        last_status:
          type:
          - string
          - 'null'
          enum:
          - ok
          - failed
          - null
          description: '`ok` if the latest delivery got a 2xx, `failed` otherwise, null before the first one.'
        last_delivery_at:
          oneOf:
          - $ref: '#/components/schemas/Timestamp'
          - type: 'null'
    WebhookEventName:
      type: string
      enum:
      - entry.created
      - entry.updated
      - entry.deleted
      - client.created
      - client.updated
      - client.deleted
      - project.created
      - project.updated
      - project.deleted
      - tag.created
      - tag.updated
      - tag.deleted
      - plan.created
      - plan.updated
      - plan.deleted
      - task.done
      - invoice.created
      - week.closed
    WebhookCreate:
      type: object
      required:
      - workspace_id
      - url
      properties:
        workspace_id:
          type: string
        url:
          type: string
          format: uri
          maxLength: 500
          description: A public https address.
        events:
          type: array
          minItems: 1
          description: Default `entry.created`, `entry.updated` and `entry.deleted`.
          items:
            $ref: '#/components/schemas/WebhookEventName'
    WebhookPatch:
      type: object
      description: Send at least one field. The secret stays the same.
      properties:
        url:
          type: string
          format: uri
          maxLength: 500
          description: A public https address. A new address also resets `last_status` and `last_delivery_at`.
        events:
          type: array
          minItems: 1
          description: Replaces the list of events.
          items:
            $ref: '#/components/schemas/WebhookEventName'
    WebhookResponse:
      type: object
      required:
      - webhook
      properties:
        webhook:
          $ref: '#/components/schemas/Webhook'
    WebhookCreated:
      type: object
      required:
      - webhook
      - secret
      properties:
        webhook:
          $ref: '#/components/schemas/Webhook'
        secret:
          type: string
          description: '`whsec_…`. Signs every delivery. Shown once.'
    WebhookEntryEvent:
      type: object
      required:
      - id
      - type
      - created_at
      - workspace_id
      - data
      properties:
        id:
          type: string
          description: The delivery id, same as `X-Hourbrook-Delivery`.
        type:
          type: string
          enum:
          - entry.created
          - entry.updated
          - entry.deleted
        created_at:
          $ref: '#/components/schemas/Timestamp'
        workspace_id:
          type: string
        data:
          type: object
          required:
          - entry
          properties:
            entry:
              $ref: '#/components/schemas/Entry'
    WebhookClientEvent:
      type: object
      required:
      - id
      - type
      - created_at
      - workspace_id
      - data
      properties:
        id:
          type: string
          description: The delivery id, same as `X-Hourbrook-Delivery`.
        type:
          type: string
          enum:
          - client.created
          - client.updated
          - client.deleted
        created_at:
          $ref: '#/components/schemas/Timestamp'
        workspace_id:
          type: string
        data:
          type: object
          required:
          - client
          properties:
            client:
              $ref: '#/components/schemas/Client'
    WebhookProjectEvent:
      type: object
      required:
      - id
      - type
      - created_at
      - workspace_id
      - data
      properties:
        id:
          type: string
          description: The delivery id, same as `X-Hourbrook-Delivery`.
        type:
          type: string
          enum:
          - project.created
          - project.updated
          - project.deleted
        created_at:
          $ref: '#/components/schemas/Timestamp'
        workspace_id:
          type: string
        data:
          type: object
          required:
          - project
          properties:
            project:
              $ref: '#/components/schemas/Project'
    WebhookTagEvent:
      type: object
      required:
      - id
      - type
      - created_at
      - workspace_id
      - data
      properties:
        id:
          type: string
          description: The delivery id, same as `X-Hourbrook-Delivery`.
        type:
          type: string
          enum:
          - tag.created
          - tag.updated
          - tag.deleted
        created_at:
          $ref: '#/components/schemas/Timestamp'
        workspace_id:
          type: string
        data:
          type: object
          required:
          - tag
          properties:
            tag:
              $ref: '#/components/schemas/Tag'
    WebhookPlanEvent:
      type: object
      required:
      - id
      - type
      - created_at
      - workspace_id
      - data
      properties:
        id:
          type: string
          description: The delivery id, same as `X-Hourbrook-Delivery`.
        type:
          type: string
          enum:
          - plan.created
          - plan.updated
          - plan.deleted
        created_at:
          $ref: '#/components/schemas/Timestamp'
        workspace_id:
          type: string
        data:
          type: object
          required:
          - plan
          properties:
            plan:
              $ref: '#/components/schemas/Plan'
    WebhookTaskDoneEvent:
      type: object
      required:
      - id
      - type
      - created_at
      - workspace_id
      - data
      properties:
        id:
          type: string
          description: The delivery id, same as `X-Hourbrook-Delivery`.
        type:
          type: string
          const: task.done
        created_at:
          $ref: '#/components/schemas/Timestamp'
        workspace_id:
          type: string
        data:
          type: object
          required:
          - task
          properties:
            task:
              type: object
              required:
              - id
              - title
              - date
              - hours_planned
              - user
              - project
              properties:
                id:
                  type: string
                  description: The plan id.
                title:
                  type:
                  - string
                  - 'null'
                date:
                  $ref: '#/components/schemas/Timestamp'
                hours_planned:
                  type: number
                user:
                  type: object
                  required:
                  - id
                  - name
                  properties:
                    id:
                      type: string
                    name:
                      type: string
                      description: Their name, else "Teammate" plus an initial. Never the email.
                project:
                  description: Null for plans without a project.
                  oneOf:
                  - type: object
                    required:
                    - id
                    - name
                    properties:
                      id:
                        type: string
                      name:
                        type:
                        - string
                        - 'null'
                  - type: 'null'
    WebhookInvoiceCreatedEvent:
      type: object
      required:
      - id
      - type
      - created_at
      - workspace_id
      - data
      properties:
        id:
          type: string
          description: The delivery id, same as `X-Hourbrook-Delivery`.
        type:
          type: string
          const: invoice.created
        created_at:
          $ref: '#/components/schemas/Timestamp'
        workspace_id:
          type: string
        data:
          type: object
          required:
          - invoice
          properties:
            invoice:
              type: object
              required:
              - id
              - number
              - client
              - total
              - currency
              - period_from
              - period_to
              properties:
                id:
                  type: string
                number:
                  type: string
                client:
                  type: object
                  required:
                  - id
                  - name
                  properties:
                    id:
                      type: string
                    name:
                      type:
                      - string
                      - 'null'
                total:
                  type: number
                currency:
                  $ref: '#/components/schemas/Currency'
                period_from:
                  $ref: '#/components/schemas/Timestamp'
                period_to:
                  $ref: '#/components/schemas/Timestamp'
    WebhookWeekClosedEvent:
      type: object
      required:
      - id
      - type
      - created_at
      - workspace_id
      - data
      properties:
        id:
          type: string
          description: The delivery id, same as `X-Hourbrook-Delivery`.
        type:
          type: string
          const: week.closed
        created_at:
          $ref: '#/components/schemas/Timestamp'
        workspace_id:
          type: string
        data:
          type: object
          required:
          - week
          - total_hours
          - projects
          - people
          properties:
            week:
              type: object
              required:
              - from
              - to
              properties:
                from:
                  $ref: '#/components/schemas/Timestamp'
                to:
                  $ref: '#/components/schemas/Timestamp'
                  description: 'Exclusive: the next Monday 00:00 UTC.'
            total_hours:
              type: number
            projects:
              type: array
              items:
                type: object
                required:
                - id
                - name
                - hours
                properties:
                  id:
                    type: string
                  name:
                    type: string
                  hours:
                    type: number
            people:
              type: array
              items:
                type: object
                required:
                - id
                - name
                - hours
                properties:
                  id:
                    type: string
                  name:
                    type: string
                    description: Their name, else "Teammate" plus an initial. Never the email.
                  hours:
                    type: number
    TaskIssueWrite:
      type:
      - object
      - 'null'
      description: 'Cached details of the issue behind an imported task, or `null` to clear them. Unknown keys are dropped.
    
        Long text is cut to size, not refused.
    
        '
      properties:
        description:
          type:
          - string
          - 'null'
          description: Cut to 20,000 characters.
        status:
          type:
          - string
          - 'null'
          description: Cut to 100 characters.
        labels:
          type:
          - array
          - 'null'
          items:
            type: string
          description: At most 50 labels of up to 100 characters each; empty ones are dropped.
        updated_at:
          oneOf:
          - $ref: '#/components/schemas/Timestamp'
          - type: 'null'
          description: When the issue last changed in the other tool.
    TaskFields:
      type: object
      description: The fields of a task that a write may carry. All are optional on an existing task.
      properties:
        title:
          type: string
          maxLength: 500
          description: Required for a new task.
        notes:
          type:
          - string
          - 'null'
          maxLength: 20000
          description: Blank text is stored as `null`.
        day:
          type: string
          pattern: ^(\d{4}-\d{2}-\d{2})?$
          description: A real calendar date as `YYYY-MM-DD`, or `""` for not on a day yet. Default `""`.
        project_id:
          type:
          - string
          - 'null'
          format: uuid
          description: A project in one of your workspaces. An unknown one is refused with `Unknown project.`
        stage:
          type:
          - string
          - 'null'
          enum:
          - todo
          - doing
          - null
        done_at:
          oneOf:
          - $ref: '#/components/schemas/Timestamp'
          - type: 'null'
          description: When it was finished; null if open.
        position:
          type: number
          description: Sort order within a day. A finite number, at most 1e15 in size. Default 0.
        estimate_minutes:
          type:
          - number
          - 'null'
          minimum: 0
          maximum: 1000000
        source:
          type:
          - string
          - 'null'
          pattern: ^[a-z][a-z0-9_-]{0,31}$
          description: Where it was imported from, as a short lower case name such as `linear`, `jira` or `github`.
        external_id:
          type:
          - string
          - 'null'
          maxLength: 200
          description: The id in that tool. One line.
        external_key:
          type:
          - string
          - 'null'
          maxLength: 100
          description: The key people see, such as `ENG-142`. One line.
          examples:
          - ENG-142
        external_url:
          type:
          - string
          - 'null'
          maxLength: 2000
          description: An `http` or `https` link to the issue. Other schemes are refused.
        issue:
          $ref: '#/components/schemas/TaskIssueWrite'
        created_at:
          $ref: '#/components/schemas/Timestamp'
          description: Defaults to the time of the first write.
    TaskWrite:
      description: Body of `PUT /v1/tasks/{id}`. Unknown fields, including `client_updated_at`, are ignored.
      allOf:
      - $ref: '#/components/schemas/TaskFields'
      - type: object
        properties:
          id:
            type: string
            format: uuid
            description: Optional. If you send it, it must equal the id in the URL.
          done:
            type: boolean
            description: 'A shortcut for `done_at`: `true` sets it to now (an already finished task keeps its time), `false`
    
              clears it. Ignored when you send `done_at`.
    
              '
    TaskTime:
      type: object
      required:
      - seconds
      - entries
      properties:
        seconds:
          type: integer
          description: Total length of your live entries linked to the task.
        entries:
          type: integer
          description: How many entries that is.
    TaskListResponse:
      type: object
      required:
      - tasks
      - subtasks
      - links
      - time
      properties:
        tasks:
          type: array
          items:
            $ref: '#/components/schemas/Task'
        subtasks:
          type: array
          description: Live checklist items of the tasks in this page, by task then position.
          items:
            $ref: '#/components/schemas/Subtask'
        links:
          type: array
          description: Live entry links of the tasks in this page.
          items:
            $ref: '#/components/schemas/TaskLink'
        time:
          type: object
          description: Logged time per task id. Tasks with no linked entries are left out.
          additionalProperties:
            $ref: '#/components/schemas/TaskTime'
        more:
          type: boolean
          const: true
          description: Present when there are more tasks after this page.
        next_cursor:
          type: string
          description: Send as `cursor` to get the next page. Present with `more`.
    TaskDetailResponse:
      type: object
      required:
      - task
      - subtasks
      - links
      - entries
      - time
      properties:
        task:
          $ref: '#/components/schemas/Task'
        subtasks:
          type: array
          items:
            $ref: '#/components/schemas/Subtask'
        links:
          type: array
          items:
            $ref: '#/components/schemas/TaskLink'
        entries:
          type: array
          description: Your live entries linked to the task, oldest first.
          items:
            $ref: '#/components/schemas/Entry'
        time:
          $ref: '#/components/schemas/TaskTime'
    TaskResponse:
      type: object
      required:
      - task
      properties:
        task:
          $ref: '#/components/schemas/Task'
    SubtaskFields:
      type: object
      description: The fields of a checklist item that a write may carry.
      properties:
        task_id:
          type: string
          format: uuid
          description: One of your tasks. Required for a new item.
        title:
          type: string
          maxLength: 500
          description: Required for a new item.
        done:
          oneOf:
          - type: boolean
          - type: integer
            enum:
            - 0
            - 1
          description: Default `false`.
        position:
          type: number
          description: Sort order within the task. A finite number, at most 1e15 in size. Default 0.
        created_at:
          $ref: '#/components/schemas/Timestamp'
          description: Defaults to the time of the first write.
    SubtaskWrite:
      description: Body of `PUT /v1/subtasks/{id}`. Unknown fields are ignored.
      allOf:
      - $ref: '#/components/schemas/SubtaskFields'
      - type: object
        properties:
          id:
            type: string
            format: uuid
            description: Optional. If you send it, it must equal the id in the URL.
    SubtaskResponse:
      type: object
      required:
      - subtask
      properties:
        subtask:
          $ref: '#/components/schemas/Subtask'
    TaskLinkResponse:
      type: object
      required:
      - link
      properties:
        link:
          $ref: '#/components/schemas/TaskLink'
    PlanBlockFields:
      type: object
      description: The fields of a planned block that a write may carry.
      properties:
        project_id:
          type:
          - string
          - 'null'
          format: uuid
          description: A project in one of your workspaces. An unknown one is refused with `Unknown project.`
        task_id:
          type:
          - string
          - 'null'
          format: uuid
          description: One of your tasks. An unknown one is refused with `Unknown task.`
        title:
          type: string
          maxLength: 500
          description: Default `""`.
        started_at:
          $ref: '#/components/schemas/Timestamp'
          description: Required for a new block.
        ended_at:
          $ref: '#/components/schemas/Timestamp'
          description: Required for a new block. After `started_at`, at most 7 days later.
        created_at:
          $ref: '#/components/schemas/Timestamp'
          description: Defaults to the time of the first write.
    PlanBlockWrite:
      description: Body of `PUT /v1/plan-blocks/{id}`. Unknown fields are ignored.
      allOf:
      - $ref: '#/components/schemas/PlanBlockFields'
      - type: object
        properties:
          id:
            type: string
            format: uuid
            description: Optional. If you send it, it must equal the id in the URL.
    PlanBlockResponse:
      type: object
      required:
      - plan_block
      properties:
        plan_block:
          $ref: '#/components/schemas/PlanBlock'
    PlanBlockListResponse:
      type: object
      required:
      - plan_blocks
      properties:
        plan_blocks:
          type: array
          items:
            $ref: '#/components/schemas/PlanBlock'
    PersonalSyncTaskRow:
      description: A task in a sync request. Send only the fields that changed.
      allOf:
      - $ref: '#/components/schemas/TaskFields'
      - type: object
        required:
        - id
        - client_updated_at
        properties:
          id:
            type: string
            format: uuid
          client_updated_at:
            $ref: '#/components/schemas/Timestamp'
          deleted_at:
            oneOf:
            - $ref: '#/components/schemas/Timestamp'
            - type: 'null'
            description: Set to delete the row. Left out or null means live.
    PersonalSyncSubtaskRow:
      description: A checklist item in a sync request. Send only the fields that changed.
      allOf:
      - $ref: '#/components/schemas/SubtaskFields'
      - type: object
        required:
        - id
        - client_updated_at
        properties:
          id:
            type: string
            format: uuid
          client_updated_at:
            $ref: '#/components/schemas/Timestamp'
          deleted_at:
            oneOf:
            - $ref: '#/components/schemas/Timestamp'
            - type: 'null'
    PersonalSyncTaskLinkRow:
      type: object
      description: A task to entry link in a sync request. The task must be yours; the entry isn't checked.
      required:
      - task_id
      - entry_id
      - client_updated_at
      properties:
        task_id:
          type: string
          format: uuid
        entry_id:
          type: string
          format: uuid
        client_updated_at:
          $ref: '#/components/schemas/Timestamp'
        deleted_at:
          oneOf:
          - $ref: '#/components/schemas/Timestamp'
          - type: 'null'
    PersonalSyncPlanBlockRow:
      description: A planned block in a sync request. Send only the fields that changed.
      allOf:
      - $ref: '#/components/schemas/PlanBlockFields'
      - type: object
        required:
        - id
        - client_updated_at
        properties:
          id:
            type: string
            format: uuid
          client_updated_at:
            $ref: '#/components/schemas/Timestamp'
          deleted_at:
            oneOf:
            - $ref: '#/components/schemas/Timestamp'
            - type: 'null'
    PersonalSyncRequest:
      type: object
      description: At most 2,000 rows in all, across the four lists.
      properties:
        cursor:
          type:
          - integer
          - 'null'
          minimum: 0
          description: The `cursor` from your last answer. Null or left out starts from the beginning.
        tasks:
          type: array
          items:
            $ref: '#/components/schemas/PersonalSyncTaskRow'
        subtasks:
          type: array
          items:
            $ref: '#/components/schemas/PersonalSyncSubtaskRow'
        task_links:
          type: array
          items:
            $ref: '#/components/schemas/PersonalSyncTaskLinkRow'
        plan_blocks:
          type: array
          items:
            $ref: '#/components/schemas/PersonalSyncPlanBlockRow'
    PersonalSyncResponse:
      type: object
      required:
      - cursor
      - tasks
      - subtasks
      - task_links
      - plan_blocks
      - timer
      - rejected
      properties:
        cursor:
          type: integer
          description: Send this back next time.
        tasks:
          type: array
          description: Includes deleted rows (with `deleted_at` set). At most 500 per page.
          items:
            $ref: '#/components/schemas/Task'
        subtasks:
          type: array
          description: Includes deleted rows. At most 5,000 per page.
          items:
            $ref: '#/components/schemas/Subtask'
        task_links:
          type: array
          description: Includes deleted rows. At most 5,000 per page.
          items:
            $ref: '#/components/schemas/TaskLink'
        plan_blocks:
          type: array
          description: Includes deleted rows. At most 5,000 per page.
          items:
            $ref: '#/components/schemas/PlanBlock'
        timer:
          description: Your running timer right now, whatever the cursor. Null if none.
          oneOf:
          - $ref: '#/components/schemas/Timer'
          - type: 'null'
        rejected:
          type: array
          description: Rows the server refused, with a reason such as `unknown task`, `unknown project`, `title is required` or
            `too many tasks`.
          items:
            $ref: '#/components/schemas/Rejected'
        more:
          type: boolean
          const: true
          description: Present when there is more to fetch. Call again with the new cursor.
        reset:
          type: boolean
          const: true
          description: Present when your cursor was too old and this answer starts from the beginning.
    ProjectList:
      type: object
      required:
      - projects
      properties:
        projects:
          type: array
          items:
            $ref: '#/components/schemas/Project'
    ClientList:
      type: object
      required:
      - clients
      properties:
        clients:
          type: array
          items:
            $ref: '#/components/schemas/Client'
    TagList:
      type: object
      required:
      - tags
      properties:
        tags:
          type: array
          items:
            $ref: '#/components/schemas/Tag'
    ProjectPut:
      type: object
      description: Unknown fields are ignored. `id` may be sent, but must match the path.
      required:
      - workspace_id
      - name
      - color
      properties:
        workspace_id:
          type: string
          format: uuid
          description: The workspace the project belongs to. You must be an owner or admin there.
        client_id:
          type:
          - string
          - 'null'
          format: uuid
          description: A client of the same workspace. Default `null`.
        name:
          type: string
          minLength: 1
          maxLength: 200
        color:
          $ref: '#/components/schemas/Color'
        rate:
          type:
          - number
          - 'null'
          minimum: 0
          maximum: 100000
          description: Hourly rate. Default `null`.
        budget_hours:
          type:
          - number
          - 'null'
          minimum: 0
          maximum: 1000000
          description: Default `null`.
        archived:
          type: boolean
          description: Default `false`.
        created_at:
          $ref: '#/components/schemas/Timestamp'
          description: Default is the stored value, or now for a new project.
        deleted_at:
          oneOf:
          - $ref: '#/components/schemas/Timestamp'
          - type: 'null'
          description: Default `null`, which undeletes a deleted project.
        billable_default:
          type: boolean
          description: Default `true` for a new project, otherwise unchanged.
        budget_amount:
          type:
          - number
          - 'null'
          minimum: 0
          maximum: 1000000000
          description: Unchanged if left out.
        budget_period:
          type:
          - string
          - 'null'
          enum:
          - total
          - monthly
          - weekly
          - null
          description: Unchanged if left out.
        currency:
          type:
          - string
          - 'null'
          pattern: ^[A-Z]{3}$
          description: Null means the workspace currency. Unchanged if left out.
        requires_tags:
          type: boolean
          description: Default `false` for a new project, otherwise unchanged.
        requires_notes:
          type: boolean
          description: Default `false` for a new project, otherwise unchanged.
        external_id:
          $ref: '#/components/schemas/ExternalId'
          description: Left out keeps the stored value; `null` clears it.
    ClientPut:
      type: object
      description: Unknown fields are ignored. `id` may be sent, but must match the path.
      required:
      - workspace_id
      - name
      properties:
        workspace_id:
          type: string
          format: uuid
          description: The workspace the client belongs to. You must be an owner or admin there.
        name:
          type: string
          minLength: 1
          maxLength: 200
        email:
          type:
          - string
          - 'null'
          maxLength: 254
          description: Default `null`.
        address:
          type:
          - string
          - 'null'
          maxLength: 1000
          description: Default `null`.
        deleted_at:
          oneOf:
          - $ref: '#/components/schemas/Timestamp'
          - type: 'null'
          description: Default `null`, which undeletes a deleted client.
        external_id:
          $ref: '#/components/schemas/ExternalId'
          description: Left out keeps the stored value; `null` clears it.
    TagPut:
      type: object
      description: Unknown fields are ignored. `id` may be sent, but must match the path.
      required:
      - workspace_id
      - name
      properties:
        workspace_id:
          type: string
          format: uuid
          description: The workspace the tag belongs to. You must be an owner or admin there.
        name:
          type: string
          minLength: 1
          maxLength: 100
          description: Trimmed. No control characters.
        color:
          $ref: '#/components/schemas/Color'
          description: Default `#8A8F98`.
        archived:
          type: boolean
          description: Default `false`.
        deleted_at:
          oneOf:
          - $ref: '#/components/schemas/Timestamp'
          - type: 'null'
          description: Default `null`, which undeletes a deleted tag.
        external_id:
          $ref: '#/components/schemas/ExternalId'
          description: Left out keeps the stored value; `null` clears it.
    WorkspaceCreate:
      type: object
      required:
      - name
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 100
          description: Trimmed. No control characters.
    WorkspaceResponse:
      type: object
      required:
      - workspace
      properties:
        workspace:
          $ref: '#/components/schemas/Workspace'
    InvoiceSettingsPatch:
      type: object
      description: 'Any of the invoice settings. Text fields take `""` or `null` to clear.
    
        '
      properties:
        company_name:
          type:
          - string
          - 'null'
          maxLength: 200
        company_address:
          type:
          - string
          - 'null'
          maxLength: 1000
        tax_id:
          type:
          - string
          - 'null'
          maxLength: 100
        footer:
          type:
          - string
          - 'null'
          maxLength: 2000
        prefix:
          type: string
          maxLength: 20
        next_number:
          type: integer
          minimum: 1
          maximum: 999999999
        default_tax_rate:
          type: number
          minimum: 0
          maximum: 100
          description: Percent.
        payment_terms_days:
          type: integer
          minimum: 0
          maximum: 365
        payment:
          type: object
          description: 'Payment details. Only an owner on a signed-in session may send this. The IBAN is checked and stored
    
            compact and upper case. The SWIFT/BIC code must have 8 or 11 letters and digits.
    
            '
          properties:
            account_name:
              type:
              - string
              - 'null'
              maxLength: 200
            bank_name:
              type:
              - string
              - 'null'
              maxLength: 200
            iban:
              type:
              - string
              - 'null'
              maxLength: 50
            account_number:
              type:
              - string
              - 'null'
              maxLength: 60
            routing:
              type:
              - string
              - 'null'
              maxLength: 60
            swift:
              type:
              - string
              - 'null'
              maxLength: 20
            other:
              type:
              - string
              - 'null'
              maxLength: 1000
    WorkspacePatch:
      type: object
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 100
          description: Not allowed on a personal workspace.
        currency:
          $ref: '#/components/schemas/Currency'
        invoice:
          $ref: '#/components/schemas/InvoiceSettingsPatch'
    PaymentHistoryResponse:
      type: object
      required:
      - changes
      properties:
        changes:
          type: array
          items:
            type: object
            required:
            - at
            - fields
            - by
            properties:
              at:
                $ref: '#/components/schemas/Timestamp'
              fields:
                type: array
                items:
                  type: string
                  enum:
                  - account_name
                  - bank_name
                  - iban
                  - account_number
                  - routing
                  - swift
                  - other
                description: Which payment fields changed. Never their values.
              by:
                description: Who changed them. Null if that account no longer exists.
                oneOf:
                - type: object
                  required:
                  - name
                  - email
                  properties:
                    name:
                      type: string
                      description: Empty if they never set one.
                    email:
                      type: string
                      format: email
                - type: 'null'
    MembersResponse:
      type: object
      required:
      - members
      - invites
      properties:
        members:
          type: array
          items:
            $ref: '#/components/schemas/Member'
        invites:
          type: array
          items:
            $ref: '#/components/schemas/Invite'
          description: Open invites. Empty unless you are an owner or admin.
    MemberPatch:
      type: object
      description: Send at least one field.
      properties:
        role:
          $ref: '#/components/schemas/Role'
          description: Needs a signed-in session. Admins can't set or change `owner`.
        capacity_hours:
          type: number
          minimum: 0
          maximum: 168
          description: Weekly working hours.
        cost_rate:
          type:
          - number
          - 'null'
          minimum: 0
          maximum: 1000000
          description: What an hour of their time costs.
        bill_rate:
          type:
          - number
          - 'null'
          minimum: 0
          maximum: 1000000
          description: Their hourly rate in reports, over the project's.
        external_id:
          $ref: '#/components/schemas/ExternalId'
          description: Their id in another system. `null` clears it.
    InviteCreate:
      type: object
      required:
      - email
      - role
      properties:
        email:
          type: string
          format: email
          maxLength: 254
        role:
          type: string
          enum:
          - admin
          - member
        capacity_hours:
          type:
          - number
          - 'null'
          minimum: 0
          maximum: 168
          description: Weekly hours they'll have once they join. Null or left out means 40.
    InvitePatch:
      type: object
      required:
      - capacity_hours
      properties:
        capacity_hours:
          type:
          - number
          - 'null'
          minimum: 0
          maximum: 168
          description: Weekly hours they'll have once they join. Null means 40.
    InviteAccept:
      type: object
      required:
      - token
      properties:
        token:
          type: string
          minLength: 1
          maxLength: 200
          description: The token from the invitation link.
    TeamPut:
      type: object
      required:
      - name
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 100
          description: Trimmed. One line.
        color:
          $ref: '#/components/schemas/Color'
          description: Default `#6A5BC2`.
    TeamMemberList:
      type: array
      maxItems: 500
      items:
        type: object
        required:
        - user_id
        properties:
          user_id:
            type: string
            format: uuid
          is_lead:
            type: boolean
            description: Default `false`.
    TeamList:
      type: object
      required:
      - teams
      properties:
        teams:
          type: array
          items:
            $ref: '#/components/schemas/Team'
    TeamResponse:
      type: object
      required:
      - team
      properties:
        team:
          $ref: '#/components/schemas/Team'
    SlackConnection:
      type: object
      required:
      - connected
      - hint
      - last_status
      - last_sent_at
      - include_people
      properties:
        connected:
          type: boolean
        hint:
          type:
          - string
          - 'null'
          description: The last 4 characters of the webhook URL. Null when not connected.
        last_status:
          type:
          - string
          - 'null'
          enum:
          - ok
          - failed
          - null
          description: Whether the last post (a test or the weekly summary) was accepted. Null before the first post.
        last_sent_at:
          oneOf:
          - $ref: '#/components/schemas/Timestamp'
          - type: 'null'
        include_people:
          type: boolean
          description: Add each person's hours to the weekly summary (team workspaces).
    SlackPut:
      type: object
      description: Send `webhook_url`, `include_people`, or both.
      properties:
        webhook_url:
          type: string
          maxLength: 300
          description: 'A Slack incoming webhook: `https://hooks.slack.com/services/…`, with no port, query or credentials.'
        include_people:
          type: boolean
