openapi: 3.0.3
info:
  title: Lookout API
  version: "1.0.0"
  description: |
    Lookout exposes two authentication styles:

    - **Bearer token** (Laravel Sanctum personal access token) for `/api/v1/*` management and triage.
    - **Project API key** for `POST /api/ingest` (and related ingest paths), plus key-scoped `GET` event listing.

    Full ingest field tables, throttles, tracing, webhooks, and examples are published at `/docs/ingest` on your Lookout host.

    For GET requests with Sanctum, `api_token` may be passed as a query parameter when `Authorization` cannot be set.

servers:
  - url: https://YOUR_LOOKOUT_HOST
    description: Replace with your app URL (no trailing slash)

tags:
  - name: v1
    description: Sanctum-authenticated JSON API
  - name: ingest
    description: Project API key — send events
  - name: events-key
    description: Project API key — read raw occurrences

paths:
  /api/v1/me:
    get:
      tags: [v1]
      summary: Current user
      security: [{ bearerAuth: [] }]
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/Me"

  /api/v1/projects:
    get:
      tags: [v1]
      summary: List accessible projects
      security: [{ bearerAuth: [] }]
      parameters:
        - in: query
          name: organization_id
          schema: { $ref: "#/components/schemas/Ulid" }
        - in: query
          name: "filter[organization_id]"
          schema: { $ref: "#/components/schemas/Ulid" }
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/perPage"
        - $ref: "#/components/parameters/pageNumber"
        - $ref: "#/components/parameters/pageSize"
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/ProjectListItem" }
                  meta:
                    $ref: "#/components/schemas/PaginationMeta"
    post:
      tags: [v1]
      summary: Create project
      security: [{ bearerAuth: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name, organization_id]
              properties:
                name: { type: string, maxLength: 255 }
                organization_id: { $ref: "#/components/schemas/Ulid" }
                team_ids:
                  type: array
                  items: { $ref: "#/components/schemas/Ulid" }
                tech_stacks:
                  type: array
                  description: Slugs from Project tech stack presets (e.g. php, laravel, node).
                  items: { type: string }
                environment_labels:
                  type: array
                  description: Ingest environment names; defaults apply when omitted or empty.
                  maxItems: 20
                  items: { type: string, maxLength: 64 }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/ProjectCreated"
        "422":
          description: |
            Validation failed (`message` plus `errors`), or the organization is at its plan project limit
            (`code` `project_limit_reached` and `billing_url` to open Billing and upgrade).
          content:
            application/json:
              schema:
                type: object
                properties:
                  message: { type: string }
                  errors:
                    type: object
                    additionalProperties: true
                  code:
                    type: string
                    example: project_limit_reached
                  billing_url:
                    type: string
                    format: uri
                    description: Absolute URL to the in-app billing page (includes upgrade hint query).

  /api/v1/projects/{project}:
    get:
      tags: [v1]
      summary: Show project
      security: [{ bearerAuth: [] }]
      parameters:
        - $ref: "#/components/parameters/projectId"
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/ProjectListItem"
        "404":
          description: Not found

  /api/v1/projects/{project}/errors:
    get:
      tags: [v1]
      summary: Grouped issues (by fingerprint)
      security: [{ bearerAuth: [] }]
      parameters:
        - $ref: "#/components/parameters/projectId"
        - in: query
          name: search
          schema: { type: string }
          description: Structured search tokens and free text (same as web UI).
        - in: query
          name: status
          schema:
            type: string
            enum: [all, open, resolved, ignored]
            default: open
        - in: query
          name: time
          schema:
            type: string
            enum: [24h, 7d, 30d]
            default: 24h
        - in: query
          name: level
          schema: { type: string }
        - in: query
          name: fingerprint
          schema: { type: string }
        - in: query
          name: environment
          schema: { type: string }
        - in: query
          name: release
          schema: { type: string }
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/perPage"
        - $ref: "#/components/parameters/pageNumber"
        - $ref: "#/components/parameters/pageSize"
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/GroupedError" }
                  meta:
                    $ref: "#/components/schemas/PaginationMeta"

  /api/v1/projects/{project}/error-count:
    get:
      tags: [v1]
      summary: Count of grouped issues (same filters as /errors)
      security: [{ bearerAuth: [] }]
      parameters:
        - $ref: "#/components/parameters/projectId"
        - in: query
          name: search
          schema: { type: string }
        - in: query
          name: status
          schema: { type: string, enum: [all, open, resolved, ignored] }
        - in: query
          name: time
          schema: { type: string, enum: [24h, 7d, 30d] }
        - in: query
          name: level
          schema: { type: string }
        - in: query
          name: fingerprint
          schema: { type: string }
        - in: query
          name: environment
          schema: { type: string }
        - in: query
          name: release
          schema: { type: string }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      count: { type: integer }

  /api/v1/projects/{project}/error-occurrence-count:
    get:
      tags: [v1]
      summary: Count of raw occurrences
      security: [{ bearerAuth: [] }]
      parameters:
        - $ref: "#/components/parameters/projectId"
        - in: query
          name: from
          schema: { type: string, format: date }
        - in: query
          name: to
          schema: { type: string, format: date }
        - in: query
          name: time
          schema: { type: string, enum: [24h, 7d, 30d] }
        - in: query
          name: level
          schema: { type: string }
        - in: query
          name: environment
          schema: { type: string }
        - in: query
          name: release
          schema: { type: string }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      count: { type: integer }

  /api/v1/errors/{error}/occurrences:
    get:
      tags: [v1]
      summary: Occurrences for error group
      description: |
        `{error}` is an error event id. When the event has a fingerprint, returns all occurrences
        in that group; otherwise returns only that event.
      security: [{ bearerAuth: [] }]
      parameters:
        - $ref: "#/components/parameters/errorId"
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/perPage"
        - $ref: "#/components/parameters/pageNumber"
        - $ref: "#/components/parameters/pageSize"
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/ErrorEventSummary" }
                  meta:
                    $ref: "#/components/schemas/PaginationMeta"

  /api/v1/error-occurrences/{occurrence}:
    get:
      tags: [v1]
      summary: Single occurrence (full detail)
      security: [{ bearerAuth: [] }]
      parameters:
        - name: occurrence
          in: path
          required: true
          schema: { $ref: "#/components/schemas/Ulid" }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/ErrorEventDetail"

  /api/v1/errors/{error}/resolve:
    post:
      tags: [v1]
      summary: Resolve group or single event
      security: [{ bearerAuth: [] }]
      parameters:
        - $ref: "#/components/parameters/errorId"
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                comment:
                  type: string
                  nullable: true
                  maxLength: 2000
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      status: { type: string }
                      fingerprint:
                        type: string
                        nullable: true

  /api/v1/errors/{error}/open:
    post:
      tags: [v1]
      summary: Reopen group or single event
      security: [{ bearerAuth: [] }]
      parameters:
        - $ref: "#/components/parameters/errorId"
      responses:
        "200":
          description: OK

  /api/v1/errors/{error}/ignore:
    post:
      tags: [v1]
      summary: Ignore group or single event
      security: [{ bearerAuth: [] }]
      parameters:
        - $ref: "#/components/parameters/errorId"
      responses:
        "200":
          description: OK

  /api/v1/errors/{error}/snooze:
    post:
      tags: [v1]
      summary: Snooze fingerprinted group
      security: [{ bearerAuth: [] }]
      parameters:
        - $ref: "#/components/parameters/errorId"
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                preset:
                  type: string
                  enum: [1h, 8h, 24h, 7d]
                until:
                  type: string
                  format: date-time
      responses:
        "200":
          description: OK
        "422":
          description: No fingerprint on event

  /api/v1/errors/{error}/unsnooze:
    post:
      tags: [v1]
      summary: Clear snooze for fingerprinted group
      security: [{ bearerAuth: [] }]
      parameters:
        - $ref: "#/components/parameters/errorId"
      responses:
        "200":
          description: OK
        "422":
          description: No fingerprint on event

  /api/ingest:
    post:
      tags: [ingest]
      summary: Ingest error or event payload
      security: [{ apiKeyHeader: [] }, { apiKeyBearer: [] }]
      description: |
        Authenticate with project API key (header, bearer, or `api_key` in JSON body).
        Returns 202 with `{ "id": "<uuid>" }` when accepted. See `/docs/ingest` for payload shape.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "202":
          description: Accepted
          content:
            application/json:
              schema:
                type: object
                properties:
                  id: { type: string, format: uuid }
        "401":
          description: Invalid API key
        "403":
          description: IP not allowlisted

  /api/v1/ingest:
    post:
      tags: [ingest]
      summary: Ingest (versioned alias)
      security: [{ apiKeyHeader: [] }, { apiKeyBearer: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "202":
          description: Accepted

  /api/v1/errors:
    post:
      tags: [ingest]
      summary: Ingest (compat alias of /api/v1/ingest)
      security: [{ apiKeyHeader: [] }, { apiKeyBearer: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "202":
          description: Accepted

  /api/ingest/profile:
    post:
      tags: [ingest]
      summary: Ingest CPU / performance profile
      security: [{ apiKeyHeader: [] }, { apiKeyBearer: [] }]
      description: |
        Stores profiler JSON under Project → Profiles. Does not count toward the error quota.
        `agent` is one of: excimer, xhprof, tideways, spx, php.manual_pulse, lookout, blackfire,
        datadog, xdebug, newrelic, otel.php, other. First-party formats: `lookout.v1` (aggregate
        frames), `lookout.samples.v1` (time-series stacks). Full field list: `/docs/ingest#profiling`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [agent, format, data]
              properties:
                agent: { type: string }
                format: { type: string }
                data: { type: object, additionalProperties: true }
                trace_id: { type: string }
                transaction: { type: string }
                environment: { type: string }
                release: { type: string }
                commit_sha: { type: string }
                deployed_at: { type: string }
      responses:
        "202":
          description: Accepted
          content:
            application/json:
              schema:
                type: object
                properties:
                  accepted: { type: boolean }
                  id: { type: string }
        "402":
          description: Billing delinquent
        "422":
          description: Validation error

  /api/ingest/log:
    post:
      tags: [ingest]
      summary: Ingest log entries (structured or plain lines)
      security: [{ apiKeyHeader: [] }, { apiKeyBearer: [] }]
      description: |
        Application logs (level, message, attributes) or batched plain lines (nginx, Apache, syslog).
        Does not count toward the error quota. Disabled when `log_ingest_enabled` is false (403).
        Full contract: `/docs/ingest#logs`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                message: { type: string }
                level: { type: string }
                timestamp: {}
                attributes: { type: object, additionalProperties: true }
                logger: { type: string }
                source: { type: string }
                environment: { type: string }
                release: { type: string }
                trace_id: { type: string }
                hostname: { type: string }
                entries:
                  type: array
                  items: { type: object, additionalProperties: true }
                lines:
                  type: array
                  items: { type: string }
      responses:
        "202":
          description: Accepted
          content:
            application/json:
              schema:
                type: object
                properties:
                  accepted: { type: boolean }
                  count: { type: integer }
                  ids:
                    type: array
                    items: { type: string }
        "402":
          description: Billing delinquent
        "403":
          description: Log ingest disabled for project
        "422":
          description: Validation error

  /api/ingest/metric:
    post:
      tags: [ingest]
      summary: Ingest custom metric samples (counter, gauge, distribution)
      security: [{ apiKeyHeader: [] }, { apiKeyBearer: [] }]
      description: |
        Batched counters, gauges, and distribution samples. Does not count toward the error quota.
        Disabled when `metrics_ingest_enabled` is false (403). Full contract: `/docs/ingest#metrics`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name: { type: string }
                kind: { type: string, enum: [counter, gauge, distribution] }
                value: { type: number }
                unit: { type: string }
                timestamp: {}
                attributes: { type: object, additionalProperties: true }
                environment: { type: string }
                release: { type: string }
                trace_id: { type: string }
                entries:
                  type: array
                  items: { type: object, additionalProperties: true }
      responses:
        "202":
          description: Accepted
          content:
            application/json:
              schema:
                type: object
                properties:
                  accepted: { type: boolean }
                  count: { type: integer }
                  ids:
                    type: array
                    items: { type: string }
        "402":
          description: Billing delinquent
        "403":
          description: Metric ingest disabled for project
        "422":
          description: Validation error

  /api/ingest/feedback:
    post:
      tags: [ingest]
      summary: Attach end-user feedback to a specific error occurrence
      security: [{ apiKeyHeader: [] }, { apiKeyBearer: [] }]
      description: |
        Crash-page feedback tied to one stored occurrence (ULID `event_id` or `occurrence_uuid`).
        Does not count toward the error quota. Disabled when `user_feedback_ingest_enabled` is false (403).
        Full contract: `/docs/ingest#user-feedback`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [comments]
              properties:
                comments: { type: string, description: User message (alias message). }
                message: { type: string }
                event_id: { type: string, description: Error occurrence ULID (alias eventId). }
                eventId: { type: string }
                occurrence_uuid: { type: string, format: uuid }
                name: { type: string }
                email: { type: string, format: email }
      responses:
        "202":
          description: Accepted
          content:
            application/json:
              schema:
                type: object
                properties:
                  accepted: { type: boolean }
                  comment_id: { type: string }
                  error_event_id: { type: string }
        "402":
          description: Billing delinquent
        "403":
          description: User feedback ingest disabled for project
        "404":
          description: No matching occurrence for this project
        "422":
          description: Validation error

  /api/projects/{project}/events:
    get:
      tags: [events-key]
      summary: List occurrences (API key)
      security: [{ apiKeyHeader: [] }, { apiKeyBearer: [] }, { apiKeyQuery: [] }]
      parameters:
        - $ref: "#/components/parameters/projectId"
        - in: query
          name: page
          schema: { type: integer, minimum: 1 }
        - in: query
          name: per_page
          schema: { type: integer, minimum: 1, maximum: 100 }
        - in: query
          name: level
          schema: { type: string }
        - in: query
          name: search
          schema: { type: string }
        - in: query
          name: fingerprint
          schema: { type: string }
        - in: query
          name: status
          schema: { type: string }
        - in: query
          name: from
          schema: { type: string }
        - in: query
          name: to
          schema: { type: string }
        - in: query
          name: environment
          schema: { type: string }
        - in: query
          name: release
          schema: { type: string }
        - in: query
          name: correlation_id
          schema: { type: string }
        - in: query
          name: parent_id
          schema: { $ref: "#/components/schemas/Ulid" }
        - in: query
          name: trace_id
          schema: { type: string }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/ErrorEventSummary" }
                  meta:
                    type: object
                    properties:
                      current_page: { type: integer }
                      total: { type: integer }
                      per_page: { type: integer }

  /api/projects/{project}/events/{event}:
    get:
      tags: [events-key]
      summary: Show occurrence (API key)
      security: [{ apiKeyHeader: [] }, { apiKeyBearer: [] }, { apiKeyQuery: [] }]
      parameters:
        - $ref: "#/components/parameters/projectId"
        - name: event
          in: path
          required: true
          schema: { $ref: "#/components/schemas/Ulid" }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/ErrorEventDetail"
        "403":
          description: Key does not match project

  /api/me:
    get:
      tags: [v1]
      summary: Legacy current user
      security: [{ bearerAuth: [] }]
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  id: { $ref: "#/components/schemas/Ulid" }
                  name: { type: string }
                  email: { type: string, format: email }

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: Sanctum API token from Profile → API tokens.
    apiKeyHeader:
      type: apiKey
      in: header
      name: X-Api-Key
    apiKeyBearer:
      type: http
      scheme: bearer
      description: Use the project API key as the bearer token value.
    apiKeyQuery:
      type: apiKey
      in: query
      name: api_key

  parameters:
    projectId:
      name: project
      in: path
      required: true
      schema: { $ref: "#/components/schemas/Ulid" }
    errorId:
      name: error
      in: path
      required: true
      schema: { $ref: "#/components/schemas/Ulid" }
    page:
      name: page
      in: query
      schema: { type: integer, minimum: 1, default: 1 }
    perPage:
      name: per_page
      in: query
      schema: { type: integer, minimum: 1, maximum: 100, default: 15 }
    pageNumber:
      name: "page[number]"
      in: query
      required: false
      schema: { type: integer, minimum: 1 }
    pageSize:
      name: "page[size]"
      in: query
      required: false
      schema: { type: integer, minimum: 1, maximum: 100 }

  schemas:
    Ulid:
      type: string
      minLength: 26
      maxLength: 26
      description: Time-ordered ULID (26-character Crockford base32).

    PaginationMeta:
      type: object
      properties:
        current_page: { type: integer }
        last_page: { type: integer }
        per_page: { type: integer }
        total: { type: integer }

    Me:
      type: object
      properties:
        id: { $ref: "#/components/schemas/Ulid" }
        name: { type: string }
        email: { type: string, format: email }
        organizations:
          type: array
          items:
            type: object
            properties:
              id: { $ref: "#/components/schemas/Ulid" }
              name: { type: string }
              slug: { type: string }
              role: { type: string }
        teams:
          type: array
          items:
            type: object
            properties:
              id: { $ref: "#/components/schemas/Ulid" }
              organization_id: { $ref: "#/components/schemas/Ulid" }
              name: { type: string }
              slug: { type: string }
              role:
                type: string
                enum: [viewer, member, lead]
                description: Role on that team (highest role wins when a project is linked to multiple teams).

    OrganizationRef:
      type: object
      properties:
        id: { $ref: "#/components/schemas/Ulid" }
        name: { type: string }
        slug: { type: string }

    ProjectListItem:
      type: object
      properties:
        id: { $ref: "#/components/schemas/Ulid" }
        organization_id: { $ref: "#/components/schemas/Ulid" }
        name: { type: string }
        slug: { type: string }
        tech_stacks:
          type: array
          items: { type: string }
        environment_labels:
          type: array
          items: { type: string }
        log_ingest_enabled:
          type: boolean
          description: When false, POST /api/ingest/log returns 403 for this project's API key.
        metrics_ingest_enabled:
          type: boolean
          description: When false, POST /api/ingest/metric returns 403 for this project's API key.
        user_feedback_ingest_enabled:
          type: boolean
          description: When false, POST /api/ingest/feedback returns 403 for this project's API key.
        performance_ingest_enabled:
          type: boolean
          description: When false, POST /api/ingest/trace returns 403 for this project's API key.
        created_at: { type: string, format: date-time, nullable: true }
        updated_at: { type: string, format: date-time, nullable: true }
        api_key_prefix: { type: string }
        organization:
          $ref: "#/components/schemas/OrganizationRef"

    ProjectCreated:
      allOf:
        - $ref: "#/components/schemas/ProjectListItem"
        - type: object
          properties:
            api_key: { type: string }

    GroupedError:
      type: object
      properties:
        id:
          allOf:
            - $ref: "#/components/schemas/Ulid"
          description: Representative occurrence ULID for this fingerprint (use with /errors/{id}/occurrences).
        fingerprint: { type: string }
        message: { type: string, nullable: true }
        level: { type: string, nullable: true }
        occurrence_count: { type: integer }
        first_seen_at: { type: string, format: date-time, nullable: true }
        last_seen_at: { type: string, format: date-time, nullable: true }
        status: { type: string }
        snoozed_until: { type: string, format: date-time, nullable: true }
        priority: { type: string, nullable: true }
        assigned_to_user_id:
          nullable: true
          allOf:
            - $ref: "#/components/schemas/Ulid"

    ErrorEventSummary:
      type: object
      properties:
        id: { $ref: "#/components/schemas/Ulid" }
        message: { type: string, nullable: true }
        level: { type: string, nullable: true }
        created_at: { type: string, format: date-time, nullable: true }
        url: { type: string, nullable: true }
        environment: { type: string, nullable: true }
        release: { type: string, nullable: true }
        exception_class: { type: string, nullable: true }
        file: { type: string, nullable: true }
        line: { type: integer, nullable: true }
        fingerprint: { type: string, nullable: true }
        correlation_id: { type: string, nullable: true }
        trace_id: { type: string, nullable: true }
        span_id: { type: string, nullable: true }
        parent_span_id: { type: string, nullable: true }
        transaction_name: { type: string, nullable: true }
        parent_id:
          nullable: true
          allOf:
            - $ref: "#/components/schemas/Ulid"
        status: { type: string, nullable: true }
        occurrence_uuid: { type: string, nullable: true }
        tracking_uuid: { type: string, nullable: true }
        entry_type: { type: string, nullable: true }
        job_class: { type: string, nullable: true }
        command_line: { type: string, nullable: true }
        handled: { type: boolean }
        language: { type: string, nullable: true }

    ErrorEventDetail:
      allOf:
        - $ref: "#/components/schemas/ErrorEventSummary"
        - type: object
          properties:
            server_name: { type: string, nullable: true }
            stack_trace: { type: string, nullable: true }
            stack_frames: { nullable: true }
            context: { nullable: true }
            breadcrumbs: { nullable: true }
            exception_chain: { nullable: true }
            source_map_url: { type: string, nullable: true }
            reliability_signals:
              type: array
              nullable: true
              description: Heuristic reliability hints derived at ingest from message/stack (not a full repo scan).
              items:
                type: object
                properties:
                  id: { type: string }
                  severity: { type: string, enum: [info, warning, alert] }
                  category: { type: string }
                  title: { type: string }
                  detail: { type: string }
