openapi: 3.1.0
info:
  title: Uprity Developer API
  version: 1.0.0
  description: >-
    Versioned customer API for explicitly granted Business and Agency resources.
    Credentials are checked against current workspace, plan, scope, resource,
    expiry, revocation, and optional IP policy on every request.
servers:
  - url: /api/v1
    description: Same-origin API
tags:
  - { name: Businesses, description: Granted business workspaces }
  - { name: Locations, description: Granted active locations }
  - { name: Reputation, description: Reviews, feedback, and request batches }
  - { name: Insights, description: Rank, visibility, and report results }
  - { name: Platform, description: Usage and webhook configuration }
security:
  - bearerAuth: []
components:
  securitySchemes:
    bearerAuth: { type: http, scheme: bearer, bearerFormat: Uprity workspace key }
  parameters:
    id: { name: id, in: path, required: true, schema: { type: string } }
    businessId: { name: businessId, in: query, schema: { type: string }, description: An ungranted ID returns an empty list. }
    cursor: { name: cursor, in: query, schema: { type: string } }
    limit: { name: limit, in: query, schema: { type: integer, minimum: 1, maximum: 200, default: 50 } }
    status: { name: status, in: query, schema: { type: string }, description: Resource-specific status filter. }
  responses:
    Unauthorized:
      description: Missing, invalid, expired, or revoked Bearer credential.
      content: { application/json: { schema: { $ref: '#/components/schemas/Error' }, example: { error: Bearer token required } } }
    Forbidden:
      description: Workspace, plan, trial, scope, resource grant, or allowed IP denied.
      content: { application/json: { schema: { $ref: '#/components/schemas/Error' }, example: { error: Missing required scope } } }
    NotFound:
      description: Resource absent or outside the credential grant.
      content: { application/json: { schema: { $ref: '#/components/schemas/Error' }, example: { error: Not found } } }
    Throttled:
      description: Minute or period allowance exhausted.
      headers: { Retry-After: { schema: { type: integer } } }
      content: { application/json: { schema: { $ref: '#/components/schemas/Error' }, example: { error: Rate limit exceeded, code: RATE_LIMITED } } }
  schemas:
    Error:
      type: object
      required: [error]
      properties:
        error: { type: string }
        code: { type: string }
        reason: { type: string }
        details: { type: array, items: { type: object, additionalProperties: true } }
    Pagination:
      type: object
      required: [hasMore, nextCursor]
      properties:
        hasMore: { type: boolean }
        nextCursor: { type: [string, 'null'] }
    Business:
      type: object
      required: [id, name, createdAt]
      properties:
        id: { type: string, example: biz_demo_001 }
        name: { type: string, example: Northstar Dental }
        category: { type: [string, 'null'] }
        city: { type: [string, 'null'] }
        state: { type: [string, 'null'] }
        countryCode: { type: [string, 'null'] }
        googleRating: { type: [number, 'null'] }
        googleReviewCount: { type: [integer, 'null'] }
        googlePlaceId: { type: [string, 'null'] }
        createdAt: { type: string, format: date-time }
    Location:
      type: object
      required: [id, businessId, name, isPrimary, createdAt]
      properties:
        id: { type: string }
        businessId: { type: string }
        name: { type: string }
        address: { type: [string, 'null'] }
        city: { type: [string, 'null'] }
        state: { type: [string, 'null'] }
        countryCode: { type: [string, 'null'] }
        googleRating: { type: [number, 'null'] }
        googleReviewCount: { type: [integer, 'null'] }
        isPrimary: { type: boolean }
        createdAt: { type: string, format: date-time }
    Review:
      type: object
      required: [id, businessId, platform, rating, createdAt]
      properties:
        id: { type: string }
        businessId: { type: string }
        platform: { type: string }
        rating: { type: number }
        authorName: { type: [string, 'null'], description: Customer personal data. }
        content: { type: [string, 'null'], description: Customer personal data. }
        aiResponse: { type: [string, 'null'] }
        sentiment: { type: [string, 'null'] }
        responseStatus: { type: [string, 'null'] }
        publishedAt: { type: [string, 'null'], format: date-time }
        createdAt: { type: string, format: date-time }
    Feedback:
      type: object
      required: [id, businessId, rating, status, createdAt]
      properties:
        id: { type: string }
        businessId: { type: string }
        rating: { type: number }
        customerName: { type: [string, 'null'], description: Customer personal data. }
        message: { type: [string, 'null'], description: Customer personal data. }
        status: { type: string }
        createdAt: { type: string, format: date-time }
    ReviewRequestBatch:
      type: object
      properties:
        id: { type: string }
        businessId: { type: string }
        name: { type: string }
        type: { type: string }
        status: { type: string }
        targetCount: { type: integer }
        sentCount: { type: integer }
        clickCount: { type: integer }
        reviewCount: { type: integer }
        scheduledAt: { type: [string, 'null'], format: date-time }
        completedAt: { type: [string, 'null'], format: date-time }
        createdAt: { type: string, format: date-time }
    RankScan:
      type: object
      properties:
        id: { type: string }
        businessId: { type: string }
        locationId: { type: [string, 'null'] }
        keyword: { type: string }
        gridSize: { type: integer }
        radiusKm: { type: number }
        gridResults: { type: [array, object, 'null'] }
        avgRank: { type: [number, 'null'] }
        top3Count: { type: integer }
        top10Count: { type: integer }
        areaDifficultyScore: { type: [number, 'null'] }
        createdAt: { type: string, format: date-time }
    VisibilityScan:
      type: object
      properties:
        id: { type: string }
        businessId: { type: string }
        scoreOverall: { type: [number, 'null'] }
        scoreByLlm: { type: [object, 'null'], additionalProperties: true }
        mentionRate: { type: [number, 'null'] }
        avgRank: { type: [number, 'null'] }
        sentiment: { type: [string, 'null'] }
        isPreview: { type: boolean }
        createdAt: { type: string, format: date-time }
    Report:
      type: object
      properties:
        id: { type: string }
        businessId: { type: string }
        schemaVersion: { type: integer }
        overallScore: { type: [number, 'null'] }
        publishedAt: { type: [string, 'null'], format: date-time }
        createdAt: { type: string, format: date-time }
    UsageBucket:
      type: object
      required: [dimension, windowStart, used]
      properties:
        dimension: { type: string, enum: [day, month, trial] }
        windowStart: { type: string, format: date-time }
        used: { type: integer }
    WebhookEvent: { type: string, enum: [REVIEW_CREATED, REVIEW_REPLIED, LOCAL_RANK_SCAN_COMPLETED] }
    Webhook: { type: object, additionalProperties: true, description: Public webhook representation. }
    Page:
      type: object
      required: [data, pagination]
      properties:
        data: { type: array, items: {} }
        pagination: { $ref: '#/components/schemas/Pagination' }
