openapi: 3.0.3
info:
  title: FreightAPIs
  description: |
    Logistics data API for freight and shipping applications.

    **Products:**
    - Carrier locations: terminals and service points across 10 LTL/freight
      carriers (ABF, Central Transport, CEVA, C.H. Robinson, Estes, GLS, SAIA,
      Seko, TForce, USPS), searchable by state, ZIP, city, geographic radius and batch.
    - Seaports: 4,400+ global seaports by country, UN/LOCODE, shipping company
      and geographic radius.
    - USPS drop points: 98,000+ post offices and collection boxes by ZIP, keyword or ID.
    - NMFC classification: full-text search of 20,000+ freight-class articles,
      article/rule/package lookups with change history, and density / Item 680
      (Note 2A/2B/2C) calculators.

    **Plans & access:** All endpoints are available on every plan (Free included)
    except NMFC classification, which requires Pro. Plans differ by monthly call
    quota, not endpoint access.

    Cached responses are served from the Cloudflare edge network for 30 days.
    Every authenticated call counts toward your monthly quota, even on a cache hit.
  version: 1.0.0
  contact:
    name: FreightAPI Support
    url: https://freightapis.dev
    email: support@freightapis.dev
  license:
    name: Proprietary
    url: https://freightapis.dev/terms

servers:
  - url: https://freightapis.dev
    description: Production

security:
  - ApiKeyAuth: []

tags:
  # General/open products first; NMFC (Pro-only) grouped at the end.
  - name: ABF Locations
    description: ABF Freight (LTL) terminal locations, coverage, service days, transit time and carrier metadata.
  - name: Central Transport Locations
    description: Central Transport (LTL) terminal locations and coverage.
  - name: CEVA Locations
    description: CEVA Logistics worldwide office locations with country, activity-type and geo-radius lookup.
  - name: C.H. Robinson Locations
    description: C.H. Robinson office locations by region and business category.
  - name: Estes Locations
    description: Estes Express terminal locations by state, ZIP and alpha code.
  - name: GLS Locations
    description: GLS US dropbox locations by state and ZIP.
  - name: SAIA Locations
    description: SAIA LTL Freight terminal locations by state, ZIP and alpha code.
  - name: Seko Locations
    description: Seko Logistics worldwide locations and global hubs.
  - name: TForce Locations
    description: TForce Freight terminal locations by state, ZIP, country and alpha code.
  - name: USPS Locations
    description: USPS retail locations by state, ZIP, city and geographic radius.
  - name: Seaports
    description: Global seaports by country, UN/LOCODE, shipping company and geographic radius.
  - name: Amazon Drop Points
    description: Amazon pickup and return point lookup by ZIP, keyword or ID.
  - name: NMFC Search
    description: NMFC article full-text search (Pro plan).
  - name: NMFC Articles
    description: NMFC article details, rules, packages and change history (Pro plan).
  - name: NMFC Calculators
    description: NMFC density and Item 680 (Note 2A/2B/2C) freight-class calculations (Pro plan).

