openapi: 3.0.3
info:
  title: OptSens REST API
  version: "1.0"
  description: |
    Machine-to-machine access to consent data, domain information and
    cookie declarations.

    Available on **Business** and **Custom** plans. Authenticate every
    request with your REST API secret. See Authentication.
  contact:
    name: OptSens support
    email: support@optsens.com
servers:
  - url: https://api.optsens.com
    description: Production
tags:
  - name: Consents
    description: Consent records, statistics, exports, proof and erasure.
  - name: Domain
    description: Domain information and the cookie declaration.
security:
  - ApiKeyAuth: []
  - BearerAuth: []
paths:
  /api/v1/consent-status:
    get:
      operationId: getConsentStatus
      summary: Get consent status
      description: |
        Returns the current valid consent for a specific visitor.
        Responds with `null` when the visitor has no valid consent.
        Cached for 30 seconds per visitor.
      tags: [Consents]
      parameters:
        - name: visitor_id
          in: query
          required: true
          description: The visitor's unique identifier.
          schema:
            type: string
            format: uuid
            example: a1b2c3d4-e5f6-7890-abcd-ef1234567890
      responses:
        "200":
          description: The consent record, or `null` when none exists.
          content:
            application/json:
              schema:
                type: object
                properties:
                  consent:
                    nullable: true
                    allOf:
                      - $ref: "#/components/schemas/Consent"
                      - type: object
                        properties:
                          updated_at:
                            type: string
                            format: date-time
              examples:
                found:
                  summary: Consent found
                  value:
                    consent:
                      visitor_id: a1b2c3d4-e5f6-7890-abcd-ef1234567890
                      categories:
                        necessary: true
                        functional: true
                        analytics: true
                        advertising: false
                        performance: true
                      action: save_choices
                      consented_at: "2026-04-01T14:30:00.000Z"
                      valid_from: "2026-04-01T14:30:00.000Z"
                      expires_at: "2027-04-01T14:30:00.000Z"
                      updated_at: "2026-04-01T14:30:00.000Z"
                none:
                  summary: No consent
                  value:
                    consent: null
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"

  /api/v1/consents:
    get:
      operationId: listConsents
      summary: List consents
      description: |
        Returns a paginated list of all consent records for the domain.
        Includes expired records (the GDPR Article 7(1) audit trail).
        PII fields are never included.
      tags: [Consents]
      parameters:
        - name: page
          in: query
          required: false
          description: Page number.
          schema:
            type: integer
            default: 1
            minimum: 1
        - name: limit
          in: query
          required: false
          description: Records per page.
          schema:
            type: integer
            default: 50
            minimum: 1
            maximum: 100
      responses:
        "200":
          description: One page of consent records.
          content:
            application/json:
              schema:
                type: object
                properties:
                  consents:
                    type: array
                    items:
                      $ref: "#/components/schemas/Consent"
                  total:
                    type: integer
                    example: 1543
                  page:
                    type: integer
                    example: 1
                  pages:
                    type: integer
                    example: 78
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"

  /api/v1/domain:
    get:
      operationId: getDomain
      summary: Get domain info
      description: |
        Returns basic information about the authenticated domain.
        Never exposes the public CDN key, user IDs or internal IDs.
        Cached for 5 minutes.
      tags: [Domain]
      responses:
        "200":
          description: Domain information.
          content:
            application/json:
              schema:
                type: object
                properties:
                  domain:
                    $ref: "#/components/schemas/Domain"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          description: Domain not found.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              example:
                error: Domain not found
        "429":
          $ref: "#/components/responses/RateLimited"

  /api/v1/cookies:
    get:
      operationId: listCookies
      summary: List cookies
      description: |
        Returns the categorized cookie list from the latest declaration.
        The declaration is the most recent scan result. Cached for 5
        minutes. Declarations only change on scan.
      tags: [Domain]
      responses:
        "200":
          description: The latest cookie declaration.
          content:
            application/json:
              schema:
                type: object
                properties:
                  declaration:
                    nullable: true
                    allOf:
                      - $ref: "#/components/schemas/DeclarationMeta"
                    description: >-
                      `null` when no scan has produced a declaration yet.
                  cookies:
                    type: array
                    items:
                      $ref: "#/components/schemas/CookieCategory"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"

  /api/v1/stats:
    get:
      operationId: getStats
      summary: Consent statistics
      description: |
        Returns aggregated consent analytics for the domain.
        Counts and percentages only, never raw records or PII. Requires
        the consent analytics feature (Business and Custom plans).
        Results are capped at 10,000 records, and when the cap is hit
        the response includes `"sampled": true`. Cached for 5 minutes.
        Rate limit: 10 requests per minute per domain.
      tags: [Consents]
      parameters:
        - name: period
          in: query
          required: false
          description: Aggregation period.
          schema:
            type: string
            enum: [7d, 30d, 90d]
            default: 30d
      responses:
        "200":
          description: Aggregated consent statistics.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Stats"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/PlanRequired"
        "429":
          $ref: "#/components/responses/RateLimited"

  /api/v1/consents/export:
    get:
      operationId: exportConsents
      summary: Export consents
      description: |
        Exports consent records as a JSON or CSV file download.
        Maximum 50,000 records per export. PII is stripped from all rows
        and CSV values are protected against spreadsheet formula
        injection. Every export is audit-logged. Requires the consent
        analytics feature.
        Rate limit: 5 exports per hour per domain.
      tags: [Consents]
      parameters:
        - name: format
          in: query
          required: false
          description: Export format.
          schema:
            type: string
            enum: [json, csv]
            default: json
        - name: period
          in: query
          required: false
          description: Time period.
          schema:
            type: string
            enum: [7d, 30d, 90d]
            default: 30d
      responses:
        "200":
          description: The export file.
          content:
            application/json:
              schema:
                type: object
                properties:
                  consents:
                    type: array
                    items:
                      $ref: "#/components/schemas/ExportRow"
                  total:
                    type: integer
                    example: 1543
                  period:
                    type: string
                    example: 30d
                  exported_at:
                    type: string
                    format: date-time
                  truncated:
                    type: boolean
                    description: >-
                      Present and `true` only when the export stopped at
                      the 50,000-record cap.
            text/csv:
              schema:
                type: string
                description: CSV file with one row per consent record.
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/PlanRequired"
        "429":
          $ref: "#/components/responses/RateLimited"

  /api/v1/consent-proof/{visitorId}:
    get:
      operationId: getConsentProof
      summary: Consent proof PDF
      description: |
        Generates a tamper-evident PDF receipt proving a visitor's consent.
        Required by GDPR Article 7(1). The PDF contains the receipt ID,
        domain, visitor ID, dates, action, the category table and a
        HMAC-SHA256 signature. The IP address appears only in masked form,
        and the PDF never contains client details or internal IDs.
        Rate limit: 20 requests per minute per domain.
      tags: [Consents]
      parameters:
        - name: visitorId
          in: path
          required: true
          description: The visitor's unique identifier.
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: The consent proof PDF.
          content:
            application/pdf:
              schema:
                type: string
                format: binary
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          description: No consent record exists for this visitor.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "429":
          $ref: "#/components/responses/RateLimited"

  /api/v1/consent/{visitorId}:
    delete:
      operationId: deleteConsent
      summary: Delete consent
      description: |
        Deletes all consent records for a visitor.
        This is the GDPR right to erasure. The action is audit-logged
        and the visitor's cached consent status is invalidated. Rate
        limit: 10 deletions per minute per API key.
      tags: [Consents]
      parameters:
        - name: visitorId
          in: path
          required: true
          description: The visitor's unique identifier.
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Deletion result.
          content:
            application/json:
              schema:
                type: object
                properties:
                  deleted:
                    type: integer
                    description: Number of records deleted.
                    example: 3
                  visitor_id:
                    type: string
                    format: uuid
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"