paths:
  /business:
    get:
      operationId: listBusinesses
      tags: [Businesses]
      summary: List granted active businesses
      x-uprity-scope: business:read
      responses:
        '200': { description: Granted businesses, content: { application/json: { schema: { type: object, properties: { data: { type: array, items: { $ref: '#/components/schemas/Business' } } } } } } }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '429': { $ref: '#/components/responses/Throttled' }
  /business/{id}:
    get:
      operationId: getBusiness
      tags: [Businesses]
      summary: Get a granted active business
      x-uprity-scope: business:read
      parameters: [{ $ref: '#/components/parameters/id' }]
      responses:
        '200': { description: Business, content: { application/json: { schema: { type: object, properties: { data: { $ref: '#/components/schemas/Business' } } } } } }
        '404': { $ref: '#/components/responses/NotFound' }
  /locations:
    get:
      operationId: listLocations
      tags: [Locations]
      summary: List granted active locations
      x-uprity-scope: locations:read
      parameters: [{ $ref: '#/components/parameters/businessId' }, { $ref: '#/components/parameters/limit' }, { $ref: '#/components/parameters/cursor' }]
      responses: { '200': { description: Location page, content: { application/json: { schema: { allOf: [{ $ref: '#/components/schemas/Page' }], properties: { data: { type: array, items: { $ref: '#/components/schemas/Location' } } } } } } } }
  /reviews:
    get:
      operationId: listReviews
      tags: [Reputation]
      summary: List granted reviews
      x-uprity-scope: reviews:read
      parameters: [{ $ref: '#/components/parameters/businessId' }, { $ref: '#/components/parameters/limit' }, { $ref: '#/components/parameters/cursor' }]
      responses: { '200': { description: Review page, content: { application/json: { schema: { allOf: [{ $ref: '#/components/schemas/Page' }], properties: { data: { type: array, items: { $ref: '#/components/schemas/Review' } } } } } } } }
  /reviews/{id}:
    get:
      operationId: getReview
      tags: [Reputation]
      summary: Get a granted review
      x-uprity-scope: reviews:read
      parameters: [{ $ref: '#/components/parameters/id' }]
      responses: { '200': { description: Review, content: { application/json: { schema: { type: object, properties: { data: { $ref: '#/components/schemas/Review' } } } } } }, '404': { $ref: '#/components/responses/NotFound' } }
  /feedback:
    get:
      operationId: listFeedback
      tags: [Reputation]
      summary: List private feedback
      x-uprity-scope: feedback:read
      parameters: [{ $ref: '#/components/parameters/businessId' }, { $ref: '#/components/parameters/status' }, { $ref: '#/components/parameters/limit' }, { $ref: '#/components/parameters/cursor' }]
      responses: { '200': { description: Feedback page, content: { application/json: { schema: { allOf: [{ $ref: '#/components/schemas/Page' }], properties: { data: { type: array, items: { $ref: '#/components/schemas/Feedback' } } } } } } } }
  /review-requests:
    get:
      operationId: listReviewRequests
      tags: [Reputation]
      summary: List review-request batch status
      description: This read operation does not send a request.
      x-uprity-scope: review_requests:read
      parameters: [{ $ref: '#/components/parameters/businessId' }, { $ref: '#/components/parameters/status' }, { $ref: '#/components/parameters/limit' }, { $ref: '#/components/parameters/cursor' }]
      responses: { '200': { description: Review-request page, content: { application/json: { schema: { allOf: [{ $ref: '#/components/schemas/Page' }], properties: { data: { type: array, items: { $ref: '#/components/schemas/ReviewRequestBatch' } } } } } } } }
  /rank-scans:
    get:
      operationId: listRankScans
      tags: [Insights]
      summary: List completed rank scans
      description: This read operation does not launch a scan.
      x-uprity-scope: rank:read
      parameters: [{ $ref: '#/components/parameters/businessId' }, { $ref: '#/components/parameters/limit' }, { $ref: '#/components/parameters/cursor' }]
      responses: { '200': { description: Rank page, content: { application/json: { schema: { allOf: [{ $ref: '#/components/schemas/Page' }], properties: { data: { type: array, items: { $ref: '#/components/schemas/RankScan' } } } } } } } }
  /visibility:
    get:
      operationId: listVisibility
      tags: [Insights]
      summary: List completed AI visibility summaries
      x-uprity-scope: visibility:read
      parameters: [{ $ref: '#/components/parameters/businessId' }, { $ref: '#/components/parameters/limit' }, { $ref: '#/components/parameters/cursor' }]
      responses: { '200': { description: Visibility page, content: { application/json: { schema: { allOf: [{ $ref: '#/components/schemas/Page' }], properties: { data: { type: array, items: { $ref: '#/components/schemas/VisibilityScan' } } } } } } } }
  /reports:
    get:
      operationId: listReports
      tags: [Insights]
      summary: List published internal report metadata
      x-uprity-scope: reports:read
      parameters: [{ $ref: '#/components/parameters/businessId' }, { $ref: '#/components/parameters/limit' }, { $ref: '#/components/parameters/cursor' }]
      responses: { '200': { description: Report page, content: { application/json: { schema: { allOf: [{ $ref: '#/components/schemas/Page' }], properties: { data: { type: array, items: { $ref: '#/components/schemas/Report' } } } } } } } }
  /reports/{id}/export-link:
    post:
      operationId: createReportExportLink
      tags: [Insights]
      summary: Issue a five-minute credential-bound report link
      description: Not enabled in the guided explorer. Current grants are rechecked on download.
      x-uprity-scope: reports:read
      parameters: [{ $ref: '#/components/parameters/id' }]
      responses:
        '200': { description: Short-lived link, content: { application/json: { schema: { type: object, required: [downloadUrl, expiresAt], properties: { downloadUrl: { type: string }, expiresAt: { type: string, format: date-time } } } } } }
        '404': { $ref: '#/components/responses/NotFound' }
  /reports/{id}/download:
    get:
      operationId: downloadReport
      tags: [Insights]
      summary: Download a completed report PDF
      description: Not enabled in the guided explorer. Requires the same Bearer credential and an unexpired token.
      x-uprity-scope: reports:read
      parameters: [{ $ref: '#/components/parameters/id' }, { name: token, in: query, required: true, schema: { type: string } }]
      responses:
        '200': { description: PDF bytes, content: { application/pdf: { schema: { type: string, contentEncoding: binary } } } }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
  /usage:
    get:
      operationId: getUsage
      tags: [Platform]
      summary: Get recent API usage counters
      x-uprity-scope: usage:read
      responses: { '200': { description: Usage counters, content: { application/json: { schema: { type: object, properties: { data: { type: array, items: { $ref: '#/components/schemas/UsageBucket' } } } } } } } }
  /webhooks:
    get:
      operationId: listWebhooks
      tags: [Platform]
      summary: List granted webhook endpoints
      x-uprity-scope: webhooks:read
      responses: { '200': { description: Webhooks, content: { application/json: { schema: { type: object, properties: { data: { type: array, items: { $ref: '#/components/schemas/Webhook' } }, availableEvents: { type: array, items: { $ref: '#/components/schemas/WebhookEvent' } } } } } } } }
    post:
      operationId: createWebhook
      tags: [Platform]
      summary: Create a webhook endpoint
      description: Has external effects and is not enabled in the guided explorer.
      x-uprity-scope: webhooks:write
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [name, url, events, businessIds]
              properties:
                name: { type: string, minLength: 1, maxLength: 100 }
                url: { type: string, format: uri }
                events: { type: array, minItems: 1, items: { $ref: '#/components/schemas/WebhookEvent' } }
                businessIds: { type: array, minItems: 1, items: { type: string } }
      responses: { '201': { description: Webhook and one-time signing secret, content: { application/json: { schema: { type: object, properties: { data: { $ref: '#/components/schemas/Webhook' }, secret: { type: string, writeOnly: true } } } } } } }
  /webhooks/{id}:
    get:
      operationId: getWebhook
      tags: [Platform]
      summary: Get a granted webhook endpoint
      x-uprity-scope: webhooks:read
      parameters: [{ $ref: '#/components/parameters/id' }]
      responses: { '200': { description: Webhook, content: { application/json: { schema: { type: object, properties: { data: { $ref: '#/components/schemas/Webhook' } } } } } }, '404': { $ref: '#/components/responses/NotFound' } }
    patch:
      operationId: updateWebhook
      tags: [Platform]
      summary: Update a webhook endpoint
      description: Has external effects and is not enabled in the guided explorer.
      x-uprity-scope: webhooks:write
      parameters: [{ $ref: '#/components/parameters/id' }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              minProperties: 1
              properties:
                name: { type: string, minLength: 1, maxLength: 100 }
                url: { type: string, format: uri }
                events: { type: array, minItems: 1, items: { $ref: '#/components/schemas/WebhookEvent' } }
                isActive: { type: boolean }
      responses: { '200': { description: Updated webhook, content: { application/json: { schema: { type: object, properties: { data: { $ref: '#/components/schemas/Webhook' } } } } } } }
    delete:
      operationId: deleteWebhook
      tags: [Platform]
      summary: Delete a webhook endpoint
      description: Destructive and not enabled in the guided explorer.
      x-uprity-scope: webhooks:write
      parameters: [{ $ref: '#/components/parameters/id' }]
      responses: { '200': { description: Deletion result, content: { application/json: { schema: { type: object, required: [deleted], properties: { deleted: { type: boolean } } } } } } }
