openapi: 3.1.0
info:
  title: Local Rank Guru Public API
  version: 1.0.0
  description: Public scoped-token API for Local Rank Guru account, business, keyword, scan, report, and share-link integrations.
servers:
  - url: https://www.localrank.guru/api/v1
security:
  - bearerAuth: []
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
  schemas:
    ErrorEnvelope:
      type: object
      properties:
        error:
          type: object
          properties:
            code:
              type: string
            message:
              type: string
            details:
              type: object
    Business:
      type: object
      properties:
        id: { type: integer }
        name: { type: string }
        address: { type: string }
        latitude: { type: number }
        longitude: { type: number }
        place_id: { type: string, nullable: true }
    Keyword:
      type: object
      properties:
        id: { type: integer }
        business_id: { type: integer }
        keyword: { type: string }
    ScanTemplate:
      type: object
      properties:
        id: { type: integer }
        business_id: { type: integer }
        name: { type: string }
        keyword:
          type: object
          nullable: true
          properties:
            id: { type: integer }
            keyword: { type: string }
        grid_size: { type: string }
        point_distance: { type: number }
        distance_unit: { type: string, enum: [km, mi] }
        grid_shape: { type: string }
        grid_shape_payload:
          type: object
          nullable: true
    Scan:
      type: object
      properties:
        id: { type: integer }
        business_id: { type: integer }
        keyword: { type: string, nullable: true }
        status: { type: string }
        progress:
          type: object
paths:
  /account:
    get:
      tags: [Account]
      operationId: getAccount
      summary: Get account summary
      responses:
        '200':
          description: Account summary
        '401':
          description: Authentication required
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorEnvelope' }
  /businesses:
    get:
      tags: [Businesses]
      operationId: listBusinesses
      summary: List owned businesses
      parameters:
        - in: query
          name: limit
          schema: { type: integer, minimum: 1, maximum: 100 }
        - in: query
          name: page_cursor
          schema: { type: string }
      responses:
        '200':
          description: Business list
    post:
      tags: [Businesses]
      operationId: createBusiness
      summary: Create a business
      parameters:
        - in: header
          name: Idempotency-Key
          schema: { type: string }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                place_id: { type: string }
                name: { type: string }
                address: { type: string }
                latitude: { type: number }
                longitude: { type: number }
      responses:
        '201':
          description: Created business
  /businesses/{business}:
    get:
      tags: [Businesses]
      operationId: getBusiness
      summary: Get a business
      parameters:
        - in: path
          name: business
          required: true
          schema: { type: integer }
      responses:
        '200':
          description: Business
  /businesses/{business}/keywords:
    get:
      tags: [Keywords]
      operationId: listBusinessKeywords
      summary: List business keywords
      parameters:
        - in: path
          name: business
          required: true
          schema: { type: integer }
      responses:
        '200':
          description: Keyword list
    post:
      tags: [Keywords]
      operationId: createBusinessKeywords
      summary: Add keywords to a business
      parameters:
        - in: path
          name: business
          required: true
          schema: { type: integer }
        - in: header
          name: Idempotency-Key
          schema: { type: string }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [keywords]
              properties:
                keywords:
                  type: array
                  items: { type: string }
      responses:
        '201':
          description: Keywords created
  /scan-templates:
    get:
      tags: [Scan Templates]
      operationId: listScanTemplates
      summary: List scan templates
      parameters:
        - in: query
          name: business_id
          schema: { type: integer }
        - in: query
          name: limit
          schema: { type: integer, minimum: 1, maximum: 100 }
        - in: query
          name: page_cursor
          schema: { type: string }
      responses:
        '200':
          description: Scan template list
    post:
      tags: [Scan Templates]
      operationId: createScanTemplates
      summary: Create scan templates
      parameters:
        - in: header
          name: Idempotency-Key
          schema: { type: string }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [business_id, keywords, point_distance, distance_unit]
              properties:
                business_id: { type: integer }
                keywords:
                  type: array
                  minItems: 1
                  maxItems: 100
                  items: { type: string }
                grid_size: { type: string, default: 7x7 }
                point_distance: { type: number, minimum: 0.1, maximum: 50 }
                distance_unit: { type: string, enum: [km, mi] }
                grid_shape: { type: string, enum: [square, rings, custom_polygon] }
                ring_count: { type: integer, minimum: 1, maximum: 5 }
                grid_shape_payload: { type: object }
                center:
                  type: object
                  properties:
                    latitude: { type: number, minimum: -90, maximum: 90 }
                    longitude: { type: number, minimum: -180, maximum: 180 }
      responses:
        '201':
          description: Scan templates created
  /scan-templates/{scan_template}:
    get:
      tags: [Scan Templates]
      operationId: getScanTemplate
      summary: Get scan template metadata
      parameters:
        - in: path
          name: scan_template
          required: true
          schema: { type: integer }
      responses:
        '200':
          description: Scan template
  /scans/preview:
    post:
      tags: [Scans]
      operationId: previewScan
      summary: Preview scan cost and grid size
      responses:
        '200':
          description: Scan preview
  /scans:
    get:
      tags: [Scans]
      operationId: listScans
      summary: List scans
      responses:
        '200':
          description: Scan list
    post:
      tags: [Scans]
      operationId: createScan
      summary: Create credit-backed scans
      parameters:
        - in: header
          name: Idempotency-Key
          schema: { type: string }
      responses:
        '202':
          description: Scans queued
  /scans/{scan}:
    get:
      tags: [Scans]
      operationId: getScan
      summary: Get scan metadata
      parameters:
        - in: path
          name: scan
          required: true
          schema: { type: integer }
      responses:
        '200':
          description: Scan
  /scans/{scan}/cancel:
    post:
      tags: [Scans]
      operationId: cancelScan
      summary: Cancel a non-terminal scan
      parameters:
        - in: path
          name: scan
          required: true
          schema: { type: integer }
      responses:
        '200':
          description: Cancelled scan
  /scans/{scan}/grid-points:
    get:
      tags: [Reports]
      operationId: getScanGridPoints
      summary: Get scan grid points
      parameters:
        - in: path
          name: scan
          required: true
          schema: { type: integer }
      responses:
        '200':
          description: Grid points
  /scans/{scan}/competitors:
    get:
      tags: [Reports]
      operationId: getScanCompetitors
      summary: Get competitor summary
      parameters:
        - in: path
          name: scan
          required: true
          schema: { type: integer }
      responses:
        '200':
          description: Competitor summary
  /reports/scans/{scan}:
    get:
      tags: [Reports]
      operationId: getScanReport
      summary: Get complete scan report payload
      parameters:
        - in: path
          name: scan
          required: true
          schema: { type: integer }
      responses:
        '200':
          description: Scan report
  /scans/{scan}/share-link:
    post:
      tags: [Share Links]
      operationId: createScanShareLink
      summary: Create or return a public scan share link
      parameters:
        - in: path
          name: scan
          required: true
          schema: { type: integer }
        - in: header
          name: Idempotency-Key
          schema: { type: string }
      responses:
        '201':
          description: Share link