paths:
  /api/search:
    post:
      tags: [NMFC Search]
      summary: Keyword search
      description: |
        Full-text search across NMFC articles and/or the alphabetical index.
        Returns paginated results. Minimum query length is 3 characters.
      operationId: search
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SearchRequest'
            examples:
              basic:
                summary: Search articles only
                value:
                  q: "steel pipe"
                  page: 1
                  scopes: ["article"]
              both:
                summary: Search both articles and index
                value:
                  q: "furniture"
                  page: 1
                  scopes: ["article", "index"]
      responses:
        '200':
          description: Search results
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SearchResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/PlanRequired'
        '429':
          $ref: '#/components/responses/RateLimited'

  /api/article/{num}:
    get:
      tags: [NMFC Articles]
      summary: Get article details
      description: Retrieve full details for a single NMFC article by its article number.
      operationId: getArticle
      parameters:
        - name: num
          in: path
          required: true
          schema:
            type: string
          example: "156600"
          description: NMFC article number
      responses:
        '200':
          description: Article details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ArticleResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/PlanRequired'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'

  /api/rule/{num}:
    get:
      tags: [NMFC Articles]
      summary: Get rule details
      description: Retrieve details for an NMFC rule by its rule number.
      operationId: getRule
      parameters:
        - name: num
          in: path
          required: true
          schema:
            type: string
          example: "110"
          description: NMFC rule number
      responses:
        '200':
          description: Rule details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RuleResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/PlanRequired'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'

  /api/package/{num}:
    get:
      tags: [NMFC Articles]
      summary: Get package specification
      description: Retrieve details for an NMFC package specification.
      operationId: getPackage
      parameters:
        - name: num
          in: path
          required: true
          schema:
            type: string
          example: "245"
          description: NMFC package number
      responses:
        '200':
          description: Package details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PackageResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/PlanRequired'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'

  /api/history/{num}:
    get:
      tags: [NMFC Articles]
      summary: Get article change history
      description: Retrieve docket-by-docket change history for an NMFC article.
      operationId: getHistory
      parameters:
        - name: num
          in: path
          required: true
          schema:
            type: string
          example: "156600"
          description: NMFC article number
      responses:
        '200':
          description: Change history
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HistoryResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/PlanRequired'
        '429':
          $ref: '#/components/responses/RateLimited'

  /api/density:
    post:
      tags: [NMFC Calculators]
      summary: Density calculator
      description: |
        Calculate shipping density (weight per cubic foot or kg per cubic meter)
        from handling-unit dimensions and weight. Supports inch/feet/cm/m and pound/kg.
      operationId: calculateDensity
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/DensityRequest'
            example:
              dimensionType: "Inch"
              weightType: "Pound"
              resultUnitType: "PoundsPerCubicFeet"
              unitDimensions:
                - HandlingUnitCount: 1
                  length: 48
                  width: 40
                  height: 36
                  handlingUnitWeight: 500
      responses:
        '200':
          description: Density calculation result
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DensityResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/PlanRequired'
        '429':
          $ref: '#/components/responses/RateLimited'

  /api/item680/{type}:
    post:
      tags: [NMFC Calculators]
      summary: Item 680 calculator (Note 2A / 2B / 2C)
      description: |
        Calculate the three Item 680 notes used to determine freight class for items
        in containers or on handling platforms.

        - **2A**: Percentage of package volume occupied by commodity
        - **2B**: Percentage of handling-platform surface area occupied by commodity
        - **2C**: Ratio of commodity height to lift-truck reach, with max-allowable dimensions
      operationId: calculateItem680
      parameters:
        - name: type
          in: path
          required: true
          schema:
            type: string
            enum: ["2a", "2b", "2c"]
          description: Note type
      requestBody:
        required: true
        content:
          application/json:
            schema:
              oneOf:
                - $ref: '#/components/schemas/Item680Note2ARequest'
                - $ref: '#/components/schemas/Item680Note2BRequest'
                - $ref: '#/components/schemas/Item680Note2CRequest'
            examples:
              note2a:
                summary: Note 2A
                value:
                  commodity: { length: 20, width: 15, height: 10 }
                  package: { length: 24, width: 18, height: 12 }
              note2b:
                summary: Note 2B
                value:
                  commodity: { length: 40, width: 30 }
                  handlingPlatformType: { length: 48, width: 40 }
              note2c:
                summary: Note 2C
                value:
                  length: 48
                  width: 40
                  height: 60
                  liftLength: 48
                  liftWidth: 40
      responses:
        '200':
          description: Calculation result
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: '#/components/schemas/Item680Note2AResponse'
                  - $ref: '#/components/schemas/Item680Note2BResponse'
                  - $ref: '#/components/schemas/Item680Note2CResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/PlanRequired'
        '429':
          $ref: '#/components/responses/RateLimited'

  /api/drop-points:
    get:
      tags: [Amazon Drop Points]
      summary: List drop points by ZIP code
      description: Returns USPS drop points (post offices and collection boxes) within the given 5-digit ZIP code.
      operationId: getDropPointsByZip
      parameters:
        - name: zip
          in: query
          required: true
          schema:
            type: string
            pattern: '^\d{5}$'
          example: "90001"
          description: 5-digit US ZIP code
        - name: page
          in: query
          required: false
          schema:
            type: integer
            default: 1
            minimum: 1
      responses:
        '200':
          description: Drop points within ZIP code
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DropPointsListResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/RateLimited'

  /api/drop-points/search:
    get:
      tags: [Amazon Drop Points]
      summary: Search drop points
      description: Search USPS drop points by name, address or city. Minimum query length is 2 characters.
      operationId: searchDropPoints
      parameters:
        - name: q
          in: query
          required: true
          schema:
            type: string
            minLength: 2
          example: "Hollywood"
          description: Search keyword (name, city, address)
        - name: page
          in: query
          required: false
          schema:
            type: integer
            default: 1
            minimum: 1
      responses:
        '200':
          description: Search results
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DropPointsSearchResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/RateLimited'

  /api/drop-points/{id}:
    get:
      tags: [Amazon Drop Points]
      summary: Get drop point detail
      description: Retrieve full details for a single USPS drop point by its access-point ID.
      operationId: getDropPoint
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          description: Access point ID
      responses:
        '200':
          description: Drop point details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DropPointDetailResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'

  # ===========================================================
  # ABF Freight Locations
  # ===========================================================
  /api/abf-location:
    get:
      tags: [ABF Locations]
      summary: Get ABF location by ZIP
      operationId: abfLocationByZip
      parameters:
        - { name: zip, in: query, required: true, schema: { type: string, pattern: '^\d{5}$' }, example: "72204" }
        - { name: city, in: query, required: false, schema: { type: string } }
      responses:
        '200': { description: ABF location(s) for the ZIP, content: { application/json: { schema: { $ref: '#/components/schemas/CarrierLocationListResponse' } } } }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /api/abf-location/search:
    get:
      tags: [ABF Locations]
      summary: Search ABF locations
      operationId: abfLocationSearch
      parameters:
        - { name: q, in: query, required: true, schema: { type: string, minLength: 2 } }
        - { name: page, in: query, required: false, schema: { type: integer, default: 1, minimum: 1 } }
      responses:
        '200': { description: Paginated ABF search results, content: { application/json: { schema: { $ref: '#/components/schemas/CarrierLocationListResponse' } } } }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /api/abf-location/station/{num}:
    get:
      tags: [ABF Locations]
      summary: Get ABF locations by station number
      operationId: abfLocationByStation
      parameters:
        - { name: num, in: path, required: true, schema: { type: string }, description: ABF station number }
        - { name: page, in: query, required: false, schema: { type: integer, default: 1, minimum: 1 } }
      responses:
        '200': { description: Locations served by station, content: { application/json: { schema: { $ref: '#/components/schemas/CarrierLocationListResponse' } } } }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /api/abf-location/batch:
    post:
      tags: [ABF Locations]
      summary: Batch ABF lookup by ZIPs
      description: Pro plan allows up to 20 ZIPs per call, Enterprise up to 50.
      operationId: abfLocationBatch
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ZipBatchRequest' }
      responses:
        '200': { description: Batch results, content: { application/json: { schema: { $ref: '#/components/schemas/BatchResponse' } } } }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /api/abf-location/coverage:
    get:
      tags: [ABF Locations]
      summary: ABF coverage statistics by state
      operationId: abfCoverage
      parameters:
        - { name: state, in: query, required: true, schema: { type: string, pattern: '^[A-Za-z]{2}$' }, example: "CA" }
      responses:
        '200': { description: Coverage stats, content: { application/json: { schema: { $ref: '#/components/schemas/CoverageResponse' } } } }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /api/abf-location/service-days:
    get:
      tags: [ABF Locations]
      summary: ABF service-days schedule and next pickup/delivery
      operationId: abfServiceDays
      parameters:
        - { name: zip, in: query, required: true, schema: { type: string, pattern: '^\d{5}$' } }
        - { name: city, in: query, required: false, schema: { type: string } }
      responses:
        '200': { description: Service days info, content: { application/json: { schema: { type: object } } } }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /api/abf-location/transit-info:
    get:
      tags: [ABF Locations]
      summary: ABF transit info between two ZIPs
      operationId: abfTransitInfo
      parameters:
        - { name: origin_zip, in: query, required: true, schema: { type: string, pattern: '^\d{5}$' } }
        - { name: dest_zip, in: query, required: true, schema: { type: string, pattern: '^\d{5}$' } }
      responses:
        '200': { description: Transit info with add-on days, content: { application/json: { schema: { type: object } } } }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /api/abf-location/carriers:
    get:
      tags: [ABF Locations]
      summary: ABF carrier metadata (SCAC, add-on days) for a ZIP
      operationId: abfCarriers
      parameters:
        - { name: zip, in: query, required: true, schema: { type: string, pattern: '^\d{5}$' } }
        - { name: city, in: query, required: false, schema: { type: string } }
      responses:
        '200': { description: Carrier metadata, content: { application/json: { schema: { type: object } } } }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/RateLimited' }

  # ===========================================================
  # Central Transport Locations
  # ===========================================================
  /api/ct-location:
    get:
      tags: [Central Transport Locations]
      summary: Get Central Transport location by ZIP
      operationId: ctLocationByZip
      parameters:
        - { name: zip, in: query, required: true, schema: { type: string, pattern: '^\d{5}$' } }
        - { name: city, in: query, required: false, schema: { type: string } }
      responses:
        '200': { description: CT location(s), content: { application/json: { schema: { $ref: '#/components/schemas/CarrierLocationListResponse' } } } }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /api/ct-location/search:
    get:
      tags: [Central Transport Locations]
      summary: Search Central Transport locations
      operationId: ctLocationSearch
      parameters:
        - { name: q, in: query, required: true, schema: { type: string, minLength: 2 } }
        - { name: page, in: query, required: false, schema: { type: integer, default: 1, minimum: 1 } }
      responses:
        '200': { description: Paginated CT search results, content: { application/json: { schema: { $ref: '#/components/schemas/CarrierLocationListResponse' } } } }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /api/ct-location/terminal/{num}:
    get:
      tags: [Central Transport Locations]
      summary: Get Central Transport locations by terminal number
      operationId: ctLocationByTerminal
      parameters:
        - { name: num, in: path, required: true, schema: { type: string } }
        - { name: page, in: query, required: false, schema: { type: integer, default: 1, minimum: 1 } }
      responses:
        '200': { description: Terminal locations, content: { application/json: { schema: { $ref: '#/components/schemas/CarrierLocationListResponse' } } } }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /api/ct-location/batch:
    post:
      tags: [Central Transport Locations]
      summary: Batch Central Transport lookup by ZIPs
      operationId: ctLocationBatch
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ZipBatchRequest' }
      responses:
        '200': { description: Batch results, content: { application/json: { schema: { $ref: '#/components/schemas/BatchResponse' } } } }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /api/ct-location/coverage:
    get:
      tags: [Central Transport Locations]
      summary: Central Transport coverage statistics
      operationId: ctCoverage
      parameters:
        - { name: state, in: query, required: true, schema: { type: string, pattern: '^[A-Za-z]{2}$' } }
      responses:
        '200': { description: Coverage stats, content: { application/json: { schema: { $ref: '#/components/schemas/CoverageResponse' } } } }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/RateLimited' }

  # ===========================================================
  # CEVA Locations
  # ===========================================================
  /api/ceva-location:
    get:
      tags: [CEVA Locations]
      summary: Get CEVA locations by country code
      operationId: cevaLocationByCountry
      parameters:
        - { name: country_code, in: query, required: true, schema: { type: string, pattern: '^[A-Za-z]{2,3}$' }, example: "US" }
        - { name: city, in: query, required: false, schema: { type: string } }
      responses:
        '200': { description: CEVA locations, content: { application/json: { schema: { $ref: '#/components/schemas/CarrierLocationListResponse' } } } }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /api/ceva-location/search:
    get:
      tags: [CEVA Locations]
      summary: Search CEVA locations
      operationId: cevaLocationSearch
      parameters:
        - { name: q, in: query, required: true, schema: { type: string, minLength: 2 } }
        - { name: page, in: query, required: false, schema: { type: integer, default: 1, minimum: 1 } }
      responses:
        '200': { description: Paginated CEVA search results, content: { application/json: { schema: { $ref: '#/components/schemas/CarrierLocationListResponse' } } } }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /api/ceva-location/country/{code}:
    get:
      tags: [CEVA Locations]
      summary: Get all CEVA locations in a country
      operationId: cevaLocationsByCountryCode
      parameters:
        - { name: code, in: path, required: true, schema: { type: string, pattern: '^[A-Za-z]{2,3}$' } }
      responses:
        '200': { description: Locations in country, content: { application/json: { schema: { $ref: '#/components/schemas/CarrierLocationListResponse' } } } }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /api/ceva-location/batch:
    post:
      tags: [CEVA Locations]
      summary: Batch CEVA lookup by country codes
      operationId: cevaLocationBatch
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [country_codes]
              properties:
                country_codes:
                  type: array
                  items: { type: string, pattern: '^[A-Za-z]{2,3}$' }
                  maxItems: 50
      responses:
        '200': { description: Batch results, content: { application/json: { schema: { $ref: '#/components/schemas/BatchResponse' } } } }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /api/ceva-location/coverage:
    get:
      tags: [CEVA Locations]
      summary: CEVA coverage by continent
      operationId: cevaCoverage
      parameters:
        - { name: continent, in: query, required: false, schema: { type: string } }
      responses:
        '200': { description: Coverage stats, content: { application/json: { schema: { $ref: '#/components/schemas/CoverageResponse' } } } }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /api/ceva-location/nearby:
    get:
      tags: [CEVA Locations]
      summary: CEVA locations within radius of a coordinate
      operationId: cevaNearby
      parameters:
        - { name: lat, in: query, required: true, schema: { type: number, minimum: -90, maximum: 90 } }
        - { name: lng, in: query, required: true, schema: { type: number, minimum: -180, maximum: 180 } }
        - { name: radius, in: query, required: false, schema: { type: integer, default: 50, minimum: 1, maximum: 500 }, description: Radius in kilometers }
        - { name: page, in: query, required: false, schema: { type: integer, default: 1, minimum: 1 } }
      responses:
        '200': { description: Nearby locations, content: { application/json: { schema: { $ref: '#/components/schemas/CarrierLocationListResponse' } } } }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /api/ceva-location/activities:
    get:
      tags: [CEVA Locations]
      summary: CEVA locations by activity type
      operationId: cevaByActivity
      parameters:
        - name: type
          in: query
          required: true
          schema:
            type: string
            enum: [AIR, "CONTRACT LOGISTICS", FVL, GROUND, "LEAD LOGISTICS", OCEAN, RAIL, VAS]
        - { name: page, in: query, required: false, schema: { type: integer, default: 1, minimum: 1 } }
      responses:
        '200': { description: Locations by activity, content: { application/json: { schema: { $ref: '#/components/schemas/CarrierLocationListResponse' } } } }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /api/ceva-location/contacts:
    get:
      tags: [CEVA Locations]
      summary: CEVA country-level contacts
      operationId: cevaContacts
      parameters:
        - { name: country_code, in: query, required: true, schema: { type: string, pattern: '^[A-Za-z]{2,3}$' } }
      responses:
        '200': { description: Country contacts, content: { application/json: { schema: { type: object } } } }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/RateLimited' }

  # ===========================================================
  # C.H. Robinson Locations
  # ===========================================================
  /api/chrobinson-location:
    get:
      tags: [C.H. Robinson Locations]
      summary: Get C.H. Robinson locations by region
      operationId: chrobinsonByRegion
      parameters:
        - name: region
          in: query
          required: true
          schema: { type: integer, enum: [1, 2, 3, 4, 5, 6, 7, 9] }
          description: Region ID
        - name: category
          in: query
          required: false
          schema:
            type: string
            enum:
              - "Surface Transportation & Logistics"
              - "Global Forwarding"
              - "Corporate"
              - "Robinson Fresh"
              - "Robinson Managed Solutions"
      responses:
        '200': { description: Locations in region, content: { application/json: { schema: { $ref: '#/components/schemas/CarrierLocationListResponse' } } } }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /api/chrobinson-location/search:
    get:
      tags: [C.H. Robinson Locations]
      summary: Search C.H. Robinson locations
      operationId: chrobinsonSearch
      parameters:
        - { name: q, in: query, required: true, schema: { type: string, minLength: 2 } }
        - { name: page, in: query, required: false, schema: { type: integer, default: 1, minimum: 1 } }
      responses:
        '200': { description: Paginated search results, content: { application/json: { schema: { $ref: '#/components/schemas/CarrierLocationListResponse' } } } }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /api/chrobinson-location/category/{name}:
    get:
      tags: [C.H. Robinson Locations]
      summary: Get C.H. Robinson locations by category
      operationId: chrobinsonByCategory
      parameters:
        - { name: name, in: path, required: true, schema: { type: string }, description: URL-encoded category name }
        - { name: page, in: query, required: false, schema: { type: integer, default: 1, minimum: 1 } }
      responses:
        '200': { description: Locations in category, content: { application/json: { schema: { $ref: '#/components/schemas/CarrierLocationListResponse' } } } }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /api/chrobinson-location/batch:
    post:
      tags: [C.H. Robinson Locations]
      summary: Batch C.H. Robinson lookup by region IDs
      operationId: chrobinsonBatch
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [region_ids]
              properties:
                region_ids:
                  type: array
                  items: { type: integer, enum: [1, 2, 3, 4, 5, 6, 7, 9] }
      responses:
        '200': { description: Batch results, content: { application/json: { schema: { $ref: '#/components/schemas/BatchResponse' } } } }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /api/chrobinson-location/coverage:
    get:
      tags: [C.H. Robinson Locations]
      summary: C.H. Robinson coverage statistics
      operationId: chrobinsonCoverage
      responses:
        '200': { description: Coverage stats, content: { application/json: { schema: { $ref: '#/components/schemas/CoverageResponse' } } } }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/RateLimited' }

  # ===========================================================
  # Estes Locations
  # ===========================================================
  /api/estes-location:
    get:
      tags: [Estes Locations]
      summary: Get Estes locations by state or ZIP
      description: Either `state` or `zip` must be provided.
      operationId: estesLocations
      parameters:
        - { name: state, in: query, required: false, schema: { type: string, pattern: '^[A-Za-z]{2}$' } }
        - { name: zip, in: query, required: false, schema: { type: string, pattern: '^\d{5}$' } }
      responses:
        '200': { description: Estes locations, content: { application/json: { schema: { $ref: '#/components/schemas/CarrierLocationListResponse' } } } }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /api/estes-location/search:
    get:
      tags: [Estes Locations]
      summary: Search Estes locations
      operationId: estesSearch
      parameters:
        - { name: q, in: query, required: true, schema: { type: string, minLength: 2 } }
        - { name: page, in: query, required: false, schema: { type: integer, default: 1, minimum: 1 } }
      responses:
        '200': { description: Paginated search results, content: { application/json: { schema: { $ref: '#/components/schemas/CarrierLocationListResponse' } } } }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /api/estes-location/terminal/{code}:
    get:
      tags: [Estes Locations]
      summary: Get Estes terminal by alpha code
      operationId: estesTerminalByCode
      parameters:
        - { name: code, in: path, required: true, schema: { type: string, pattern: '^[A-Za-z]{2,4}$' }, example: "RIC" }
      responses:
        '200': { description: Terminal details, content: { application/json: { schema: { $ref: '#/components/schemas/CarrierLocationDetailResponse' } } } }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /api/estes-location/batch:
    post:
      tags: [Estes Locations]
      summary: Batch Estes lookup by states
      operationId: estesBatch
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/StateBatchRequest' }
      responses:
        '200': { description: Batch results, content: { application/json: { schema: { $ref: '#/components/schemas/BatchResponse' } } } }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /api/estes-location/coverage:
    get:
      tags: [Estes Locations]
      summary: Estes coverage statistics
      operationId: estesCoverage
      responses:
        '200': { description: Coverage stats, content: { application/json: { schema: { $ref: '#/components/schemas/CoverageResponse' } } } }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/RateLimited' }

  # ===========================================================
  # GLS Locations
  # ===========================================================
  /api/gls-location:
    get:
      tags: [GLS Locations]
      summary: Get GLS dropboxes by state or ZIP
      description: Either `state` or `zip` must be provided.
      operationId: glsLocations
      parameters:
        - { name: state, in: query, required: false, schema: { type: string, pattern: '^[A-Za-z]{2}$' } }
        - { name: zip, in: query, required: false, schema: { type: string, pattern: '^\d{5}$' } }
      responses:
        '200': { description: GLS dropboxes, content: { application/json: { schema: { $ref: '#/components/schemas/CarrierLocationListResponse' } } } }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /api/gls-location/search:
    get:
      tags: [GLS Locations]
      summary: Search GLS dropboxes
      operationId: glsSearch
      parameters:
        - { name: q, in: query, required: true, schema: { type: string, minLength: 2 } }
        - { name: page, in: query, required: false, schema: { type: integer, default: 1, minimum: 1 } }
      responses:
        '200': { description: Paginated search results, content: { application/json: { schema: { $ref: '#/components/schemas/CarrierLocationListResponse' } } } }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /api/gls-location/batch:
    post:
      tags: [GLS Locations]
      summary: Batch GLS lookup by states
      operationId: glsBatch
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/StateBatchRequest' }
      responses:
        '200': { description: Batch results, content: { application/json: { schema: { $ref: '#/components/schemas/BatchResponse' } } } }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /api/gls-location/coverage:
    get:
      tags: [GLS Locations]
      summary: GLS coverage statistics
      operationId: glsCoverage
      responses:
        '200': { description: Coverage stats, content: { application/json: { schema: { $ref: '#/components/schemas/CoverageResponse' } } } }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/RateLimited' }

  # ===========================================================
  # SAIA Locations
  # ===========================================================
  /api/saia-location:
    get:
      tags: [SAIA Locations]
      summary: Get SAIA locations by state or ZIP
      description: Either `state` or `zip` must be provided.
      operationId: saiaLocations
      parameters:
        - { name: state, in: query, required: false, schema: { type: string, pattern: '^[A-Za-z]{2}$' } }
        - { name: zip, in: query, required: false, schema: { type: string, pattern: '^\d{5}$' } }
      responses:
        '200': { description: SAIA locations, content: { application/json: { schema: { $ref: '#/components/schemas/CarrierLocationListResponse' } } } }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /api/saia-location/search:
    get:
      tags: [SAIA Locations]
      summary: Search SAIA locations
      operationId: saiaSearch
      parameters:
        - { name: q, in: query, required: true, schema: { type: string, minLength: 2 } }
        - { name: page, in: query, required: false, schema: { type: integer, default: 1, minimum: 1 } }
      responses:
        '200': { description: Paginated search results, content: { application/json: { schema: { $ref: '#/components/schemas/CarrierLocationListResponse' } } } }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /api/saia-location/terminal/{code}:
    get:
      tags: [SAIA Locations]
      summary: Get SAIA terminal by alpha code
      operationId: saiaTerminalByCode
      parameters:
        - { name: code, in: path, required: true, schema: { type: string, pattern: '^[A-Za-z]{2,4}$' } }
      responses:
        '200': { description: Terminal details, content: { application/json: { schema: { $ref: '#/components/schemas/CarrierLocationDetailResponse' } } } }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /api/saia-location/batch:
    post:
      tags: [SAIA Locations]
      summary: Batch SAIA lookup by states
      operationId: saiaBatch
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/StateBatchRequest' }
      responses:
        '200': { description: Batch results, content: { application/json: { schema: { $ref: '#/components/schemas/BatchResponse' } } } }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /api/saia-location/coverage:
    get:
      tags: [SAIA Locations]
      summary: SAIA coverage statistics
      operationId: saiaCoverage
      responses:
        '200': { description: Coverage stats, content: { application/json: { schema: { $ref: '#/components/schemas/CoverageResponse' } } } }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/RateLimited' }

  # ===========================================================
  # Seko Locations
  # ===========================================================
  /api/seko-location:
    get:
      tags: [Seko Locations]
      summary: Get Seko locations by country
      operationId: sekoByCountry
      parameters:
        - { name: country, in: query, required: true, schema: { type: string } }
      responses:
        '200': { description: Seko locations, content: { application/json: { schema: { $ref: '#/components/schemas/CarrierLocationListResponse' } } } }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /api/seko-location/search:
    get:
      tags: [Seko Locations]
      summary: Search Seko locations
      operationId: sekoSearch
      parameters:
        - { name: q, in: query, required: true, schema: { type: string, minLength: 2 } }
        - { name: page, in: query, required: false, schema: { type: integer, default: 1, minimum: 1 } }
      responses:
        '200': { description: Paginated search results, content: { application/json: { schema: { $ref: '#/components/schemas/CarrierLocationListResponse' } } } }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /api/seko-location/hubs:
    get:
      tags: [Seko Locations]
      summary: Seko global hub locations
      operationId: sekoHubs
      responses:
        '200': { description: Hub locations, content: { application/json: { schema: { $ref: '#/components/schemas/CarrierLocationListResponse' } } } }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /api/seko-location/batch:
    post:
      tags: [Seko Locations]
      summary: Batch Seko lookup by countries
      operationId: sekoBatch
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [countries]
              properties:
                countries:
                  type: array
                  items: { type: string }
                  maxItems: 10
      responses:
        '200': { description: Batch results, content: { application/json: { schema: { $ref: '#/components/schemas/BatchResponse' } } } }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /api/seko-location/coverage:
    get:
      tags: [Seko Locations]
      summary: Seko coverage statistics
      operationId: sekoCoverage
      responses:
        '200': { description: Coverage stats, content: { application/json: { schema: { $ref: '#/components/schemas/CoverageResponse' } } } }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/RateLimited' }

  # ===========================================================
  # TForce Locations
  # ===========================================================
  /api/tforce-location:
    get:
      tags: [TForce Locations]
      summary: Get TForce locations by state, ZIP or country
      description: One of `state`, `zip`, or `country` is required.
      operationId: tforceLocations
      parameters:
        - { name: state, in: query, required: false, schema: { type: string, pattern: '^[A-Za-z]{2}$' } }
        - { name: zip, in: query, required: false, schema: { type: string, pattern: '^\d{5}$' } }
        - { name: country, in: query, required: false, schema: { type: string } }
      responses:
        '200': { description: TForce locations, content: { application/json: { schema: { $ref: '#/components/schemas/CarrierLocationListResponse' } } } }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /api/tforce-location/search:
    get:
      tags: [TForce Locations]
      summary: Search TForce locations
      operationId: tforceSearch
      parameters:
        - { name: q, in: query, required: true, schema: { type: string, minLength: 2 } }
        - { name: page, in: query, required: false, schema: { type: integer, default: 1, minimum: 1 } }
      responses:
        '200': { description: Paginated search results, content: { application/json: { schema: { $ref: '#/components/schemas/CarrierLocationListResponse' } } } }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /api/tforce-location/terminal/{code}:
    get:
      tags: [TForce Locations]
      summary: Get TForce terminal by alpha code
      operationId: tforceTerminalByCode
      parameters:
        - { name: code, in: path, required: true, schema: { type: string } }
      responses:
        '200': { description: Terminal details, content: { application/json: { schema: { $ref: '#/components/schemas/CarrierLocationDetailResponse' } } } }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /api/tforce-location/batch:
    post:
      tags: [TForce Locations]
      summary: Batch TForce lookup by states
      operationId: tforceBatch
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/StateBatchRequest' }
      responses:
        '200': { description: Batch results, content: { application/json: { schema: { $ref: '#/components/schemas/BatchResponse' } } } }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /api/tforce-location/coverage:
    get:
      tags: [TForce Locations]
      summary: TForce coverage statistics
      operationId: tforceCoverage
      responses:
        '200': { description: Coverage stats, content: { application/json: { schema: { $ref: '#/components/schemas/CoverageResponse' } } } }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/RateLimited' }

  # ===========================================================
  # USPS Locations
  # ===========================================================
  /api/usps-location:
    get:
      tags: [USPS Locations]
      summary: Get USPS locations by state, ZIP or city
      description: Either `state` or `zip` is required. `city` is optional and combines with the others.
      operationId: uspsLocations
      parameters:
        - { name: state, in: query, required: false, schema: { type: string, pattern: '^[A-Za-z]{2}$' } }
        - { name: zip, in: query, required: false, schema: { type: string, pattern: '^\d{5}$' } }
        - { name: city, in: query, required: false, schema: { type: string } }
      responses:
        '200': { description: USPS locations, content: { application/json: { schema: { $ref: '#/components/schemas/CarrierLocationListResponse' } } } }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /api/usps-location/search:
    get:
      tags: [USPS Locations]
      summary: Search USPS locations
      operationId: uspsSearch
      parameters:
        - { name: q, in: query, required: true, schema: { type: string, minLength: 2 } }
        - { name: page, in: query, required: false, schema: { type: integer, default: 1, minimum: 1 } }
      responses:
        '200': { description: Paginated search results, content: { application/json: { schema: { $ref: '#/components/schemas/CarrierLocationListResponse' } } } }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /api/usps-location/nearby:
    get:
      tags: [USPS Locations]
      summary: USPS locations within radius of a coordinate
      operationId: uspsNearby
      parameters:
        - { name: lat, in: query, required: true, schema: { type: number, minimum: -90, maximum: 90 } }
        - { name: lng, in: query, required: true, schema: { type: number, minimum: -180, maximum: 180 } }
        - { name: radius, in: query, required: false, schema: { type: integer, default: 10, minimum: 0, maximum: 100 }, description: Radius in miles }
        - { name: page, in: query, required: false, schema: { type: integer, default: 1, minimum: 1 } }
      responses:
        '200': { description: Nearby USPS locations with distance, content: { application/json: { schema: { $ref: '#/components/schemas/CarrierLocationListResponse' } } } }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /api/usps-location/batch:
    post:
      tags: [USPS Locations]
      summary: Batch USPS lookup by states
      operationId: uspsBatch
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/StateBatchRequest' }
      responses:
        '200': { description: Batch results, content: { application/json: { schema: { $ref: '#/components/schemas/BatchResponse' } } } }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /api/usps-location/coverage:
    get:
      tags: [USPS Locations]
      summary: USPS coverage statistics
      operationId: uspsCoverage
      responses:
        '200': { description: Coverage stats, content: { application/json: { schema: { $ref: '#/components/schemas/CoverageResponse' } } } }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /api/ports:
    get:
      tags: [Seaports]
      summary: List seaports by country
      description: Returns all ports for a country. Available on all plans.
      operationId: portsByCountry
      parameters:
        - { name: country, in: query, required: true, schema: { type: string, pattern: '^[A-Za-z]{2}$' }, description: 2-letter country code (e.g. US, CN, NL) }
      responses:
        '200': { description: Ports in the country, content: { application/json: { schema: { $ref: '#/components/schemas/PortListResponse' } } } }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /api/ports/search:
    get:
      tags: [Seaports]
      summary: Search seaports by name
      description: Free-text port-name search. Available on all plans.
      operationId: portsSearch
      parameters:
        - { name: q, in: query, required: true, schema: { type: string, minLength: 2 } }
        - { name: page, in: query, required: false, schema: { type: integer, default: 1, minimum: 1 } }
      responses:
        '200': { description: Paginated search results, content: { application/json: { schema: { $ref: '#/components/schemas/PortListResponse' } } } }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /api/ports/by-company:
    get:
      tags: [Seaports]
      summary: List seaports served by a shipping company
      description: Ports served by a given carrier (e.g. COSCO, CMA CGM). Available on all plans.
      operationId: portsByCompany
      parameters:
        - { name: company, in: query, required: true, schema: { type: string }, description: Shipping company name (e.g. COSCO) }
        - { name: page, in: query, required: false, schema: { type: integer, default: 1, minimum: 1 } }
      responses:
        '200': { description: Paginated ports for the company, content: { application/json: { schema: { $ref: '#/components/schemas/PortListResponse' } } } }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /api/ports/nearby:
    get:
      tags: [Seaports]
      summary: Seaports within radius of a coordinate
      description: Ports within a geographic radius, sorted by distance. Available on all plans.
      operationId: portsNearby
      parameters:
        - { name: lat, in: query, required: true, schema: { type: number, minimum: -90, maximum: 90 } }
        - { name: lng, in: query, required: true, schema: { type: number, minimum: -180, maximum: 180 } }
        - { name: radius, in: query, required: false, schema: { type: integer, default: 100, minimum: 0, maximum: 1000 }, description: Radius in miles }
        - { name: page, in: query, required: false, schema: { type: integer, default: 1, minimum: 1 } }
      responses:
        '200': { description: Nearby ports with distance, content: { application/json: { schema: { $ref: '#/components/schemas/PortListResponse' } } } }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /api/ports/{unlocode}:
    get:
      tags: [Seaports]
      summary: Get a single seaport by UN/LOCODE
      description: Returns one port by its 5-character UN/LOCODE (e.g. CNSHA, NLROT). Available on all plans.
      operationId: portByUnlocode
      parameters:
        - { name: unlocode, in: path, required: true, schema: { type: string, pattern: '^[A-Za-z]{2}[A-Za-z0-9]{3}$' }, description: UN/LOCODE (e.g. CNSHA) }
      responses:
        '200': { description: Port detail, content: { application/json: { schema: { $ref: '#/components/schemas/PortResponse' } } } }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { description: Port not found }
        '429': { $ref: '#/components/responses/RateLimited' }

  /api/ports/batch:
    post:
      tags: [Seaports]
      summary: Batch seaport lookup by UN/LOCODE
      description: Look up multiple ports by UN/LOCODE in one call. Available on all plans.
      operationId: portsBatch
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/PortBatchRequest' }
      responses:
        '200': { description: Batch results keyed by UN/LOCODE, content: { application/json: { schema: { $ref: '#/components/schemas/PortBatchResponse' } } } }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/RateLimited' }

