{
  "openapi": "3.1.0",
  "info": {
    "title": "Lucent API",
    "description": "HTTP APIs for Lucent. The Data API reads Lucent data and updates issue status. The Ingest API receives rrweb batches from the @lucenthq/sdk browser package and custom integrations built on top of the same contract.",
    "license": { "name": "MIT" },
    "version": "0.1.0"
  },
  "servers": [
    {
      "url": "https://app.lucenthq.com",
      "description": "Production app"
    }
  ],
  "paths": {
    "/api/sdk/replay": {
      "servers": [
        {
          "url": "https://batch-jobs-lucent.onrender.com",
          "description": "Production ingest"
        }
      ],
      "post": {
        "summary": "Submit a replay batch",
        "description": "Accepts a batch of rrweb events for a single session window. Batches are idempotent per `(sessionId, sequence)` — the same batch submitted twice is stored once. When `flush` is `final`, the server dispatches the session for processing immediately; otherwise the session is left in a `receiving` state until a scheduled finalizer picks it up.",
        "operationId": "sdkReplay",
        "security": [{ "lucentApiKey": [] }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/ReplayBatch" }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Batch accepted.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["ok"],
                  "properties": {
                    "ok": { "type": "boolean", "const": true }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid replay batch.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["error"],
                  "properties": {
                    "error": {
                      "type": "string",
                      "example": "Invalid replay batch"
                    },
                    "details": {
                      "type": "array",
                      "description": "Zod validation issues.",
                      "items": { "type": "object" }
                    }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "500": {
            "description": "Storage upload or server error.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["error"],
                  "properties": { "error": { "type": "string" } }
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/issues": {
      "get": {
        "summary": "List issues",
        "description": "Lists issues visible to the bearer token's organization, ordered by `createdAt` descending and then `id` descending.",
        "operationId": "listIssues",
        "security": [{ "lucentBearer": [] }],
        "parameters": [
          { "$ref": "#/components/parameters/LimitIssues" },
          { "$ref": "#/components/parameters/Cursor" },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "Filter issues by status.",
            "schema": { "$ref": "#/components/schemas/IssueStatus" }
          }
        ],
        "responses": {
          "200": {
            "description": "Issues page.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ListIssuesResponse" }
              }
            }
          },
          "400": { "$ref": "#/components/responses/DataBadRequest" },
          "401": { "$ref": "#/components/responses/DataUnauthorized" },
          "429": { "$ref": "#/components/responses/DataRateLimited" }
        }
      }
    },
    "/api/v1/issues/{issueId}": {
      "get": {
        "summary": "Get an issue",
        "description": "Fetches a single issue by UUID for the bearer token's organization.",
        "operationId": "getIssue",
        "security": [{ "lucentBearer": [] }],
        "parameters": [{ "$ref": "#/components/parameters/IssueId" }],
        "responses": {
          "200": {
            "description": "Issue details.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/GetIssueResponse" }
              }
            }
          },
          "400": { "$ref": "#/components/responses/DataBadRequest" },
          "401": { "$ref": "#/components/responses/DataUnauthorized" },
          "404": { "$ref": "#/components/responses/DataIssueNotFound" },
          "429": { "$ref": "#/components/responses/DataRateLimited" }
        }
      },
      "patch": {
        "summary": "Update issue status",
        "description": "Updates an issue's status. The operation is idempotent: setting the current status again returns `200` with the unchanged issue.",
        "operationId": "updateIssueStatus",
        "security": [{ "lucentBearer": [] }],
        "parameters": [{ "$ref": "#/components/parameters/IssueId" }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["status"],
                "properties": {
                  "status": { "$ref": "#/components/schemas/IssueStatus" }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated issue details.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/GetIssueResponse" }
              }
            }
          },
          "400": { "$ref": "#/components/responses/DataBadRequest" },
          "401": { "$ref": "#/components/responses/DataUnauthorized" },
          "403": { "$ref": "#/components/responses/DataForbidden" },
          "404": { "$ref": "#/components/responses/DataIssueNotFound" },
          "429": { "$ref": "#/components/responses/DataRateLimited" }
        }
      }
    },
    "/api/v1/signals": {
      "get": {
        "summary": "List signals",
        "description": "Lists configured signals and match counts for the bearer token's organization.",
        "operationId": "listSignals",
        "security": [{ "lucentBearer": [] }],
        "parameters": [
          { "$ref": "#/components/parameters/LimitSignals" },
          { "$ref": "#/components/parameters/Cursor" }
        ],
        "responses": {
          "200": {
            "description": "Signals page.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ListSignalsResponse" }
              }
            }
          },
          "400": { "$ref": "#/components/responses/DataBadRequest" },
          "401": { "$ref": "#/components/responses/DataUnauthorized" },
          "429": { "$ref": "#/components/responses/DataRateLimited" }
        }
      }
    },
    "/api/v1/insights": {
      "get": {
        "summary": "List insights",
        "description": "Lists recent AI-generated insights for the bearer token's organization.",
        "operationId": "listInsights",
        "security": [{ "lucentBearer": [] }],
        "parameters": [
          { "$ref": "#/components/parameters/LimitInsights" },
          { "$ref": "#/components/parameters/Cursor" }
        ],
        "responses": {
          "200": {
            "description": "Insights page.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ListInsightsResponse"
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/DataBadRequest" },
          "401": { "$ref": "#/components/responses/DataUnauthorized" },
          "429": { "$ref": "#/components/responses/DataRateLimited" }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "lucentApiKey": {
        "type": "apiKey",
        "in": "header",
        "name": "X-Lucent-Api-Key",
        "description": "Public key prefixed with `luc_pk_`. Safe to expose in client-side code. For `navigator.sendBeacon` callers that cannot set headers, the key may also be passed as the `api_key` query parameter."
      },
      "lucentBearer": {
        "type": "http",
        "scheme": "bearer",
        "description": "API keys prefixed with `luc_api_` or OAuth access tokens prefixed with `luc_oat_`. Legacy static keys prefixed with `luc_mcp_` continue to work."
      }
    },
    "parameters": {
      "IssueId": {
        "name": "issueId",
        "in": "path",
        "required": true,
        "description": "Issue UUID.",
        "schema": {
          "type": "string",
          "format": "uuid",
          "example": "87b3a6b6-5af5-438b-a39c-eff148a68ccb"
        }
      },
      "Cursor": {
        "name": "cursor",
        "in": "query",
        "required": false,
        "description": "Opaque pagination cursor returned as `nextCursor` from the previous page.",
        "schema": { "type": "string" }
      },
      "LimitIssues": {
        "name": "limit",
        "in": "query",
        "required": false,
        "description": "Maximum issues to return.",
        "schema": {
          "type": "integer",
          "minimum": 1,
          "maximum": 200,
          "default": 25
        }
      },
      "LimitSignals": {
        "name": "limit",
        "in": "query",
        "required": false,
        "description": "Maximum signals to return.",
        "schema": {
          "type": "integer",
          "minimum": 1,
          "maximum": 200,
          "default": 50
        }
      },
      "LimitInsights": {
        "name": "limit",
        "in": "query",
        "required": false,
        "description": "Maximum insights to return.",
        "schema": {
          "type": "integer",
          "minimum": 1,
          "maximum": 100,
          "default": 25
        }
      }
    },
    "responses": {
      "Unauthorized": {
        "description": "Missing, invalid, or revoked API key.",
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "required": ["error"],
              "properties": {
                "error": {
                  "type": "string",
                  "enum": ["Missing API key", "Invalid or revoked API key"]
                }
              }
            }
          }
        }
      },
      "RateLimited": {
        "description": "Rate limit exceeded (100 requests per minute per API key).",
        "headers": {
          "Retry-After": {
            "description": "Seconds until the rate limit resets.",
            "schema": { "type": "integer", "minimum": 1 }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "required": ["error"],
              "properties": {
                "error": { "type": "string", "const": "Rate limit exceeded" }
              }
            }
          }
        }
      },
      "DataBadRequest": {
        "description": "Invalid request parameter or body.",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/ErrorResponse" }
          }
        }
      },
      "DataUnauthorized": {
        "description": "Missing, malformed, revoked, or unknown bearer token.",
        "headers": {
          "WWW-Authenticate": {
            "description": "Bearer challenge with the required scope.",
            "schema": {
              "type": "string",
              "example": "Bearer scope=\"read:lucent\""
            }
          }
        },
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/ErrorResponse" }
          }
        }
      },
      "DataForbidden": {
        "description": "The bearer token is valid but lacks the required scope.",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/ErrorResponse" }
          }
        }
      },
      "DataIssueNotFound": {
        "description": "The issue was not found in the token's organization.",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/ErrorResponse" }
          }
        }
      },
      "DataRateLimited": {
        "description": "Rate limit exceeded. Reads allow 300 requests per minute; writes allow 60 requests per minute.",
        "headers": {
          "Retry-After": {
            "description": "Seconds until the rate limit resets.",
            "schema": { "type": "integer", "minimum": 1 }
          },
          "X-RateLimit-Limit": {
            "description": "Limit for the current window.",
            "schema": { "type": "integer" }
          },
          "X-RateLimit-Remaining": {
            "description": "Remaining requests in the current window.",
            "schema": { "type": "integer" }
          },
          "X-RateLimit-Reset": {
            "description": "Unix timestamp when the current window resets.",
            "schema": { "type": "integer" }
          }
        },
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/ErrorResponse" }
          }
        }
      }
    },
    "schemas": {
      "ErrorResponse": {
        "type": "object",
        "required": ["error"],
        "properties": {
          "error": { "type": "string" }
        }
      },
      "IssueStatus": {
        "type": "string",
        "enum": ["unresolved", "ticket_created", "transient", "resolved"]
      },
      "IssuePriority": {
        "type": "string",
        "enum": ["low", "medium", "high", "critical"]
      },
      "IssueSummary": {
        "type": "object",
        "required": [
          "id",
          "title",
          "description",
          "status",
          "priority",
          "previewUrl",
          "createdAt",
          "sessionCount"
        ],
        "properties": {
          "id": { "type": "string", "format": "uuid" },
          "title": { "type": "string" },
          "description": { "type": "string", "nullable": true },
          "status": { "$ref": "#/components/schemas/IssueStatus" },
          "priority": { "$ref": "#/components/schemas/IssuePriority" },
          "previewUrl": { "type": "string", "nullable": true },
          "createdAt": { "type": "string", "format": "date-time" },
          "sessionCount": { "type": "integer", "minimum": 0 },
          "ticketProvider": { "type": "string", "nullable": true },
          "ticketUrl": { "type": "string", "nullable": true },
          "ticketIdentifier": { "type": "string", "nullable": true }
        }
      },
      "Issue": {
        "allOf": [
          { "$ref": "#/components/schemas/IssueSummary" },
          {
            "type": "object",
            "required": ["updatedAt"],
            "properties": {
              "stepsToReplicate": { "type": "string", "nullable": true },
              "aiVerified": { "type": "boolean", "nullable": true },
              "updatedAt": { "type": "string", "format": "date-time" }
            }
          }
        ]
      },
      "ListIssuesResponse": {
        "type": "object",
        "required": ["issues", "nextCursor"],
        "properties": {
          "issues": {
            "type": "array",
            "items": { "$ref": "#/components/schemas/IssueSummary" }
          },
          "nextCursor": { "type": "string", "nullable": true }
        }
      },
      "GetIssueResponse": {
        "type": "object",
        "required": ["issue"],
        "properties": {
          "issue": { "$ref": "#/components/schemas/Issue" }
        }
      },
      "Signal": {
        "type": "object",
        "required": [
          "id",
          "name",
          "prompt",
          "status",
          "backfillStatus",
          "createdAt",
          "matchCount"
        ],
        "properties": {
          "id": { "type": "string", "format": "uuid" },
          "name": { "type": "string" },
          "prompt": { "type": "string" },
          "status": { "type": "string" },
          "backfillStatus": { "type": "string", "nullable": true },
          "createdAt": { "type": "string", "format": "date-time" },
          "matchCount": { "type": "integer", "minimum": 0 },
          "lastMatchAt": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          }
        }
      },
      "ListSignalsResponse": {
        "type": "object",
        "required": ["signals", "nextCursor"],
        "properties": {
          "signals": {
            "type": "array",
            "items": { "$ref": "#/components/schemas/Signal" }
          },
          "nextCursor": { "type": "string", "nullable": true }
        }
      },
      "Insight": {
        "type": "object",
        "required": [
          "id",
          "createdAt",
          "intervalStart",
          "intervalEnd",
          "sessionsCount",
          "contentPreview"
        ],
        "properties": {
          "id": { "type": "string", "format": "uuid" },
          "createdAt": { "type": "string", "format": "date-time" },
          "intervalStart": { "type": "string", "format": "date-time" },
          "intervalEnd": { "type": "string", "format": "date-time" },
          "sessionsCount": { "type": "integer", "minimum": 0 },
          "contentPreview": { "type": "string" }
        }
      },
      "ListInsightsResponse": {
        "type": "object",
        "required": ["insights", "nextCursor"],
        "properties": {
          "insights": {
            "type": "array",
            "items": { "$ref": "#/components/schemas/Insight" }
          },
          "nextCursor": { "type": "string", "nullable": true }
        }
      },
      "RrwebEvent": {
        "type": "object",
        "description": "A single rrweb event. Opaque to Lucent — stored and replayed as-is.",
        "required": ["timestamp", "type", "data"],
        "properties": {
          "timestamp": {
            "type": "number",
            "description": "Epoch milliseconds when the event was captured."
          },
          "type": {
            "type": "integer",
            "description": "rrweb event type discriminator."
          },
          "data": {
            "description": "rrweb event payload. Shape depends on `type`."
          }
        }
      },
      "ReplayBatch": {
        "type": "object",
        "required": ["session", "user", "page", "replay"],
        "properties": {
          "session": {
            "type": "object",
            "required": [
              "id",
              "windowId",
              "startedAt",
              "lastActivityAt",
              "url"
            ],
            "properties": {
              "id": {
                "type": "string",
                "minLength": 1,
                "description": "Stable session ID generated by the SDK. Shared across batches belonging to the same session."
              },
              "windowId": {
                "type": "string",
                "minLength": 1,
                "description": "Per-tab window ID. Distinguishes concurrent tabs within a session."
              },
              "startedAt": {
                "type": "number",
                "description": "Epoch milliseconds when the session started."
              },
              "lastActivityAt": {
                "type": "number",
                "description": "Epoch milliseconds of the most recent event in the session so far."
              },
              "url": {
                "type": "string",
                "description": "URL the session started on."
              }
            }
          },
          "user": {
            "type": "object",
            "required": ["anonymousId"],
            "properties": {
              "id": {
                "type": "string",
                "nullable": true,
                "description": "Your internal user ID if `identify` has been called."
              },
              "anonymousId": {
                "type": "string",
                "minLength": 1,
                "description": "Anonymous ID generated by the SDK and persisted in localStorage."
              },
              "properties": {
                "type": "object",
                "additionalProperties": true,
                "nullable": true,
                "description": "Arbitrary user properties. `email` and `name` are surfaced in the dashboard when present."
              }
            }
          },
          "page": {
            "type": "object",
            "required": ["url"],
            "properties": {
              "url": { "type": "string" },
              "referrer": { "type": "string", "nullable": true },
              "viewport": {
                "type": "object",
                "nullable": true,
                "required": ["width", "height"],
                "properties": {
                  "width": { "type": "number" },
                  "height": { "type": "number" }
                }
              }
            }
          },
          "replay": {
            "type": "object",
            "required": ["events", "sequence"],
            "properties": {
              "events": {
                "type": "array",
                "minItems": 1,
                "items": { "$ref": "#/components/schemas/RrwebEvent" },
                "description": "rrweb events in chronological order."
              },
              "sequence": {
                "type": "integer",
                "minimum": 0,
                "description": "Monotonically increasing batch sequence number within a session. Used as an idempotency key together with `session.id`."
              }
            }
          },
          "metadata": {
            "type": "object",
            "nullable": true,
            "properties": {
              "userAgent": { "type": "string", "nullable": true },
              "language": { "type": "string", "nullable": true }
            }
          },
          "flush": {
            "type": "string",
            "enum": ["normal", "final"],
            "default": "normal",
            "description": "`final` tells the server this is the last batch for the session — typically sent from a `beforeunload` handler. `final` batches trigger immediate session processing."
          }
        }
      }
    }
  }
}
