{
  "openapi": "3.0.3",
  "info": {
    "title": "DEJA API",
    "version": "1.0.0",
    "description": "The public HTTP API behind DEJA. It covers three jobs: recording sessions from an installed tracker, managing the projects those recordings land in, and letting an agent finish an install on its own.\n\nEvery error is JSON with a stable `code`, never an HTML page. Every endpoint sends CORS headers and answers preflight. Ingest endpoints deliberately answer 200 for payloads they discard, so a recorder can never surface an error into the page it is recording.",
    "termsOfService": "https://deja.design/about",
    "contact": {
      "name": "DEJA",
      "url": "https://deja.design/developers"
    },
    "license": {
      "name": "Proprietary",
      "url": "https://deja.design/about"
    }
  },
  "servers": [
    {
      "url": "https://deja.design/api/v1",
      "description": "Production. Use this base for new integrations."
    }
  ],
  "tags": [
    {
      "name": "Status",
      "description": "Liveness and release information. No credential."
    },
    {
      "name": "Artifacts",
      "description": "The tracker, the CLI and the install documents. No credential."
    },
    {
      "name": "Ingest",
      "description": "Write paths used by an installed tracker. Project public key."
    },
    {
      "name": "Projects",
      "description": "Manage projects. Organisation API key."
    },
    {
      "name": "Install",
      "description": "Scoped, expiring token so an install can finish itself."
    }
  ],
  "paths": {
    "/health": {
      "get": {
        "operationId": "getHealth",
        "summary": "Check that the DEJA API is serving",
        "description": "Liveness probe. Answers without any credential and reports the tracker version this deployment currently serves. Use it to confirm reachability before a longer integration run.",
        "tags": [
          "Status"
        ],
        "security": [],
        "responses": {
          "200": {
            "description": "The deployment is serving.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Health"
                }
              }
            }
          }
        }
      }
    },
    "/release": {
      "get": {
        "operationId": "getRelease",
        "summary": "Read the release manifest",
        "description": "Returns the current CLI, install-skill and tracker versions, each with a sha256 of the artifact. The CLI verifies a download against this hash before it writes anything, so a truncated response can never overwrite a working install.",
        "tags": [
          "Status"
        ],
        "security": [],
        "responses": {
          "200": {
            "description": "Current release manifest.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Release"
                }
              }
            }
          }
        }
      }
    },
    "/tracker.js": {
      "get": {
        "operationId": "getTrackerBundle",
        "summary": "Download the tracker bundle",
        "description": "The recorder itself, as JavaScript — roughly 30 KB gzipped. This is the exact file an installed site loads; it takes its project key from a `data-deja-key` attribute or a `?key=` parameter on its own script tag.",
        "tags": [
          "Artifacts"
        ],
        "security": [],
        "responses": {
          "200": {
            "description": "The tracker bundle.",
            "content": {
              "text/javascript": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/cli.mjs": {
      "get": {
        "operationId": "getCliBundle",
        "summary": "Download the CLI",
        "description": "The DEJA CLI as one dependency-free ESM file for Node 18 or newer. Verify it against the `cli.sha256` in the release manifest before writing it to disk.",
        "tags": [
          "Artifacts"
        ],
        "security": [],
        "responses": {
          "200": {
            "description": "The CLI bundle.",
            "content": {
              "text/javascript": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/install.md": {
      "get": {
        "operationId": "getInstallGuide",
        "summary": "Read the install guide as Markdown",
        "description": "The end-user install document. Written to be followed by a coding agent working inside someone else's repository with nothing but a public key, so it is plain Markdown with no assumed context.",
        "tags": [
          "Artifacts"
        ],
        "security": [],
        "responses": {
          "200": {
            "description": "The install guide.",
            "content": {
              "text/markdown": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/skill.md": {
      "get": {
        "operationId": "getAgentSkill",
        "summary": "Read the agent install skill as Markdown",
        "description": "The Claude Code skill that installs the tracker into the current repository — which framework gets which snippet, and how to load it without blocking the page. Self-contained on purpose: it is read from repositories that have no other DEJA documentation.",
        "tags": [
          "Artifacts"
        ],
        "security": [],
        "responses": {
          "200": {
            "description": "The install skill.",
            "content": {
              "text/markdown": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/ingest": {
      "post": {
        "operationId": "ingestSessionChunk",
        "summary": "Upload one session-recording chunk",
        "description": "Appends one chunk of a recording to its session. The body is opaque — gzip or raw JSON rrweb events, never decompressed server-side — and all of the chunk's metadata travels in the `X-Deja-Meta` header. Chunks above 1,000,000 bytes are rejected. Anything the server cannot use is discarded and still answered 200, because a recorder must never surface an error into the page it is recording.",
        "tags": [
          "Ingest"
        ],
        "security": [
          {
            "projectPublicKey": []
          }
        ],
        "parameters": [
          {
            "name": "pk",
            "in": "query",
            "required": true,
            "description": "The project's public key (`deja_pk_…`). It is embedded in the page that does the recording and is not a secret; it authorises writes to exactly one project and reads nothing.",
            "schema": {
              "type": "string",
              "pattern": "^deja_pk_[0-9a-f]+$"
            }
          },
          {
            "name": "X-Deja-Meta",
            "in": "header",
            "required": true,
            "description": "JSON object describing the chunk: `sk` (session key), `vid` (visitor id), `seq`, `pageIndex`, `url`, `firstTs`, `lastTs`, `eventCount`, `encoding` (`gzip` or `json`), `startedAt`, and optional viewport, screen and identity fields.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "description": "The chunk, gzip-compressed or raw JSON.",
          "content": {
            "application/octet-stream": {
              "schema": {
                "type": "string",
                "format": "binary"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Chunk accepted, or discarded safely.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IngestAccepted"
                }
              }
            }
          },
          "400": {
            "description": "The `X-Deja-Meta` header is missing or malformed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No project matches the public key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "The chunk exceeded 1,000,000 bytes.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/signals": {
      "post": {
        "operationId": "ingestSignalsBatch",
        "summary": "Upload a batch of page-analysis signals",
        "description": "Records page views, clicks and JavaScript errors — the input to heatmaps, the element leaderboard and the friction feed. This path survives the recording-retention purge, so it is what long-range page analysis is built on. Batches above 256,000 bytes are rejected; individual malformed entries are dropped without failing the batch.",
        "tags": [
          "Ingest"
        ],
        "security": [
          {
            "projectPublicKey": []
          }
        ],
        "parameters": [
          {
            "name": "pk",
            "in": "query",
            "required": true,
            "description": "The project's public key (`deja_pk_…`). It is embedded in the page that does the recording and is not a secret; it authorises writes to exactly one project and reads nothing.",
            "schema": {
              "type": "string",
              "pattern": "^deja_pk_[0-9a-f]+$"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SignalsBatch"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Batch accepted, or discarded safely.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IngestAccepted"
                }
              }
            }
          },
          "400": {
            "description": "The body was not JSON, or not a signals batch.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No project matches the public key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "The batch exceeded 256,000 bytes.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/cli/whoami": {
      "post": {
        "operationId": "getApiKeyIdentity",
        "summary": "Identify the calling API key",
        "description": "Reports which organisation and user an API key belongs to, and the label it was issued under. The cheapest way to verify a credential before doing real work with it.",
        "tags": [
          "Projects"
        ],
        "security": [
          {
            "apiKey": []
          }
        ],
        "responses": {
          "200": {
            "description": "The key's identity.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiKeyIdentity"
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed, invalid or expired credential.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/cli/projects.list": {
      "post": {
        "operationId": "listProjects",
        "summary": "List every project the key can see",
        "description": "Returns all projects in the API key's organisation, each with its public key, domain and recorded session count. This is how an integration discovers the project id every other call needs.",
        "tags": [
          "Projects"
        ],
        "security": [
          {
            "apiKey": []
          }
        ],
        "responses": {
          "200": {
            "description": "The organisation's projects.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProjectList"
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed, invalid or expired credential.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/cli/projects.create": {
      "post": {
        "operationId": "createProject",
        "summary": "Create a project",
        "description": "Creates a project in the key's organisation and returns it, including the public key the tracker will need. Supply `pathRules` here when you already know them — page identity is decided when the first session lands, and changing the grouping afterwards means re-reading every stored recording.",
        "tags": [
          "Projects"
        ],
        "security": [
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateProjectRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The created project.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProjectEnvelope"
                }
              }
            }
          },
          "400": {
            "description": "`name` is required.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed, invalid or expired credential.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/cli/projects.path-rules": {
      "post": {
        "operationId": "setProjectPathRules",
        "summary": "Set a project's page-grouping rules",
        "description": "Replaces the URL patterns that decide which URLs count as the same page — `/{org}/{project}/*` collapses every org and project into one page rather than thousands. A pattern that does not compile is dropped at read time rather than rejected here, so one typo can never take a project's ingest down.",
        "tags": [
          "Projects"
        ],
        "security": [
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PathRulesRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The project's new rules.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProjectEnvelope"
                }
              }
            }
          },
          "400": {
            "description": "The body or parameters did not validate.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed, invalid or expired credential.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such endpoint, project or resource.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/cli/sessions.count": {
      "post": {
        "operationId": "countProjectSessions",
        "summary": "Count a project's recorded sessions",
        "description": "Returns how many sessions have been recorded for one project. Used to confirm an install is live — poll it after installing the tracker and watch it move off zero.",
        "tags": [
          "Projects"
        ],
        "security": [
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "projectId"
                ],
                "properties": {
                  "projectId": {
                    "type": "string",
                    "description": "Project to count."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The session count.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SessionCount"
                }
              }
            }
          },
          "400": {
            "description": "The body or parameters did not validate.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed, invalid or expired credential.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such endpoint, project or resource.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/setup/whoami": {
      "post": {
        "operationId": "getSetupTokenProject",
        "summary": "Identify the project behind a setup token",
        "description": "Reports the single project a setup token is scoped to, with its current path rules and session count. A setup token cannot read sessions and cannot list projects, so this is the whole of what it can see.",
        "tags": [
          "Install"
        ],
        "security": [
          {
            "setupToken": []
          }
        ],
        "responses": {
          "200": {
            "description": "The token's project.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SetupProject"
                }
              }
            }
          },
          "401": {
            "description": "The setup token is invalid or expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/setup/path-rules": {
      "post": {
        "operationId": "setSetupPathRules",
        "summary": "Set page-grouping rules with a setup token",
        "description": "Sets the path rules on the token's project. This exists so the agent performing an install can finish the job itself: the rules have to be right before the first session arrives, and that cannot depend on a human noticing a message and opening Settings. `backfillNeeded` comes back true only when the project already held pages under the old grouping.",
        "tags": [
          "Install"
        ],
        "security": [
          {
            "setupToken": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SetupPathRulesRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The rules now in force.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SetupPathRulesResult"
                }
              }
            }
          },
          "400": {
            "description": "The patterns did not compile, or the body was not a rules array.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The setup token is invalid or expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "Error": {
        "type": "object",
        "description": "Every failure on this API is JSON in this shape. `error` is the human-readable message and stays a plain string for backwards compatibility with installed CLIs; `code` is the stable machine-readable discriminator to branch on; `hint` says what to do next where there is something to do.",
        "required": [
          "error",
          "code"
        ],
        "properties": {
          "error": {
            "type": "string",
            "description": "Human-readable explanation."
          },
          "code": {
            "type": "string",
            "description": "Stable error identifier — branch on this, not on the message.",
            "enum": [
              "invalid_request",
              "unauthorized",
              "not_found",
              "not_acceptable",
              "payload_too_large",
              "internal_error"
            ]
          },
          "hint": {
            "type": "string",
            "description": "Suggested resolution, when one exists."
          },
          "documentation_url": {
            "type": "string",
            "format": "uri",
            "description": "Where the relevant contract is documented."
          },
          "ok": {
            "type": "boolean",
            "description": "Present and false on the ingest endpoints, which carry an `ok` discriminator on every response."
          },
          "invalid": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "The path-rule patterns that failed to compile, when that is the failure."
          }
        }
      },
      "Health": {
        "type": "object",
        "required": [
          "ok",
          "service",
          "version"
        ],
        "properties": {
          "ok": {
            "type": "boolean",
            "description": "True when the deployment is serving."
          },
          "service": {
            "type": "string",
            "description": "Always \"deja\"."
          },
          "version": {
            "type": "string",
            "description": "Version of the tracker bundle this deployment serves."
          }
        }
      },
      "Release": {
        "type": "object",
        "description": "Manifest of the self-updating artifacts. Each entry carries a sha256 the CLI verifies before writing anything to disk.",
        "properties": {
          "cli": {
            "type": "object",
            "description": "The published CLI artifact.",
            "properties": {
              "version": {
                "type": "string",
                "description": "Current CLI version."
              },
              "sha256": {
                "type": "string",
                "description": "Hex sha256 of /cli.mjs."
              }
            }
          },
          "skill": {
            "type": "object",
            "description": "The published install-skill artifact.",
            "properties": {
              "version": {
                "type": "string",
                "description": "Current install-skill version."
              },
              "sha256": {
                "type": "string",
                "description": "Hex sha256 of /skill.md."
              }
            }
          },
          "tracker": {
            "type": "object",
            "description": "The published tracker bundle.",
            "properties": {
              "version": {
                "type": "string",
                "description": "Current tracker version."
              }
            }
          }
        }
      },
      "IngestAccepted": {
        "type": "object",
        "required": [
          "ok"
        ],
        "description": "Ingest answers `{ \"ok\": true }` for anything it accepted or safely discarded. A recorder must never be able to break the page it is recording, so malformed payloads are dropped rather than rejected.",
        "properties": {
          "ok": {
            "type": "boolean",
            "description": "Always true. Ingest does not report per-payload rejection."
          }
        }
      },
      "SignalsBatch": {
        "type": "object",
        "required": [
          "sk",
          "vid"
        ],
        "description": "One batch of page-analysis signals. Entries that do not validate are dropped individually; the rest of the batch is still recorded.",
        "properties": {
          "sk": {
            "type": "string",
            "description": "Session key this batch belongs to.",
            "maxLength": 256
          },
          "vid": {
            "type": "string",
            "description": "Stable per-visitor id.",
            "maxLength": 256
          },
          "startedAt": {
            "type": "integer",
            "format": "int64",
            "description": "Session start, epoch milliseconds. Clamped to a window around the server clock."
          },
          "dropped": {
            "type": "integer",
            "description": "How many signals the client dropped before sending, if it had to."
          },
          "views": {
            "type": "array",
            "maxItems": 200,
            "description": "Page views in this batch.",
            "items": {
              "$ref": "#/components/schemas/SignalView"
            }
          },
          "clicks": {
            "type": "array",
            "maxItems": 2000,
            "description": "Clicks in this batch, ordinary and friction alike.",
            "items": {
              "$ref": "#/components/schemas/SignalClick"
            }
          },
          "errors": {
            "type": "array",
            "maxItems": 100,
            "description": "JavaScript errors observed in this batch.",
            "items": {
              "$ref": "#/components/schemas/SignalError"
            }
          }
        }
      },
      "SignalView": {
        "type": "object",
        "required": [
          "url",
          "startedAt"
        ],
        "properties": {
          "url": {
            "type": "string",
            "maxLength": 2000,
            "description": "Full URL of the view."
          },
          "title": {
            "type": "string",
            "maxLength": 256,
            "description": "Document title."
          },
          "vw": {
            "type": "integer",
            "description": "Viewport width in CSS pixels."
          },
          "vh": {
            "type": "integer",
            "description": "Viewport height in CSS pixels."
          },
          "docH": {
            "type": "integer",
            "description": "Full document height in CSS pixels."
          },
          "startedAt": {
            "type": "integer",
            "format": "int64",
            "description": "Epoch milliseconds."
          },
          "endedAt": {
            "type": "integer",
            "format": "int64",
            "description": "Epoch milliseconds."
          },
          "activeMs": {
            "type": "integer",
            "description": "Milliseconds the tab was actually active."
          },
          "maxScrollPct": {
            "type": "number",
            "description": "Deepest scroll reached, 0–100."
          },
          "exit": {
            "type": "boolean",
            "description": "True if the session ended on this view."
          },
          "quickBack": {
            "type": "boolean",
            "description": "True if the visitor bounced straight back."
          }
        }
      },
      "SignalClick": {
        "type": "object",
        "required": [
          "url",
          "ts",
          "xRatio",
          "yDoc"
        ],
        "properties": {
          "url": {
            "type": "string",
            "maxLength": 2000,
            "description": "URL the click happened on."
          },
          "ts": {
            "type": "integer",
            "format": "int64",
            "description": "Epoch milliseconds."
          },
          "xRatio": {
            "type": "number",
            "description": "Horizontal position as a fraction of viewport width."
          },
          "yDoc": {
            "type": "number",
            "description": "Vertical position in document pixels."
          },
          "vw": {
            "type": "integer",
            "description": "Viewport width at click time."
          },
          "docH": {
            "type": "integer",
            "description": "Document height at click time."
          },
          "selector": {
            "type": "string",
            "maxLength": 256,
            "description": "Stable selector for the element."
          },
          "label": {
            "type": "string",
            "maxLength": 256,
            "description": "Visible label of the element."
          },
          "tag": {
            "type": "string",
            "maxLength": 40,
            "description": "Tag name of the element."
          },
          "interactive": {
            "type": "boolean",
            "description": "Whether the element was actually clickable."
          },
          "kind": {
            "type": "string",
            "enum": [
              "click",
              "rage",
              "dead",
              "error"
            ],
            "description": "Friction classification. Anything else is discarded."
          },
          "n": {
            "type": "integer",
            "description": "Repeat count, for collapsed rage clicks."
          }
        }
      },
      "SignalError": {
        "type": "object",
        "required": [
          "url",
          "ts",
          "message"
        ],
        "properties": {
          "url": {
            "type": "string",
            "maxLength": 2000,
            "description": "URL the error happened on."
          },
          "ts": {
            "type": "integer",
            "format": "int64",
            "description": "Epoch milliseconds."
          },
          "message": {
            "type": "string",
            "maxLength": 256,
            "description": "Error message."
          }
        }
      },
      "ApiKeyIdentity": {
        "type": "object",
        "required": [
          "orgId",
          "userId"
        ],
        "properties": {
          "orgId": {
            "type": "string",
            "description": "Organisation the key belongs to."
          },
          "userId": {
            "type": "string",
            "description": "User who issued the key."
          },
          "label": {
            "type": "string",
            "description": "Label given to the key."
          }
        }
      },
      "Project": {
        "type": "object",
        "required": [
          "id",
          "name",
          "publicKey",
          "sessionCount"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "Project id — the handle every other call takes."
          },
          "name": {
            "type": "string",
            "description": "Display name."
          },
          "domain": {
            "type": "string",
            "nullable": true,
            "description": "Primary domain, if one was given."
          },
          "publicKey": {
            "type": "string",
            "description": "The `deja_pk_…` key the tracker on this project's site sends."
          },
          "sessionCount": {
            "type": "integer",
            "description": "Sessions recorded so far."
          },
          "createdAt": {
            "type": "number",
            "format": "double",
            "description": "Creation time, epoch milliseconds."
          },
          "pathRules": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "URL patterns that decide how pages are grouped."
          }
        }
      },
      "ProjectList": {
        "type": "object",
        "required": [
          "projects"
        ],
        "properties": {
          "projects": {
            "type": "array",
            "description": "Every project in the organisation.",
            "items": {
              "$ref": "#/components/schemas/Project"
            }
          }
        }
      },
      "ProjectEnvelope": {
        "type": "object",
        "required": [
          "project"
        ],
        "properties": {
          "project": {
            "allOf": [
              {
                "$ref": "#/components/schemas/Project"
              }
            ],
            "description": "The project this call created or updated."
          }
        }
      },
      "CreateProjectRequest": {
        "type": "object",
        "required": [
          "name"
        ],
        "properties": {
          "name": {
            "type": "string",
            "description": "Display name for the project."
          },
          "domain": {
            "type": "string",
            "description": "Primary domain, e.g. `example.com`."
          },
          "pathRules": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Optional URL patterns for page grouping, e.g. `/{org}/{project}/*`. Set them at creation: page identity is decided when the first session lands."
          }
        }
      },
      "PathRulesRequest": {
        "type": "object",
        "required": [
          "projectId",
          "rules"
        ],
        "properties": {
          "projectId": {
            "type": "string",
            "description": "Project to update."
          },
          "rules": {
            "type": "array",
            "minItems": 1,
            "items": {
              "type": "string"
            },
            "description": "Patterns, e.g. `[\"/{org}/{project}\", \"/{org}/{project}/*\"]`."
          }
        }
      },
      "SessionCount": {
        "type": "object",
        "required": [
          "count"
        ],
        "properties": {
          "count": {
            "type": "integer",
            "description": "Sessions recorded for the project."
          }
        }
      },
      "SetupProject": {
        "type": "object",
        "required": [
          "project"
        ],
        "properties": {
          "project": {
            "type": "object",
            "description": "The one project this setup token is scoped to.",
            "required": [
              "name",
              "sessionCount"
            ],
            "properties": {
              "name": {
                "type": "string",
                "description": "Project the token is scoped to."
              },
              "domain": {
                "type": "string",
                "nullable": true,
                "description": "Primary domain."
              },
              "pathRules": {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "description": "Current page-grouping patterns."
              },
              "sessionCount": {
                "type": "integer",
                "description": "Sessions recorded so far."
              }
            }
          }
        }
      },
      "SetupPathRulesRequest": {
        "type": "object",
        "required": [
          "rules"
        ],
        "properties": {
          "rules": {
            "type": "array",
            "minItems": 1,
            "items": {
              "type": "string"
            },
            "description": "Patterns to set on the token's project."
          }
        }
      },
      "SetupPathRulesResult": {
        "type": "object",
        "required": [
          "ok",
          "pathRules"
        ],
        "properties": {
          "ok": {
            "type": "boolean",
            "description": "True when the rules were applied."
          },
          "project": {
            "type": "object",
            "description": "The project that was updated.",
            "properties": {
              "name": {
                "type": "string",
                "description": "Project that was updated."
              }
            }
          },
          "pathRules": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Patterns now in force."
          },
          "backfillNeeded": {
            "type": "boolean",
            "description": "True when the project already held pages under the previous grouping, and those need re-deriving. False at install time."
          }
        }
      }
    },
    "securitySchemes": {
      "apiKey": {
        "type": "http",
        "scheme": "bearer",
        "description": "An organisation API key (`deja_sk_…`) from the DEJA dashboard, sent as `Authorization: Bearer`. Reaches every project in the issuing organisation — treat it as a secret."
      },
      "setupToken": {
        "type": "http",
        "scheme": "bearer",
        "description": "A short-lived setup token (`deja_st_…`) issued for one install, sent as `Authorization: Bearer`. Scoped to one project and one field, and it expires."
      },
      "projectPublicKey": {
        "type": "apiKey",
        "in": "query",
        "name": "pk",
        "description": "A project public key (`deja_pk_…`). Write-only, one project, and not a secret — it ships in the page source of the site being recorded."
      }
    }
  }
}