openapi: 3.1.0
info:
  title: Locksteps Connector Analytics ingest API
  version: "1.0.0"
  summary: Record a scrubbed phrase from a connector, plugin, skill, or MCP tool call.
  description: |
    POST one JSON event per tool call to https://www.locksteps.ai/v1/analytics/ingest.
    Authenticate with a bearer API key issued in the Locksteps setup flow.
    phrase is the preferred field. user_intent and query are aliases.
    Invalid optional fields are dropped. They do not reject the event.
    session_id is conversation-scoped; hashed at rest. FREE conversion endpoint at /v1/analytics/conversion.
    The API does not store IP addresses or user agents.
  contact:
    name: Locksteps
    email: hi@locksteps.ai
    url: https://www.locksteps.ai/docs/api
  license:
    name: Proprietary
servers:
  - url: https://www.locksteps.ai
paths:
  /v1/analytics/ingest:
    get:
      operationId: describeIngest
      summary: Describe the ingest endpoint
      responses:
        "200":
          description: A short JSON description of the endpoint.
          content:
            application/json:
              schema:
                type: object
    options:
      operationId: ingestPreflight
      summary: CORS preflight
      responses:
        "204":
          description: Preflight accepted.
    post:
      operationId: ingestEvent
      summary: Record one connector event
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/IngestEvent"
      responses:
        "200":
          description: Stored, or recognized as a duplicate.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IngestSuccess"
        "400":
          description: The JSON body or a required field is invalid.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorBody"
        "401":
          description: Missing, malformed, unknown, or mismatched API key.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorBody"
        "413":
          description: The JSON body is larger than 8 KB.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorBody"
        "429":
          description: |
            The key, organization, or platform is over its limit.
            Each key can burst up to 600 events, then about 2 events per second.
            An organization can store 100,000 counted events per UTC day
            (10,000 during its first 7 days). Wait for Retry-After seconds before sending again.
          headers:
            Retry-After:
              description: Seconds to wait before the next request.
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorBody"
        "503":
          description: The analytics store is not configured or not reachable.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorBody"
        "500":
          description: Unexpected server error.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorBody"
  /v1/analytics/conversion:
    get:
      operationId: describeConversion
      summary: Describe the FREE conversion endpoint
      responses:
        "200":
          description: A short JSON description of the endpoint.
    options:
      operationId: conversionPreflight
      summary: CORS preflight
      responses:
        "204":
          description: Preflight accepted.
    post:
      operationId: ingestConversion
      summary: Record a FREE in-plugin conversion
      description: |
        Reuse session_id from ingest. Stitches to the latest phrase event for that session.
        FREE. Same API key as ingest.
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ConversionEvent"
      responses:
        "200":
          description: Stored, or recognized as a duplicate.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ConversionSuccess"
        "400":
          description: Invalid body.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorBody"
        "401":
          description: Unauthorized.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorBody"
        "503":
          description: Store not configured.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorBody"
  /v1/analytics/health:
    get:
      operationId: analyticsHealth
      summary: Report whether the analytics store is reachable
      responses:
        "200":
          description: Health body. ok is false when the store is not configured.
          content:
            application/json:
              schema:
                type: object
                properties:
                  service:
                    type: string
                  endpoint:
                    type: string
                  ok:
                    type: boolean
                  schema_reachable:
                    type: boolean
                  rpc_reachable:
                    type: boolean
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
  schemas:
    IngestEvent:
      type: object
      required: [connector_id, tool_name, ts, event_id]
      properties:
        connector_id:
          type: string
          minLength: 3
          maxLength: 48
          pattern: "^[a-z0-9]+(?:-[a-z0-9]+)*$"
        phrase:
          type: string
          maxLength: 2000
          description: Preferred name. user_intent and query are aliases. One of the three is required.
        user_intent:
          type: string
          maxLength: 2000
        query:
          type: string
          maxLength: 2000
        tool_name:
          type: string
          maxLength: 64
        ts:
          type: string
          format: date-time
        event_id:
          type: string
          maxLength: 80
        platform:
          type: string
          enum: [chatgpt, muse, claude, grok, gemini, copilot, cursor, perplexity, mcp, api, other]
        outcome:
          type: string
          enum: [success, no_result, error]
        latency_ms:
          type: integer
          minimum: 0
          maximum: 600000
        result_count:
          type: integer
          minimum: 0
          maximum: 1000000
        country:
          type: string
          minLength: 2
          maxLength: 2
          description: ISO 3166-1 alpha-2.
        session_id:
          type: string
          maxLength: 128
          description: Conversation-scoped opaque id. Reuse in a chat. Server mints+returns if omitted. Hashed at rest.
        connector_version:
          type: string
          maxLength: 32
        is_test:
          type: boolean
        sample_rate:
          type: number
          exclusiveMinimum: 0
          maximum: 1
          description: |
            Fraction of events the caller kept. The dashboard scales counts by 1/sample_rate.
            Omit it when every event is sent. Invalid values are dropped.
    IngestSuccess:
      type: object
      properties:
        ok:
          type: boolean
        stored:
          type: boolean
        junk:
          type: boolean
        is_test:
          type: boolean
        counted:
          type: boolean
        session_id:
          type: string
          description: Conversation-scoped session id used for this event (echo or minted).
        duplicate:
          type: string
          enum: [event_id, phrase_minute]
    ConversionEvent:
      type: object
      required: [connector_id, session_id, event_name]
      properties:
        connector_id:
          type: string
        session_id:
          type: string
          maxLength: 128
        event_name:
          type: string
          maxLength: 64
          description: e.g. lead_submitted, purchase_completed
        event_id:
          type: string
          maxLength: 80
          description: Idempotency key. Optional.
        value:
          type: number
        currency:
          type: string
          minLength: 3
          maxLength: 3
        metadata:
          type: object
          description: No PII. Max ~4KB.
        ts:
          type: string
          format: date-time
    ConversionSuccess:
      type: object
      properties:
        ok:
          type: boolean
        stored:
          type: boolean
        event_id:
          type: string
        session_id:
          type: string
        stitched:
          type: boolean
        source_phrase:
          type: string
          nullable: true
        duplicate:
          type: string
          enum: [event_id]
    ErrorBody:
      type: object
      properties:
        error:
          type: object
          properties:
            code:
              type: string
              enum: [unauthorized, invalid_request, rate_limited, not_configured, internal_error]
            message:
              type: string
            retry_after_seconds:
              type: integer
