openapi: 3.1.0
info:
  title: Open Places API
  version: 1.0.0
  description: First-production public Places API contract.
servers:
  - url: https://api.openplacesapi.com
security:
  - bearerApiKey: []
paths:
  /v1/places:
    get:
      operationId: searchPlaces
      summary: Search places by text near a location
      description: Searches place names and scoped addresses near a latitude/longitude point.
      parameters:
        - name: q
          in: query
          required: true
          schema:
            type: string
            minLength: 3
            maxLength: 128
          description: Search text. Trimmed and whitespace-collapsed before validation. After public normalization, `q` must include at least 3 searchable ASCII letters or numbers. Punctuation and separators do not count. `mode=address` requires at least 4 searchable characters; 3-character all-mode queries search names only and return an `address_matching_skipped` warning.
          example: coffee
        - name: lat
          in: query
          required: true
          schema:
            type: number
            minimum: -90
            maximum: 90
          example: 40.7128
        - name: lon
          in: query
          required: true
          schema:
            type: number
            minimum: -180
            maximum: 180
          example: -74.006
        - name: radius_mi
          in: query
          required: false
          schema:
            type: number
            exclusiveMinimum: 0
            maximum: 50
            default: 25
          example: 25
        - name: mode
          in: query
          required: false
          schema:
            type: string
            enum: [all, name, address]
            default: all
          example: all
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 20
            default: 10
          example: 10
        - name: layer_preset
          in: query
          required: false
          schema:
            type: string
            pattern: "^[a-z0-9][a-z0-9_-]{0,62}$"
          description: Account-owned layer preset slug. Mutually exclusive with `layers`.
          example: client-a
        - name: layers
          in: query
          required: false
          schema:
            type: string
          description: Explicit ordered layer selection starting with `base`, followed by optional `open`, then zero or more `account:<layer_slug>` tokens separated by commas. Mutually exclusive with `layer_preset`.
          example: base,open,account:client-a
      responses:
        "200":
          description: Search completed, including zero-result searches.
          headers:
            X-Request-ID:
              $ref: "#/components/headers/XRequestID"
            X-Quota-Limit:
              $ref: "#/components/headers/XQuotaLimit"
            X-Quota-Remaining:
              $ref: "#/components/headers/XQuotaRemaining"
            X-Quota-Reset:
              $ref: "#/components/headers/XQuotaReset"
            Cache-Control:
              $ref: "#/components/headers/NoStore"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SearchResponse"
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/AuthError"
        "402":
          $ref: "#/components/responses/QuotaExhausted"
        "403":
          $ref: "#/components/responses/LayerNotAvailable"
        "405":
          $ref: "#/components/responses/MethodNotAllowed"
        "422":
          $ref: "#/components/responses/UnsupportedQuery"
        "429":
          $ref: "#/components/responses/RateLimited"
        "500":
          $ref: "#/components/responses/InternalError"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"
