openapi: 3.1.0
info:
  title: Polyfence API
  version: 1.0.0
  description: |
    Polyfence is a privacy-first geofencing API that enables location-based features without persistent user tracking.

    **Quick start**
    1. Click **Authorize** and paste your API key
    2. Try `GET /health` or `GET /zones`
  termsOfService: https://polyfence.io/terms
  contact:
    name: Polyfence
    url: https://polyfence.io
    email: support@polyfence.io
  license:
    name: Proprietary
    url: https://polyfence.io/license

servers:
  - url: https://api.polyfence.io/v1
    description: Production server

security:
  - ApiKeyAuth: []
  - SessionAuth: []

tags:
  - name: Zones
    description: Zone management operations (create, read, update, delete)
  - name: Discovery
    description: Public zone discovery by location
  - name: API Keys
    description: API key management
  - name: Geocoding
    description: Forward and reverse geocoding
  - name: Admin
    description: Admin-only endpoints (requires admin role)

paths:
  /zones:
    get:
      tags:
        - Zones
      summary: List zones
      description: |
        Retrieve all zones owned by the authenticated user.

        - Regular users see only their own zones
        - Admins should use `/api/admin/zones` to see all zones

        Supports filtering by country, region, status, and type.
      operationId: listZones
      security:
        - ApiKeyAuth: []
        - SessionAuth: []
      parameters:
        - name: country
          in: query
          description: Filter by country code (e.g., "UK", "US")
          schema:
            type: string
            example: "UK"
        - name: region
          in: query
          description: Filter by region name
          schema:
            type: string
            example: "London"
        - name: active
          in: query
          description: Filter by active status
          schema:
            type: boolean
            example: true
        - name: type
          in: query
          description: Filter by zone type
          schema:
            type: string
            enum: [circle, polygon, test]
            example: "circle"
      responses:
        '200':
          description: Successfully retrieved zones
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/ApiResponse'
                  - type: object
                    properties:
                      data:
                        type: array
                        items:
                          $ref: '#/components/schemas/Zone'
              examples:
                success:
                  value:
                    success: true
                    data:
                      - id: "550e8400-e29b-41d4-a716-446655440000"
                        profile_user_id: "123e4567-e89b-12d3-a456-426614174000"
                        name: "London Office"
                        type: "circle"
                        center_lat: 51.5074
                        center_lng: -0.1278
                        radius_meters: 500
                        polygon: null
                        info_link: null
                        region: "London"
                        country: "United Kingdom"
                        country_code: "GB"
                        is_active: true
                        source: "manual"
                        created_at: "2025-01-15T10:30:00Z"
                        updated_at: "2025-01-15T10:30:00Z"
                    message: "Zones retrieved successfully"
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '500':
          $ref: '#/components/responses/InternalServerError'

    post:
      tags:
        - Zones
      summary: Create a zone
      description: |
        Create a new geofence zone. Zones can be either:

        - **Circle**: Defined by center coordinates and radius
        - **Polygon**: Defined by an array of coordinate points

        The zone will be associated with the authenticated user.
      operationId: createZone
      security:
        - ApiKeyAuth: []
        - SessionAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ZoneRequest'
            examples:
              circle:
                summary: Circle zone example
                value:
                  name: "Office Zone"
                  type: "circle"
                  center_lat: 51.5074
                  center_lng: -0.1278
                  radius_meters: 500
                  region: "London"
                  country: "UK"
                  is_active: true
              polygon:
                summary: Polygon zone example
                value:
                  name: "Campus Zone"
                  type: "polygon"
                  polygon:
                    type: "Polygon"
                    coordinates:
                      - [
                          [-0.1278, 51.5074],
                          [-0.1268, 51.5074],
                          [-0.1268, 51.5084],
                          [-0.1278, 51.5084],
                          [-0.1278, 51.5074]
                        ]
                  region: "London"
                  country: "UK"
                  is_active: true
              etl_tfl_congestion:
                summary: ETL Example - TfL Congestion Charge Zone
                description: |
                  Real-world ETL example: Importing London Congestion Charge Zone 
                  from Transport for London (TfL) API using GeoJSON format
                value:
                  name: "London Congestion Charge Zone"
                  type: "polygon"
                  polygon:
                    type: "Polygon"
                    coordinates:
                      - [
                          [-0.1357, 51.5074],
                          [-0.1200, 51.5200],
                          [-0.0900, 51.5150],
                          [-0.0950, 51.4950],
                          [-0.1357, 51.5074]
                        ]
                  region: "London"
                  country: "UK"
                  is_active: true
                  info_link: "https://tfl.gov.uk/modes/driving/congestion-charge"
              etl_ulez:
                summary: ETL Example - ULEZ Clean Air Zone
                description: |
                  Real-world ETL example: Importing Ultra Low Emission Zone (ULEZ) 
                  from public API using GeoJSON format
                value:
                  name: "Ultra Low Emission Zone (ULEZ)"
                  type: "polygon"
                  polygon:
                    type: "Polygon"
                    coordinates:
                      - [
                          [-0.4893, 51.6918],
                          [-0.0515, 51.6918],
                          [-0.0515, 51.3847],
                          [-0.4893, 51.3847],
                          [-0.4893, 51.6918]
                        ]
                  region: "Greater London"
                  country: "UK"
                  is_active: true
                  info_link: "https://tfl.gov.uk/modes/driving/ultra-low-emission-zone"
              polygon_simple_format:
                summary: Polygon with Simple Array Format
                description: Alternative to GeoJSON - using simple lat/lng objects
                value:
                  name: "Delivery Zone"
                  type: "polygon"
                  polygon:
                    - lat: 51.5074
                      lng: -0.1278
                    - lat: 51.5084
                      lng: -0.1268
                    - lat: 51.5094
                      lng: -0.1288
                  region: "London"
                  country: "UK"
      responses:
        '201':
          description: Zone created successfully
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/ApiResponse'
                  - type: object
                    properties:
                      data:
                        $ref: '#/components/schemas/Zone'
              examples:
                success:
                  value:
                    success: true
                    data:
                      id: "550e8400-e29b-41d4-a716-446655440000"
                      profile_user_id: "123e4567-e89b-12d3-a456-426614174000"
                      name: "Office Zone"
                      type: "circle"
                      center_lat: 51.5074
                      center_lng: -0.1278
                      radius_meters: 500
                      polygon: null
                      info_link: null
                      region: "London"
                      country: "United Kingdom"
                      country_code: "GB"
                      is_active: true
                      source: "manual"
                      created_at: "2025-01-15T10:30:00Z"
                      updated_at: "2025-01-15T10:30:00Z"
                    message: "Zone created successfully"
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '429':
          $ref: '#/components/responses/RateLimitError'
        '500':
          $ref: '#/components/responses/InternalServerError'

  /zones/{id}:
    get:
      tags:
        - Zones
      summary: Get a zone by ID
      description: |
        Retrieve a specific zone by its ID.

        - Regular users can only access their own zones
        - Admins can access any zone
      operationId: getZone
      security:
        - ApiKeyAuth: []
        - SessionAuth: []
      parameters:
        - $ref: '#/components/parameters/ZoneId'
      responses:
        '200':
          description: Successfully retrieved zone
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/ApiResponse'
                  - type: object
                    properties:
                      data:
                        $ref: '#/components/schemas/Zone'
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '403':
          $ref: '#/components/responses/ForbiddenError'
        '404':
          $ref: '#/components/responses/NotFoundError'
        '500':
          $ref: '#/components/responses/InternalServerError'

    put:
      tags:
        - Zones
      summary: Update a zone
      description: |
        Update an existing zone. You can update any field except `id` and `profile_user_id`.

        - Regular users can only update their own zones
        - Admins can update any zone
      operationId: updateZone
      security:
        - ApiKeyAuth: []
        - SessionAuth: []
      parameters:
        - $ref: '#/components/parameters/ZoneId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ZoneUpdateRequest'
            examples:
              updateName:
                summary: Update zone name
                value:
                  name: "Updated Office Name"
              updateRadius:
                summary: Update circle radius
                value:
                  radius_meters: 750
              updateStatus:
                summary: Deactivate zone
                value:
                  is_active: false
      responses:
        '200':
          description: Zone updated successfully
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/ApiResponse'
                  - type: object
                    properties:
                      data:
                        $ref: '#/components/schemas/Zone'
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '403':
          $ref: '#/components/responses/ForbiddenError'
        '404':
          $ref: '#/components/responses/NotFoundError'
        '500':
          $ref: '#/components/responses/InternalServerError'

    delete:
      tags:
        - Zones
      summary: Delete a zone
      description: |
        Delete a zone by ID.

        **Safety checks:**
        - Only `manual` and `test` zones can be deleted
        - Official zones (e.g., `source: "official"`) cannot be deleted
        - Regular users can only delete their own zones
        - Admins can delete any eligible zone
      operationId: deleteZone
      security:
        - ApiKeyAuth: []
        - SessionAuth: []
      parameters:
        - $ref: '#/components/parameters/ZoneId'
      responses:
        '200':
          description: Zone deleted successfully
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/ApiResponse'
                  - type: object
                    properties:
                      data:
                        type: object
                        properties:
                          message:
                            type: string
                            example: "Zone deleted successfully"
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '403':
          description: Forbidden - Cannot delete official zones or zones owned by others
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiResponse'
              examples:
                officialZone:
                  value:
                    success: false
                    error:
                      error: "Forbidden"
                      message: "Cannot delete official zones"
                    message: "Cannot delete official zones"
        '404':
          $ref: '#/components/responses/NotFoundError'
        '500':
          $ref: '#/components/responses/InternalServerError'

  /zones/bulk:
    post:
      tags:
        - Zones
      summary: Bulk zone operations
      description: |
        Perform bulk operations on multiple zones:

        - **delete**: Delete multiple zones at once
        - **activate**: Set multiple zones to active
        - **deactivate**: Set multiple zones to inactive

        The API will only modify zones the user has permission to access.
        Returns lists of successful and failed operations.
      operationId: bulkZoneOperation
      security:
        - ApiKeyAuth: []
        - SessionAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BulkOperationRequest'
            examples:
              delete:
                summary: Bulk delete zones
                value:
                  action: "delete"
                  zoneIds:
                    - "550e8400-e29b-41d4-a716-446655440000"
                    - "550e8400-e29b-41d4-a716-446655440001"
              activate:
                summary: Bulk activate zones
                value:
                  action: "activate"
                  zoneIds:
                    - "550e8400-e29b-41d4-a716-446655440000"
                    - "550e8400-e29b-41d4-a716-446655440001"
      responses:
        '200':
          description: Bulk operation completed
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/ApiResponse'
                  - type: object
                    properties:
                      data:
                        type: object
                        properties:
                          deleted:
                            type: integer
                            description: Number of zones deleted (for delete action)
                          updated:
                            type: integer
                            description: Number of zones updated (for activate/deactivate)
                          deletedZones:
                            type: array
                            items:
                              type: object
                              properties:
                                id:
                                  type: string
                                name:
                                  type: string
                          nonDeletableZones:
                            type: array
                            items:
                              type: object
                              properties:
                                id:
                                  type: string
                                name:
                                  type: string
                                reason:
                                  type: string
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '500':
          $ref: '#/components/responses/InternalServerError'

  /zones/discovery:
    post:
      tags:
        - Discovery
      summary: Discover zones at a location
      description: |
        **Public endpoint** - No authentication required.

        Find all active zones that contain a given GPS coordinate.
        Useful for checking if a user is inside any geofenced areas.

        Returns zones sorted by distance (closest first).
      operationId: discoverZones
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ZoneDiscoveryRequest'
            example:
              lat: 51.5074
              lng: -0.1278
      responses:
        '200':
          description: Successfully discovered zones
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ZoneDiscoveryResponse'
              example:
                zones:
                  - id: "550e8400-e29b-41d4-a716-446655440000"
                    name: "London Office"
                    type: "circle"
                    distance_meters: 0
                    is_active: true
                  - id: "550e8400-e29b-41d4-a716-446655440001"
                    name: "Nearby Warehouse"
                    type: "polygon"
                    distance_meters: 0
                    is_active: true
        '400':
          $ref: '#/components/responses/ValidationError'
        '500':
          $ref: '#/components/responses/InternalServerError'

  /zones/validate:
    post:
      tags:
        - Zones
      summary: Validate zone data
      description: |
        Validate zone data without creating it. Useful for ETL pipelines to test 
        transformations before bulk importing.
        
        **Use Cases:**
        - Test zone format before bulk import
        - Validate GeoJSON conversion
        - Check polygon complexity (point count)
        - Verify coordinate bounds
        
        **ETL Example:**
        ```python
        # Test your transform before importing
        zones = transform_from_public_api()
        
        for zone in zones:
          result = polyfence.validate_zone(zone)
          if not result["valid"]:
            print(f"Invalid: {result['errors']}")
        ```
      operationId: validateZone
      security:
        - ApiKeyAuth: []
        - SessionAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ZoneRequest'
            examples:
              circleZone:
                summary: Circle zone validation
                value:
                  name: "Test Circle"
                  type: "circle"
                  center_lat: 51.5074
                  center_lng: -0.1278
                  radius_meters: 500
              polygonSimple:
                summary: Polygon with simple array format
                value:
                  name: "Test Polygon"
                  type: "polygon"
                  polygon:
                    - lat: 51.5074
                      lng: -0.1278
                    - lat: 51.5075
                      lng: -0.1280
                    - lat: 51.5076
                      lng: -0.1279
              polygonGeoJSON:
                summary: Polygon with GeoJSON format (ETL)
                value:
                  name: "London Congestion Zone"
                  type: "polygon"
                  polygon:
                    type: "Polygon"
                    coordinates:
                      - [
                          [-0.1278, 51.5074],
                          [-0.1280, 51.5075],
                          [-0.1279, 51.5076],
                          [-0.1278, 51.5074]
                        ]
      responses:
        '200':
          description: Validation result
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    description: Always true for validation endpoint
                  valid:
                    type: boolean
                    description: Whether the zone data is valid
                  errors:
                    type: object
                    description: Validation errors (if any)
                    additionalProperties:
                      type: string
                  normalized:
                    type: object
                    description: Normalized zone data (shows how polygon will be stored)
              examples:
                valid:
                  summary: Valid zone
                  value:
                    success: true
                    valid: true
                    errors: {}
                    normalized:
                      name: "Test Zone"
                      type: "polygon"
                      polygon:
                        - lat: 51.5074
                          lng: -0.1278
                        - lat: 51.5075
                          lng: -0.1280
                        - lat: 51.5076
                          lng: -0.1279
                invalid:
                  summary: Invalid zone (too few points)
                  value:
                    success: true
                    valid: false
                    errors:
                      polygon: "Polygon must have at least 3 points (currently has 2 points)"
                invalidTooManyPoints:
                  summary: Invalid zone (too many points)
                  value:
                    success: true
                    valid: false
                    errors:
                      polygon: "Polygon has too many points (15000). Maximum allowed is 10000 points."
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '500':
          $ref: '#/components/responses/InternalServerError'

  /keys:
    get:
      tags:
        - API Keys
      summary: List API keys
      description: |
        Retrieve all API keys for the authenticated user.

        **Note:** The actual key value is only shown once during creation.
        This endpoint only returns metadata (prefix, name, creation date).
      operationId: listApiKeys
      security:
        - SessionAuth: []
      responses:
        '200':
          description: Successfully retrieved API keys
          content:
            application/json:
              schema:
                type: object
                properties:
                  keys:
                    type: array
                    items:
                      $ref: '#/components/schemas/ApiKeyMetadata'
              example:
                keys:
                  - id: "123e4567-e89b-12d3-a456-426614174000"
                    prefix: "pd_"
                    createdAt: "2025-01-15T10:30:00Z"
                    revokedAt: null
                    isActive: true
                    last_used_at: "2025-01-15T12:00:00Z"
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '500':
          $ref: '#/components/responses/InternalServerError'

    post:
      tags:
        - API Keys
      summary: Create an API key
      description: |
        Generate a new API key for programmatic access.

        **Important:** The full key value is only returned once in this response.
        Store it securely - you won't be able to retrieve it again.
      operationId: createApiKey
      security:
        - SessionAuth: []
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                  default: "Untitled Key"
                  description: Descriptive name for the API key (e.g., "Production API", "Mobile App")
                  maxLength: 255
            example:
              name: "Production API Key"
      responses:
        '201':
          description: API key created successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  key:
                    type: string
                    description: The full API key (only shown once!). Prefix is environment-specific (pf_prod_, pf_test_, pf_dev_)
                    example: "pf_prod_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0u1v2w3x4y5z6"
                  keyData:
                    $ref: '#/components/schemas/ApiKeyMetadata'
        '400':
          description: API key limit exceeded or invalid request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiResponse'
              examples:
                keyLimitExceeded:
                  summary: Maximum API keys exceeded
                  value:
                    success: false
                    error:
                      error: "Failed to create API key"
                      message: "Maximum 10 active API keys allowed per user. Please revoke an existing key before creating a new one."
                    message: "Maximum 10 active API keys allowed per user"
                invalidScopes:
                  summary: Invalid scopes provided
                  value:
                    success: false
                    error:
                      error: "Invalid scopes provided"
                      invalidScopes: ["zones:invalid"]
                    message: "Invalid scopes provided"
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '429':
          $ref: '#/components/responses/RateLimitError'
        '500':
          $ref: '#/components/responses/InternalServerError'

  /geocode:
    get:
      tags:
        - Geocoding
      summary: Forward geocoding
      description: |
        Convert an address or place name into GPS coordinates.

        Uses OpenStreetMap Nominatim API under the hood.
        Server-side proxy to avoid CORS issues and rate limiting.
      operationId: geocodeAddress
      parameters:
        - name: q
          in: query
          required: true
          description: Address or place name to geocode
          schema:
            type: string
            example: "10 Downing Street, London, UK"
      responses:
        '200':
          description: Successfully geocoded address
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    example: true
                  result:
                    type: object
                    nullable: true
                    properties:
                      lat:
                        type: string
                        example: "51.5033635"
                      lon:
                        type: string
                        example: "-0.1276248"
                      display_name:
                        type: string
                        example: "10 Downing Street, Westminster, London, Greater London, England, SW1A 2AA, United Kingdom"
        '400':
          $ref: '#/components/responses/ValidationError'
        '500':
          $ref: '#/components/responses/InternalServerError'

