openapi: 3.0.3
info:
  title: Rivioo API
  version: "1.0"
  description: >
    App Store and Google Play reviews as JSON. Cache-first: recently requested
    apps return instantly; new apps are fetched live and polled as a job.
  contact:
    email: admin@rivioo.app
servers:
  - url: https://api.rivioo.app/v1
  - url: https://rivioo-api.up.railway.app/api/v1
security:
  - bearerAuth: []
paths:
  /apps/search:
    get:
      summary: Search for an app
      description: Find an app's canonical id. Never counts against your monthly quota.
      parameters:
        - name: q
          in: query
          required: true
          schema: { type: string, minLength: 2 }
        - name: store
          in: query
          schema: { type: string, enum: [ios, android] }
        - name: country
          in: query
          schema: { type: string, default: us }
      responses:
        "200":
          description: Matching apps with canonical ids
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/AppResult" }
                  meta:
                    type: object
                    properties:
                      count: { type: integer }
        "400": { $ref: "#/components/responses/Error" }
        "401": { $ref: "#/components/responses/Error" }
        "429": { $ref: "#/components/responses/Error" }
  /reviews/fetch:
    post:
      summary: Fetch reviews for an app
      description: >
        Cache hit returns HTTP 200 with the first page of reviews. Cache miss
        or stale data starts a live fetch and returns HTTP 202 with a job to poll.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [store, app_id]
              properties:
                store: { type: string, enum: [ios, android] }
                app_id:
                  type: string
                  description: Android package name or iOS numeric track id
                country: { type: string, default: us }
                limit:
                  type: integer
                  description: Total reviews wanted, clamped to the plan's per-request max
                freshness_days: { type: integer, minimum: 1, maximum: 30, default: 7 }
                allow_stale:
                  type: boolean
                  default: false
                  description: Serve older cached data instantly instead of refreshing
      responses:
        "200":
          description: Cache hit — first page of reviews
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ReviewPage" }
        "202":
          description: Live fetch started — poll the job
          content:
            application/json:
              schema:
                type: object
                properties:
                  job:
                    type: object
                    properties:
                      id: { type: string, format: uuid }
                      status: { type: string, enum: [queued] }
                  meta:
                    type: object
                    properties:
                      source: { type: string }
                      poll: { type: string }
                      estimated_seconds: { type: integer }
        "400": { $ref: "#/components/responses/Error" }
        "401": { $ref: "#/components/responses/Error" }
        "429": { $ref: "#/components/responses/Error" }
  /jobs/{id}:
    get:
      summary: Check a job's status
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string, format: uuid }
      responses:
        "200":
          description: Job status
          content:
            application/json:
              schema:
                type: object
                properties:
                  job: { $ref: "#/components/schemas/Job" }
        "404": { $ref: "#/components/responses/Error" }
  /jobs/{id}/reviews:
    get:
      summary: Get a completed job's reviews
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string, format: uuid }
        - name: page_size
          in: query
          schema: { type: integer, minimum: 1, maximum: 500, default: 100 }
      responses:
        "200":
          description: First page of the job's reviews
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ReviewPage" }
        "400": { $ref: "#/components/responses/Error" }
        "404": { $ref: "#/components/responses/Error" }
  /reviews:
    get:
      summary: Page through reviews
      description: Follow meta.next_cursor from any page until it is null.
      parameters:
        - name: cursor
          in: query
          required: true
          schema: { type: string }
        - name: page_size
          in: query
          schema: { type: integer, minimum: 1, maximum: 500, default: 100 }
      responses:
        "200":
          description: A page of reviews
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ReviewPage" }
        "400": { $ref: "#/components/responses/Error" }
        "429": { $ref: "#/components/responses/Error" }
  /usage:
    get:
      summary: Check your usage
      description: Plan, quota used and remaining, reset date, and today's refresh count. Never counts against your monthly quota.
      responses:
        "200":
          description: Current usage
          content:
            application/json:
              schema:
                type: object
                properties:
                  plan: { type: string }
                  period: { type: string, example: "2026-07" }
                  quota:
                    type: object
                    properties:
                      monthly_reviews: { type: integer }
                      used: { type: integer }
                      remaining: { type: integer }
                      resets_at: { type: string, format: date-time }
                  requests_this_month: { type: integer }
                  scrape_jobs_today: { type: integer }
                  scrape_jobs_daily_cap: { type: integer }
                  max_reviews_per_request: { type: integer }
                  rate_limit_rpm: { type: integer }
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: "API key: rk_live_..."
  responses:
    Error:
      description: Error envelope
      content:
        application/json:
          schema:
            type: object
            properties:
              error:
                type: object
                properties:
                  code:
                    type: string
                    enum: [UNAUTHORIZED, KEY_REVOKED, CLIENT_SUSPENDED, VALIDATION_ERROR, NOT_FOUND, RATE_LIMITED, QUOTA_EXCEEDED, SCRAPE_CAP_REACHED, INTERNAL]
                  message: { type: string }
                  status: { type: integer }
  schemas:
    AppResult:
      type: object
      properties:
        name: { type: string }
        developer: { type: string }
        icon: { type: string }
        rating: { type: number }
        stores:
          type: array
          items: { type: string }
        app_id:
          type: object
          properties:
            android: { type: string }
            ios: { type: string }
    Review:
      type: object
      properties:
        id: { type: string }
        store: { type: string, enum: [ios, android] }
        app_id: { type: string }
        country: { type: string, nullable: true }
        lang: { type: string, nullable: true }
        rating: { type: integer, nullable: true }
        title: { type: string, nullable: true }
        body: { type: string, nullable: true }
        reviewer: { type: string, nullable: true }
        app_version: { type: string, nullable: true }
        developer_reply: { type: string, nullable: true }
        developer_reply_at: { type: string, format: date-time, nullable: true }
        url: { type: string, nullable: true }
        posted_at: { type: string, format: date-time, nullable: true }
    ReviewPage:
      type: object
      properties:
        data:
          type: array
          items: { $ref: "#/components/schemas/Review" }
        meta:
          type: object
          properties:
            delivered: { type: integer }
            next_cursor: { type: string, nullable: true }
            quota_remaining: { type: integer }
    Job:
      type: object
      properties:
        id: { type: string, format: uuid }
        status: { type: string, enum: [queued, running, completed, failed] }
        progress: { type: integer }
        review_count: { type: integer, nullable: true }
        created_at: { type: string, format: date-time }
        completed_at: { type: string, format: date-time, nullable: true }
        error: { type: string, nullable: true }
        reviews_url: { type: string }