components:
  securitySchemes:
    bearerApiKey:
      type: http
      scheme: bearer
      bearerFormat: opa_live
      description: Server-side API key in the `opa_live_<prefix>_<secret>` format.
  headers:
    XRequestID:
      description: Server-generated UUIDv4 request ID.
      schema:
        type: string
        format: uuid
    XQuotaLimit:
      description: Current quota period limit.
      schema:
        type: integer
    XQuotaRemaining:
      description: Remaining searches in the current quota period.
      schema:
        type: integer
    XQuotaReset:
      description: Current quota period reset time as Unix epoch seconds.
      schema:
        type: integer
    NoStore:
      description: Authenticated public API responses are not cacheable.
      schema:
        type: string
        const: no-store
    RetryAfter:
      description: Seconds until the client should retry.
      schema:
        type: integer
  responses:
    ValidationError:
      description: Invalid query parameter shape.
      headers:
        X-Request-ID:
          $ref: "#/components/headers/XRequestID"
        Cache-Control:
          $ref: "#/components/headers/NoStore"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
    AuthError:
      description: Missing or invalid bearer API key.
      headers:
        X-Request-ID:
          $ref: "#/components/headers/XRequestID"
        Cache-Control:
          $ref: "#/components/headers/NoStore"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
    QuotaExhausted:
      description: Monthly quota exhausted.
      headers:
        X-Request-ID:
          $ref: "#/components/headers/XRequestID"
        X-Quota-Limit:
          $ref: "#/components/headers/XQuotaLimit"
        X-Quota-Remaining:
          $ref: "#/components/headers/XQuotaRemaining"
        X-Quota-Reset:
          $ref: "#/components/headers/XQuotaReset"
        Cache-Control:
          $ref: "#/components/headers/NoStore"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
    LayerNotAvailable:
      description: Requested layer selection is well-formed but unavailable to the authenticated account.
      headers:
        X-Request-ID:
          $ref: "#/components/headers/XRequestID"
        Cache-Control:
          $ref: "#/components/headers/NoStore"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
    MethodNotAllowed:
      description: Only GET is supported.
      headers:
        Allow:
          schema:
            type: string
            const: GET
        X-Request-ID:
          $ref: "#/components/headers/XRequestID"
        Cache-Control:
          $ref: "#/components/headers/NoStore"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
    UnsupportedQuery:
      description: Recognized but unsupported search shape.
      headers:
        X-Request-ID:
          $ref: "#/components/headers/XRequestID"
        Cache-Control:
          $ref: "#/components/headers/NoStore"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
    RateLimited:
      description: Burst or auth throttle was exceeded.
      headers:
        X-Request-ID:
          $ref: "#/components/headers/XRequestID"
        Retry-After:
          $ref: "#/components/headers/RetryAfter"
        Cache-Control:
          $ref: "#/components/headers/NoStore"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
    ServiceUnavailable:
      description: Coverage or quota gate is temporarily unavailable.
      headers:
        X-Request-ID:
          $ref: "#/components/headers/XRequestID"
        Retry-After:
          $ref: "#/components/headers/RetryAfter"
        Cache-Control:
          $ref: "#/components/headers/NoStore"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
    InternalError:
      description: Generic internal error.
      headers:
        X-Request-ID:
          $ref: "#/components/headers/XRequestID"
        Cache-Control:
          $ref: "#/components/headers/NoStore"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
  schemas:
    SearchResponse:
      type: object
      required: [results, meta]
      properties:
        results:
          type: array
          items:
            $ref: "#/components/schemas/Place"
        meta:
          $ref: "#/components/schemas/SearchMeta"
    SearchMeta:
      type: object
      required: [request_id, q, mode, lat, lon, radius_mi, limit, warnings, layer_selection]
      properties:
        request_id:
          type: string
          format: uuid
        q:
          type: string
        mode:
          type: string
          enum: [all, name, address]
        lat:
          type: number
        lon:
          type: number
        radius_mi:
          type: number
        limit:
          type: integer
        warnings:
          type: array
          items:
            $ref: "#/components/schemas/SearchWarning"
        layer_selection:
          $ref: "#/components/schemas/LayerSelection"
    LayerSelection:
      type: object
      required: [source, preset, layers]
      properties:
        source:
          type: string
          enum: [default, api_key_default, preset, explicit]
        preset:
          type:
            - string
            - "null"
        layers:
          type: array
          items:
            $ref: "#/components/schemas/LayerSelectionItem"
    LayerSelectionItem:
      oneOf:
        - type: object
          required: [type]
          properties:
            type:
              type: string
              const: overture_base
        - type: object
          required: [type]
          properties:
            type:
              type: string
              const: open
        - type: object
          required: [type, slug]
          properties:
            type:
              type: string
              const: account
            slug:
              type: string
    SearchWarning:
      type: object
      required: [code, message]
      properties:
        code:
          type: string
          enum: [address_matching_skipped]
        message:
          type: string
    Place:
      type: object
      required: [place_id, name, lat, lon, distance_mi, categories, address]
      properties:
        place_id:
          type: string
        name:
          type: string
        lat:
          type: number
        lon:
          type: number
        distance_mi:
          type: number
        categories:
          type: array
          items:
            type: string
        address:
          $ref: "#/components/schemas/Address"
    Address:
      type: object
      properties:
        formatted:
          type: string
        street:
          type: string
        locality:
          type: string
        region:
          type: string
        postal_code:
          type: string
        country_code:
          type: string
    ErrorResponse:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [code, message, request_id]
          properties:
            code:
              type: string
              enum:
                - validation_error
                - unauthenticated
                - invalid_api_key
                - method_not_allowed
                - unsupported_query
                - coverage_not_ready
                - search_timeout
                - quota_exhausted
                - rate_limited
                - layer_not_available
                - quota_gate_unavailable
                - internal_error
            message:
              type: string
            request_id:
              type: string
              format: uuid
            details:
              type: array
              items:
                type: object
                required: [field, message]
                properties:
                  field:
                    type: string
                  message:
                    type: string
