openapi: 3.0.3
info:
  title: Mobily URL Safety API
  version: 2.1.0
  description: |
    PIPEDA-compliant URL safety scanning API.
    Canadian-hosted in Montreal. 99.9 % uptime SLA on Enterprise.

    All responses use the Mobily envelope:
    `{success, data, error, code?, timestamp}`.
  contact:
    name: Mobily Support
    email: hello@mobily.ca
    url: https://mobily.ca/url-safety-api.html
  license:
    name: Commercial
    url: https://mobily.ca/legal/terms

servers:
  - url: https://mobily.ca/api/v2/url-safety
    description: Production (Montreal, Canada)

security:
  - ApiKeyAuth: []

components:
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-API-Key

  headers:
    RateLimitLimit:    { schema: { type: integer }, description: Requests allowed per minute. }
    RateLimitReset:    { schema: { type: integer }, description: Unix epoch when the minute resets. }
    RetryAfter:        { schema: { type: integer }, description: Seconds to wait before retrying. }

  schemas:
    Envelope:
      type: object
      required: [success, data, error, timestamp]
      properties:
        success:   { type: boolean }
        data:      { type: object, nullable: true }
        error:     { type: string, nullable: true }
        code:      { type: integer, nullable: true }
        timestamp: { type: string, format: date-time }
    ScanRequest:
      type: object
      required: [url]
      properties:
        url: { type: string, format: uri, maxLength: 2048, example: https://example.com }
    ScanResult:
      type: object
      properties:
        url:             { type: string }
        is_safe:         { type: boolean }
        threat_type:
          type: string
          nullable: true
          enum: [null, MALWARE, SOCIAL_ENGINEERING, UNWANTED_SOFTWARE, POTENTIALLY_HARMFUL_APPLICATION, SUSPICIOUS_TLD, HOMOGRAPH]
        confidence:      { type: number, format: float, minimum: 0, maximum: 1 }
        scan_time_ms:    { type: integer }
        cached:          { type: boolean }
        scans_remaining: { type: integer }
        plan:            { type: string, enum: [free, starter, pro, enterprise] }
    BatchScanRequest:
      type: object
      required: [urls]
      properties:
        urls:
          type: array
          minItems: 1
          maxItems: 1000
          items: { type: string, format: uri, maxLength: 2048 }
    BatchScanResult:
      type: object
      properties:
        results:
          type: array
          items: { $ref: '#/components/schemas/ScanResult' }
        scans_used:      { type: integer }
        scans_remaining: { type: integer }
    KeySummary:
      type: object
      properties:
        id:              { type: string, format: uuid }
        api_key_prefix:  { type: string, example: '1f7a2b8c4d12…' }
        key_name:        { type: string }
        plan:            { type: string, enum: [free, starter, pro, enterprise] }
        scans_used:      { type: integer }
        scans_limit:     { type: integer }
        scans_remaining: { type: integer }
        is_active:       { type: boolean }
        last_reset_at:   { type: string, format: date-time }
        created_at:      { type: string, format: date-time }
    CreatedKey:
      type: object
      properties:
        id:          { type: string, format: uuid }
        api_key:     { type: string, minLength: 64, maxLength: 64, description: 'Shown ONCE — store securely.' }
        key_name:    { type: string }
        plan:        { type: string }
        scans_limit: { type: integer }
        warning:     { type: string }
    Usage:
      type: object
      properties:
        plan:            { type: string }
        month:           { type: string, example: '2026-04' }
        scans_used:      { type: integer }
        scans_limit:     { type: integer }
        scans_remaining: { type: integer }
        rate_limit_rpm:  { type: integer }
        month_to_date:   { type: integer }
        daily_breakdown:
          type: array
          items:
            type: object
            properties:
              date:            { type: string, format: date }
              scans_count:     { type: integer }
              safe_count:      { type: integer }
              unsafe_count:    { type: integer }
              avg_response_ms: { type: integer }
    Export:
      type: object
      description: PIPEDA data-portability export for the authed user.
      properties:
        exported_at:  { type: string, format: date-time }
        user_id:      { type: integer }
        keys:
          type: array
          items: { $ref: '#/components/schemas/KeySummary' }
        scan_logs:
          type: array
          items:
            type: object
            properties:
              url_scanned:      { type: string }
              is_safe:          { type: boolean }
              threat_type:      { type: string, nullable: true }
              response_time_ms: { type: integer }
              cached:           { type: boolean }
              created_at:       { type: string, format: date-time }

paths:
  /scan:
    post:
      summary: Scan a single URL
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ScanRequest' }
      responses:
        '200':
          description: Scan verdict
          headers:
            X-RateLimit-Limit: { $ref: '#/components/headers/RateLimitLimit' }
            X-RateLimit-Reset: { $ref: '#/components/headers/RateLimitReset' }
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - properties: { data: { $ref: '#/components/schemas/ScanResult' } }
        '400': { description: Invalid or private URL }
        '401': { description: Missing / invalid X-API-Key }
        '413': { description: URL exceeds 2048 chars }
        '429':
          description: Rate limit or monthly quota exceeded
          headers: { Retry-After: { $ref: '#/components/headers/RetryAfter' } }

  /scan/batch:
    post:
      summary: Scan up to N URLs in a single request (N depends on plan)
      description: |
        Plan limits: Free disabled · Starter 10 · Pro 100 · Enterprise 1000.
        Each URL in the batch counts against your monthly quota.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/BatchScanRequest' }
      responses:
        '200':
          description: Per-URL verdicts in request order
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - properties: { data: { $ref: '#/components/schemas/BatchScanResult' } }
        '400': { description: Missing / empty urls[] }
        '401': { description: Missing / invalid X-API-Key }
        '403': { description: Batch not available on your plan (Free) }
        '413': { description: Batch exceeds plan limit }
        '429': { description: Quota exceeded }

  /keys:
    get:
      summary: List API keys for the authed user
      responses:
        '200':
          description: Keys list
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - properties:
                      data:
                        type: object
                        properties:
                          keys:  { type: array, items: { $ref: '#/components/schemas/KeySummary' } }
                          count: { type: integer }
    post:
      summary: Create a new key (plan auto-pinned to user subscription)
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                key_name: { type: string, maxLength: 100 }
      responses:
        '201':
          description: Key created (raw key returned ONCE)
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - properties: { data: { $ref: '#/components/schemas/CreatedKey' } }

  /keys/{uuid}:
    delete:
      summary: Revoke (permanent) a key
      parameters:
        - in: path
          name: uuid
          required: true
          schema: { type: string, format: uuid }
      responses:
        '200': { description: Revoked }
        '404': { description: Not found or not owned by the authed user }

  /usage:
    get:
      summary: Monthly usage + daily breakdown
      responses:
        '200':
          description: Usage report
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - properties: { data: { $ref: '#/components/schemas/Usage' } }

  /export:
    get:
      summary: PIPEDA data-portability export (JSON)
      description: |
        Returns all URL Safety API data held for the authed user —
        every API key (redacted to prefix), plus every scan log within the
        90-day retention window. Honours the PIPEDA right to data portability.
      responses:
        '200':
          description: User's complete URL Safety dataset
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - properties: { data: { $ref: '#/components/schemas/Export' } }

  /data:
    delete:
      summary: PIPEDA right-to-erasure — delete every record for this user
      description: |
        Hard-deletes all of the user's API keys, scan logs, and usage stats.
        Cannot be undone. 200 on success with a count of rows removed.
      responses:
        '200':
          description: Erasure complete
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - properties:
                      data:
                        type: object
                        properties:
                          keys_deleted:      { type: integer }
                          scan_logs_deleted: { type: integer }
                          stats_deleted:     { type: integer }