components:
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-API-Key
      description: |
        API key issued from your FreightAPI account page.
        When proxied through RapidAPI, RapidAPI will inject the `X-RapidAPI-Key`
        and `X-RapidAPI-Proxy-Secret` headers automatically.

  responses:
    BadRequest:
      description: Invalid request body or parameters
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    Unauthorized:
      description: Missing or invalid API key
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    PlanRequired:
      description: Endpoint requires a Pro subscription
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    NotFound:
      description: Resource not found
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    RateLimited:
      description: Monthly quota exceeded
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'

  schemas:
    ErrorResponse:
      type: object
      properties:
        error:
          type: string
          example: "Resource not found"

    # ===== Search =====
    SearchRequest:
      type: object
      required: [q]
      properties:
        q:
          type: string
          minLength: 3
          description: Search query (minimum 3 characters)
          example: "steel pipe"
        page:
          type: integer
          default: 1
          minimum: 1
        scopes:
          type: array
          items:
            type: string
            enum: [article, index, all]
          default: [article]
          description: |
            Which corpora to search. `article` = NMFC articles only,
            `index` = alphabetical index only, `all` = both.

    SearchResponse:
      type: object
      properties:
        success:
          type: boolean
        article:
          type: object
          properties:
            articles:
              type: array
              items:
                $ref: '#/components/schemas/ArticleSearchResult'
            totalMatch:
              type: integer
        index:
          type: object
          properties:
            articleIndexes:
              type: array
              items:
                type: object
                properties:
                  keywords:
                    type: array
                    items: { type: string }
                  articleNumbers:
                    type: array
                    items: { type: string }
                  isHeading: { type: boolean }
            totalMatch:
              type: integer
        totalPages:
          type: integer
        currentPage:
          type: integer

    ArticleSearchResult:
      type: object
      properties:
        articleNumber: { type: string }
        description: { type: string }
        grouping: { type: string }
        classification: { type: string }

    # ===== Article =====
    ArticleResponse:
      type: object
      properties:
        success: { type: boolean }
        article:
          $ref: '#/components/schemas/Article'

    Article:
      type: object
      properties:
        articleNumber: { type: string, example: "156600" }
        description: { type: string }
        grouping: { type: string }
        subGrouping: { type: string }
        tariffIssue: { type: string }
        supplement: { type: string }
        classification: { type: string, example: "70" }
        isHazardous: { type: boolean }
        isSpecialHandling: { type: boolean }
        isCanceled: { type: boolean }
        canceledReference: { type: string, nullable: true }
        noteReferences:
          type: array
          items: { type: string }
        articleSubItems:
          type: array
          items:
            type: object
            properties:
              item: { type: string }
              description: { type: string }
              classification: { type: string }

    # ===== Rule =====
    RuleResponse:
      type: object
      properties:
        success: { type: boolean }
        rule:
          type: object
          properties:
            ruleNumber: { type: string }
            ruleHeader: { type: string }
            ruleSubHeading: { type: string }
            ruleDescriptions:
              type: array
              items:
                $ref: '#/components/schemas/Description'

    # ===== Package =====
    PackageResponse:
      type: object
      properties:
        success: { type: boolean }
        package:
          type: object
          properties:
            packageNumber: { type: string }
            packageHeader: { type: string }
            tariffIssue: { type: string }
            packageDescriptions:
              type: array
              items:
                $ref: '#/components/schemas/Description'

    Description:
      type: object
      properties:
        type: { type: string }
        title: { type: string }
        text: { type: string }

    # ===== History =====
    HistoryResponse:
      type: object
      properties:
        success: { type: boolean }
        articleNumber: { type: string }
        articleChangeHistoryDetails:
          type: array
          items:
            type: object
            properties:
              docket: { type: string }
              issuedDate: { type: string }
              subject: { type: string }
              quickView: { type: string }
              changeDetails:
                type: array
                items:
                  type: object
                  properties:
                    description: { type: string }
                    typeOfProvision: { type: string }
                    item: { type: string }
                    alternateItem: { type: string }
                    action: { type: string }

    # ===== Density =====
    DensityRequest:
      type: object
      required: [unitDimensions]
      properties:
        dimensionType:
          type: string
          enum: [Inch, Feet, Centimeter, Meter]
          default: Inch
        weightType:
          type: string
          enum: [Pound, Kilogram]
          default: Pound
        resultUnitType:
          type: string
          enum: [PoundsPerCubicFeet, KilogramPerCubicMeter]
          default: PoundsPerCubicFeet
        unitDimensions:
          type: array
          minItems: 1
          items:
            type: object
            required: [HandlingUnitCount, length, width, height, handlingUnitWeight]
            properties:
              HandlingUnitCount: { type: integer, example: 1 }
              length: { type: number, example: 48 }
              width: { type: number, example: 40 }
              height: { type: number, example: 36 }
              handlingUnitWeight: { type: number, example: 500 }

    DensityResponse:
      type: object
      properties:
        success: { type: boolean }
        unitDimensions:
          type: array
          items:
            type: object
            properties:
              handlingUnitCount: { type: integer }
              length: { type: number }
              width: { type: number }
              height: { type: number }
              handlingUnitCountVolume: { type: number }
              handlingUnitWeight: { type: number }
              density: { type: number }
        totalHandlingUnitCount: { type: integer }
        totalHandlingUnitCountVolume: { type: number }
        totalHandlingUnitWeight: { type: number }
        totalDensity: { type: number }

    # ===== Item 680 =====
    Dimensions3D:
      type: object
      required: [length, width, height]
      properties:
        length: { type: number }
        width: { type: number }
        height: { type: number }

    Dimensions2D:
      type: object
      required: [length, width]
      properties:
        length: { type: number }
        width: { type: number }

    Item680Note2ARequest:
      type: object
      required: [commodity, package]
      properties:
        commodity:
          $ref: '#/components/schemas/Dimensions3D'
        package:
          $ref: '#/components/schemas/Dimensions3D'

    Item680Note2AResponse:
      type: object
      properties:
        success: { type: boolean }
        commodityVolume: { type: number }
        packageVolume: { type: number }
        percentageVolumeOccupiedByCommodity: { type: number }
        isBelowThreshold: { type: boolean }

    Item680Note2BRequest:
      type: object
      required: [commodity, handlingPlatformType]
      properties:
        commodity:
          $ref: '#/components/schemas/Dimensions2D'
        handlingPlatformType:
          $ref: '#/components/schemas/Dimensions2D'

    Item680Note2BResponse:
      type: object
      properties:
        success: { type: boolean }
        commodityArea: { type: number }
        liftArea: { type: number }
        percentageSurfaceAreaOccupiedByCommodity: { type: number }
        isBelowThreshold: { type: boolean }

    Item680Note2CRequest:
      type: object
      required: [length, width, height, liftLength, liftWidth]
      properties:
        length: { type: number }
        width: { type: number }
        height: { type: number }
        liftLength: { type: number }
        liftWidth: { type: number }

    Item680Note2CResponse:
      type: object
      properties:
        success: { type: boolean }
        ratio: { type: number }
        isAboveThreshold: { type: boolean }
        maximumAllowableCalculations:
          type: object
          properties:
            maxCommodityHeight: { type: number }
            maxAllowableWidthHeight: { type: number }
            maxCommodityLength: { type: number }
            maxCommodityWidth: { type: number }
            maxAllowableLength: { type: number }
            maxAllowableWidth: { type: number }

    # ===== Drop Points =====
    DropPoint:
      type: object
      properties:
        accessPointId: { type: string }
        name: { type: string, example: "Hollywood Post Office" }
        chainStoreId: { type: string, nullable: true }
        address:
          type: object
          properties:
            line1: { type: string }
            city: { type: string }
            state: { type: string }
            zipCode: { type: string }
            country: { type: string }
        phone: { type: string, nullable: true }
        location:
          type: object
          properties:
            latitude: { type: number }
            longitude: { type: number }
        timezone: { type: string, nullable: true }
        operatingHours:
          type: object
          nullable: true
        email: { type: string, nullable: true }
        type: { type: string, example: "post_office" }
        isStaffAssisted: { type: boolean }
        isFeatured: { type: boolean }
        tags:
          type: array
          items: { type: string }

    DropPointsListResponse:
      type: object
      properties:
        success: { type: boolean }
        zip: { type: string }
        dropPoints:
          type: array
          items:
            $ref: '#/components/schemas/DropPoint'
        total: { type: integer }
        currentPage: { type: integer }
        totalPages: { type: integer }

    DropPointsSearchResponse:
      type: object
      properties:
        success: { type: boolean }
        query: { type: string }
        dropPoints:
          type: array
          items:
            $ref: '#/components/schemas/DropPoint'
        total: { type: integer }
        currentPage: { type: integer }
        totalPages: { type: integer }

    DropPointDetailResponse:
      type: object
      properties:
        success: { type: boolean }
        dropPoint:
          $ref: '#/components/schemas/DropPoint'

    # ===== Carrier Locations (shared) =====
    CarrierLocation:
      type: object
      description: |
        A carrier-network location (LTL terminal, freight office, dropbox, retail
        post office, etc.). Field shape varies slightly between carriers; common
        fields are listed below and carrier-specific extras (SCAC, alpha code,
        region ID, activity types, hours, distance, etc.) appear as additional
        properties on the same object.
      additionalProperties: true
      properties:
        name: { type: string, example: "Estes Express - Richmond" }
        carrier: { type: string, example: "Estes" }
        address:
          type: object
          properties:
            line1: { type: string }
            line2: { type: string, nullable: true }
            city: { type: string }
            state: { type: string }
            zipCode: { type: string }
            country: { type: string }
        phone: { type: string, nullable: true }
        fax: { type: string, nullable: true }
        email: { type: string, nullable: true }
        location:
          type: object
          properties:
            latitude: { type: number }
            longitude: { type: number }
        terminalCode: { type: string, nullable: true, description: Alpha or numeric carrier-specific code }
        scac: { type: string, nullable: true }

    CarrierLocationListResponse:
      type: object
      properties:
        success: { type: boolean }
        locations:
          type: array
          items: { $ref: '#/components/schemas/CarrierLocation' }
        total: { type: integer }
        currentPage: { type: integer }
        totalPages: { type: integer }

    CarrierLocationDetailResponse:
      type: object
      properties:
        success: { type: boolean }
        location: { $ref: '#/components/schemas/CarrierLocation' }

    CoverageResponse:
      type: object
      description: Coverage statistics. Field shape varies per carrier.
      additionalProperties: true
      properties:
        success: { type: boolean }
        totalLocations: { type: integer }
        states:
          type: array
          items:
            type: object
            additionalProperties: true
        countries:
          type: array
          items:
            type: object
            additionalProperties: true

    ZipBatchRequest:
      type: object
      required: [zips]
      properties:
        zips:
          type: array
          description: 5-digit US ZIP codes. Limit is 20 per call on Pro and 50 on Enterprise.
          items: { type: string, pattern: '^\d{5}$' }
          maxItems: 50
          example: ["72204", "90001", "10001"]

    StateBatchRequest:
      type: object
      required: [states]
      properties:
        states:
          type: array
          description: 2-letter US state codes. Maximum 10 per call.
          items: { type: string, pattern: '^[A-Za-z]{2}$' }
          maxItems: 10
          example: ["CA", "NY", "TX"]

    BatchResponse:
      type: object
      description: Result of a batch lookup. Each input key is echoed back with its matching locations.
      properties:
        success: { type: boolean }
        results:
          type: object
          additionalProperties:
            type: array
            items: { $ref: '#/components/schemas/CarrierLocation' }
        total: { type: integer }

    Port:
      type: object
      description: A seaport, keyed internationally by UN/LOCODE.
      properties:
        name: { type: string, example: Shanghai }
        unlocode: { type: string, nullable: true, example: CNSHA, description: UN/LOCODE (5 chars) }
        countryCode: { type: string, example: CN }
        countryName: { type: string, nullable: true, example: China }
        lat: { type: number, nullable: true, example: 31.4000 }
        lng: { type: number, nullable: true, example: 121.4971 }
        transshipment: { type: boolean, description: Whether the port is a transshipment hub }
        companies:
          type: array
          description: Shipping companies serving this port.
          items: { type: string }
          example: ["COSCO", "EVERGREEN", "CMA CGM"]
        distanceMiles:
          type: number
          nullable: true
          description: Distance from the query coordinate (only on /nearby responses).

    PortResponse:
      type: object
      properties:
        success: { type: boolean }
        port: { $ref: '#/components/schemas/Port' }

    PortListResponse:
      type: object
      properties:
        success: { type: boolean }
        total: { type: integer }
        currentPage: { type: integer }
        totalPages: { type: integer }
        ports:
          type: array
          items: { $ref: '#/components/schemas/Port' }

    PortBatchRequest:
      type: object
      required: [unlocodes]
      properties:
        unlocodes:
          type: array
          description: UN/LOCODEs. Limit is 20 per call (50 on Enterprise).
          items: { type: string, pattern: '^[A-Za-z]{2}[A-Za-z0-9]{3}$' }
          maxItems: 50
          example: ["CNSHA", "NLROT", "USNYC"]

    PortBatchResponse:
      type: object
      description: Batch port lookup. Each requested UN/LOCODE is echoed back with its port or null.
      properties:
        success: { type: boolean }
        results:
          type: object
          additionalProperties:
            nullable: true
            allOf: [{ $ref: '#/components/schemas/Port' }]
        summary:
          type: object
          properties:
            requested: { type: integer }
            found: { type: integer }