components:
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-Api-Key
      description: Your REST API secret, generated under API & Webhooks > REST API in the dashboard.
    BearerAuth:
      type: http
      scheme: bearer
      description: The same REST API secret sent as a bearer token.
  responses:
    PlanRequired:
      description: The domain's plan does not include this feature.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            error: Consent analytics requires a Business or Custom plan.
    Unauthorized:
      description: |
        Invalid API key. The response is identical for a missing,
        invalid or inactive key.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            error: Invalid API key
    RateLimited:
      description: Rate limit exceeded for this key, IP or endpoint.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            error: Too many requests
  schemas:
    Error:
      type: object
      properties:
        error:
          type: string
    ConsentCategories:
      type: object
      description: Consent decision per category.
      properties:
        necessary:
          type: boolean
        functional:
          type: boolean
        analytics:
          type: boolean
        advertising:
          type: boolean
        performance:
          type: boolean
    Consent:
      type: object
      properties:
        visitor_id:
          type: string
          format: uuid
        categories:
          $ref: "#/components/schemas/ConsentCategories"
        action:
          type: string
          description: >-
            How the consent was given. Common values: `accept_all`,
            `reject_all`, `save_choices`, `gpc_auto`, `dismiss`. Legacy
            records without a stored action return `unknown`.
          example: save_choices
        consented_at:
          type: string
          format: date-time
        valid_from:
          type: string
          format: date-time
        expires_at:
          type: string
          format: date-time
    Domain:
      type: object
      properties:
        name:
          type: string
          example: example.com
        uri:
          type: string
          example: https://example.com
        plan:
          type: string
          example: Business
        subscription_active:
          type: boolean
        last_scan:
          type: string
          format: date-time
        check_status:
          type: string
          example: active
        created_at:
          type: string
          format: date-time
    DeclarationMeta:
      type: object
      properties:
        scanned_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
        cookie_count:
          type: integer
          example: 24
        page_count:
          type: integer
          example: 12
    CookieCategory:
      type: object
      properties:
        category:
          type: string
          example: analytics
        services:
          type: array
          items:
            type: object
            properties:
              name:
                type: string
                example: Google Analytics
              label:
                type: string
                example: Google Analytics 4
              cookies:
                type: array
                items:
                  $ref: "#/components/schemas/Cookie"
    Cookie:
      type: object
      properties:
        name:
          type: string
          example: _ga
        provider:
          type: string
          example: Google Analytics
        description:
          type: string
          example: Registers a unique ID to generate statistical data on how the visitor uses the website
        expiry:
          type: string
          example: 2 years
        type:
          type: string
          example: HTTP Cookie
    Stats:
      type: object
      properties:
        period:
          type: string
          example: 30d
        total_consents:
          type: integer
          example: 4832
        sampled:
          type: boolean
          description: True when the 10,000 record aggregation cap was hit.
        acceptance_rate:
          type: number
          example: 72.3
        by_action:
          type: object
          properties:
            accept_all:
              type: integer
            reject_all:
              type: integer
            custom:
              type: integer
        by_category:
          type: object
          additionalProperties:
            type: object
            properties:
              count:
                type: integer
              rate:
                type: number
        by_country:
          type: array
          items:
            type: object
            properties:
              country:
                type: string
                example: RS
              count:
                type: integer
        by_day:
          type: array
          items:
            type: object
            properties:
              date:
                type: string
                format: date
              total:
                type: integer
              accepted:
                type: integer
              rejected:
                type: integer
              custom:
                type: integer
        cached_at:
          type: string
          format: date-time
    ExportRow:
      type: object
      properties:
        visitor_id:
          type: string
          format: uuid
        action:
          type: string
          example: accept_all
        necessary:
          type: boolean
        functional:
          type: boolean
        analytics:
          type: boolean
        advertising:
          type: boolean
        performance:
          type: boolean
        country:
          type: string
          example: RS
        language:
          type: string
          example: sr
        banner_mode:
          type: string
          example: gdpr
        gpc_detected:
          type: boolean
        gpc_honored:
          type: boolean
        tc_string:
          type: string
          example: CPz...
        page_url:
          type: string
          example: https://example.com/about
        consented_at:
          type: string
          format: date-time
        expires_at:
          type: string
          format: date-time