components:
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: x-api-key
      description: |
        API key authentication for external applications.

        Get your API key from https://polyfence.io/dashboard

    SessionAuth:
      type: apiKey
      in: cookie
      name: sb-{project_ref}-auth-token
      description: |
        Session-based authentication for dashboard UI.
        Handled automatically by Supabase OAuth.

  parameters:
    ZoneId:
      name: id
      in: path
      required: true
      description: Zone UUID
      schema:
        type: string
        format: uuid
        example: "550e8400-e29b-41d4-a716-446655440000"

  schemas:
    ApiResponse:
      type: object
      required:
        - success
      properties:
        success:
          type: boolean
          description: Whether the request was successful
        data:
          description: Response data (varies by endpoint)
        error:
          type: object
          properties:
            error:
              type: string
              description: Error type
            message:
              type: string
              description: Detailed error message
        message:
          type: string
          description: Human-readable message

    Zone:
      type: object
      required:
        - id
        - profile_user_id
        - name
        - type
        - country
        - is_active
        - source
        - created_at
        - updated_at
      properties:
        id:
          type: string
          format: uuid
          description: Unique zone identifier
          example: "550e8400-e29b-41d4-a716-446655440000"
        profile_user_id:
          type: string
          format: uuid
          description: Owner's user ID
          example: "123e4567-e89b-12d3-a456-426614174000"
        name:
          type: string
          description: Zone name
          example: "London Office"
          maxLength: 255
        type:
          type: string
          enum: [circle, polygon]
          description: Zone geometry type
          example: "circle"
        center_lat:
          type: number
          format: double
          nullable: true
          description: Center latitude (for circle zones)
          minimum: -90
          maximum: 90
          example: 51.5074
        center_lng:
          type: number
          format: double
          nullable: true
          description: Center longitude (for circle zones)
          minimum: -180
          maximum: 180
          example: -0.1278
        radius_meters:
          type: number
          format: double
          nullable: true
          description: Radius in meters (for circle zones)
          minimum: 0
          maximum: 1000
          example: 500
        polygon:
          nullable: true
          description: GeoJSON Polygon geometry (for polygon zones)
          oneOf:
            - $ref: '#/components/schemas/GeoJSONPolygon'
        info_link:
          type: string
          nullable: true
          description: Optional URL with more information
          example: "https://example.com/zone-info"
        region:
          type: string
          nullable: true
          description: Geographic region
          example: "London"
        country:
          type: string
          description: Country name (human-readable)
          example: "United Kingdom"
        country_code:
          type: string
          nullable: true
          description: ISO 3166-1 alpha-2 country code (e.g., GB, US, FR)
          pattern: '^[A-Z]{2}$'
          example: "GB"
        is_active:
          type: boolean
          description: Whether the zone is active
          example: true
        source:
          type: string
          enum: [api, ui, cli, admin]
          description: Zone creation source - api (API key), ui (dashboard), cli (command line), admin (admin API)
          example: "api"
        created_at:
          type: string
          format: date-time
          description: Creation timestamp
          example: "2025-01-15T10:30:00Z"
        updated_at:
          type: string
          format: date-time
          description: Last update timestamp
          example: "2025-01-15T10:30:00Z"

    ZoneRequest:
      type: object
      required:
        - name
        - type
      properties:
        name:
          type: string
          description: Zone name
          example: "Office Zone"
          maxLength: 255
        type:
          type: string
          enum: [circle, polygon]
          description: Zone geometry type
          example: "circle"
        center_lat:
          type: number
          format: double
          description: Center latitude (required for circle zones)
          minimum: -90
          maximum: 90
          example: 51.5074
        center_lng:
          type: number
          format: double
          description: Center longitude (required for circle zones)
          minimum: -180
          maximum: 180
          example: -0.1278
        radius_meters:
          type: number
          format: double
          description: Radius in meters (required for circle zones)
          minimum: 0
          maximum: 1000
          example: 500
        polygon:
          description: |
            Polygon geometry (required for polygon zones). Accepts two formats:
            
            **Format 1: Simple Array** (Documented format)
            ```json
            [
              {"lat": 51.5074, "lng": -0.1278},
              {"lat": 51.5075, "lng": -0.1280},
              {"lat": 51.5076, "lng": -0.1279}
            ]
            ```
            
            **Format 2: GeoJSON** (Auto-converts - perfect for ETL)
            ```json
            {
              "type": "Polygon",
              "coordinates": [[
                [-0.1278, 51.5074],
                [-0.1280, 51.5075],
                [-0.1279, 51.5076]
              ]]
            }
            ```
            
            **ETL Example (TfL/ULEZ):**
            ```python
            # Harvest from public API (returns GeoJSON)
            tfl_zone = fetch_tfl_congestion_zone()
            
            # Send directly to PolyFence - auto-converts!
            polyfence.create_zone({
              "name": tfl_zone["name"],
              "type": "polygon",
              "polygon": tfl_zone["geometry"]  # GeoJSON
            })
            ```
          oneOf:
            - type: array
              description: Simple array of lat/lng objects
              items:
                type: object
                required: [lat, lng]
                properties:
                  lat:
                    type: number
                    minimum: -90
                    maximum: 90
                  lng:
                    type: number
                    minimum: -180
                    maximum: 180
              minItems: 3
              maxItems: 10000
              example:
                - lat: 51.5074
                  lng: -0.1278
                - lat: 51.5075
                  lng: -0.1280
                - lat: 51.5076
                  lng: -0.1279
            - $ref: '#/components/schemas/GeoJSONPolygon'
        info_link:
          type: string
          description: Optional URL with more information
          example: "https://example.com/zone-info"
        region:
          type: string
          description: Geographic region
          example: "London"
        country:
          type: string
          description: Country name (human-readable, will be used to compute country_code)
          default: "UK"
          example: "United Kingdom"
        country_code:
          type: string
          description: ISO 3166-1 alpha-2 country code (auto-computed from country field, or can be provided directly)
          pattern: '^[A-Z]{2}$'
          example: "GB"
        is_active:
          type: boolean
          description: Whether the zone is active
          default: true
          example: true
        source:
          type: string
          enum: [api, ui, cli, admin]
          description: Zone creation source - api (API key), ui (dashboard), cli (command line), admin (admin API)
          example: "api"

    ZoneUpdateRequest:
      type: object
      description: Partial update - only include fields you want to change
      properties:
        name:
          type: string
          maxLength: 255
        type:
          type: string
          enum: [circle, polygon, test]
        center_lat:
          type: number
          format: double
          minimum: -90
          maximum: 90
        center_lng:
          type: number
          format: double
          minimum: -180
          maximum: 180
        radius_meters:
          type: number
          format: double
          minimum: 0
          maximum: 1000
        polygon:
          oneOf:
            - $ref: '#/components/schemas/GeoJSONPolygon'
        info_link:
          type: string
        region:
          type: string
        country:
          type: string
        is_active:
          type: boolean

    GeoJSONPolygon:
      type: object
      description: |
        GeoJSON Polygon format (RFC 7946) - automatically converted to internal format.
        
        **Coordinate Order:** GeoJSON uses [longitude, latitude] order.
        
        **ETL Integration:** You can send GeoJSON directly from public APIs (TfL, ULEZ, etc.) 
        and PolyFence will automatically convert coordinates.
        
        **Limits:**
        - Minimum: 3 points
        - Maximum: 10,000 points (for performance)
        - Latitude: -90 to 90
        - Longitude: -180 to 180
      required:
        - type
        - coordinates
      properties:
        type:
          type: string
          enum: [Polygon]
          example: "Polygon"
        coordinates:
          type: array
          description: Array of linear rings (first is exterior, rest are holes). Each position is [lng, lat].
          items:
            type: array
            description: Linear ring (array of [lng, lat] positions). First and last point must be identical.
            minItems: 3
            maxItems: 10000
            items:
              type: array
              description: Position [longitude, latitude]
              items:
                type: number
              minItems: 2
              maxItems: 2
          example:
            - [
                [-0.1278, 51.5074],
                [-0.1268, 51.5074],
                [-0.1268, 51.5084],
                [-0.1278, 51.5084],
                [-0.1278, 51.5074]
              ]

    BulkOperationRequest:
      type: object
      required:
        - action
        - zoneIds
      properties:
        action:
          type: string
          enum: [delete, activate, deactivate]
          description: Bulk operation to perform
          example: "delete"
        zoneIds:
          type: array
          items:
            type: string
            format: uuid
          description: Array of zone UUIDs to operate on
          minItems: 1
          example:
            - "550e8400-e29b-41d4-a716-446655440000"
            - "550e8400-e29b-41d4-a716-446655440001"

    ZoneDiscoveryRequest:
      type: object
      required:
        - lat
        - lng
      properties:
        lat:
          type: number
          format: double
          description: Latitude to check
          minimum: -90
          maximum: 90
          example: 51.5074
        lng:
          type: number
          format: double
          description: Longitude to check
          minimum: -180
          maximum: 180
          example: -0.1278
        radius_meters:
          type: number
          format: double
          description: Optional search radius (not currently implemented)
          example: 1000

    ZoneDiscoveryResponse:
      type: object
      properties:
        zones:
          type: array
          items:
            type: object
            properties:
              id:
                type: string
                format: uuid
              name:
                type: string
              type:
                type: string
                enum: [circle, polygon, test]
              distance_meters:
                type: integer
                description: Distance from query point (0 if inside polygon)
              is_active:
                type: boolean

    ApiKeyMetadata:
      type: object
      properties:
        id:
          type: string
          format: uuid
          description: API key ID
        prefix:
          type: string
          description: Key prefix (environment-specific - pd_ for production, pt_ for test, dev_ for development)
          example: "pd_"
        createdAt:
          type: string
          format: date-time
          description: Creation timestamp
        revokedAt:
          type: string
          format: date-time
          nullable: true
          description: Revocation timestamp (null if active)
        isActive:
          type: boolean
          description: Whether the key is currently active
        last_used_at:
          type: string
          format: date-time
          nullable: true
          description: Last time this key was used

  responses:
    UnauthorizedError:
      description: Authentication required or invalid
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ApiResponse'
          example:
            success: false
            error:
              error: "Unauthorized"
              message: "Authentication required. Provide either a valid session token or x-api-key header."
            message: "Authentication required. Provide either a valid session token or x-api-key header."

    ForbiddenError:
      description: Access denied - insufficient permissions
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ApiResponse'
          example:
            success: false
            error:
              error: "Forbidden"
              message: "You do not have permission to access this resource"
            message: "Access denied"

    NotFoundError:
      description: Resource not found
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ApiResponse'
          example:
            success: false
            error:
              error: "Not found"
              message: "Zone not found"
            message: "Zone not found"

    ValidationError:
      description: Invalid request data
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ApiResponse'
          example:
            success: false
            error:
              error: "Validation failed"
              message: "Name is required"
            message: "Name is required"

    InternalServerError:
      description: Server error
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ApiResponse'
          example:
            success: false
            error:
              error: "Internal server error"
            message: "An unexpected error occurred"

    RateLimitError:
      description: Rate limit exceeded
      headers:
        Retry-After:
          description: Seconds to wait before making another request
          schema:
            type: integer
            example: 1
        X-RateLimit-Limit:
          description: Maximum requests allowed per second
          schema:
            type: integer
            example: 10
        X-RateLimit-Remaining:
          description: Requests remaining in current window
          schema:
            type: integer
            example: 0
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ApiResponse'
          example:
            success: false
            error:
              error: "Rate limit exceeded"
              message: "Maximum 10 requests per second allowed. Please slow down."
            message: "Rate limit exceeded"
