openapi: 3.1.0
info:
  title: OSOW Quote API
  version: 1.0.0
  summary: Permit, escort and ancillary cost quotes for oversize/overweight loads across the US.
  description: |
    The executable source of truth is `lib/api/contract.ts` (Zod); this file mirrors it
    for tooling and docs. Field names and enum values are frozen for v1.
    Human docs: https://osowloads.com/developers (also https://osowloads.com/developers/llms.txt).

    **Access**: any paid plan (Solo, Team, Enterprise). Bearer API key issued from the dashboard.
    **Billing**: prepaid. Monthly included calls by plan — Solo 30, Team 100, Enterprise 200;
    extra credit packs add 2 API calls per credit (API credits never expire). One successful quote
    = one unit. Failures, replays and `unavailable` results are never charged.
    **Limits**: 200 requests/min and 1,000/day per organization (UTC day). Contact us from
    the dashboard to raise them.
    **Idempotency**: every `POST` requires `idempotency_key`; a retry with the same key
    and body replays the original response at no charge.
servers:
  - url: https://osowloads.com/api/v1
security:
  - bearerAuth: []
tags:
  - name: quotes
paths:
  /quotes:
    post:
      tags: [quotes]
      operationId: createQuote
      summary: Quote one lane for one load (synchronous)
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/QuoteRequest' }
            examples:
              two_state_superload:
                summary: Savannah GA → Birmingham AL, 7-axle superload
                value:
                  idempotency_key: quote-2026-001
                  client_ref: LANE-001
                  origin: "Savannah, GA"
                  destination: "Birmingham, AL"
                  engine: here
                  dims: { width_in: 134, height_in: 161, length_in: 960, weight_lbs: 134075, axles: 7 }
      responses:
        '200':
          description: Quote (or idempotent replay of a prior quote)
          headers:
            X-Usage-Charge: { schema: { type: string, enum: [allowance, lot, none] } }
            X-Usage-Remaining-Allowance: { schema: { type: integer } }
            X-Usage-Remaining-Lot-Credits: { schema: { type: integer } }
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Quote' }
        '401': { $ref: '#/components/responses/Error' }
        '402': { $ref: '#/components/responses/Error' }
        '403': { $ref: '#/components/responses/Error' }
        '409': { $ref: '#/components/responses/Error' }
        '422': { $ref: '#/components/responses/Error' }
        '423': { $ref: '#/components/responses/Error' }
        '429':
          description: Rate limited (per-minute or per-day). `Retry-After` in seconds.
          headers:
            Retry-After: { schema: { type: integer } }
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ApiError' }
        '500': { $ref: '#/components/responses/Error' }
        '503': { $ref: '#/components/responses/Error' }
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: API key from the dashboard (any paid plan). Shown once at creation.
  responses:
    Error:
      description: Error envelope
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ApiError' }
  schemas:
    YCN:
      type: string
      enum: [Yes, CBC, No]
    Qty:
      type: string
      description: '"" | "N" | "N +M CBC" — N firm states, M case-by-case states.'
      pattern: '^$|^\d+$|^\d+ \+\d+ CBC$'
    Engine:
      type: string
      enum: [here, google]
    LatLng:
      type: object
      additionalProperties: false
      required: [lat, lng]
      properties:
        lat: { type: number, minimum: -90, maximum: 90 }
        lng: { type: number, minimum: -180, maximum: 180 }
    Location:
      oneOf:
        - { type: string, minLength: 3, maxLength: 200, pattern: '^[^|]*$', description: 'City, ST or full address; geocoded, resolution echoed in meta.resolved' }
        - { $ref: '#/components/schemas/LatLng' }
    Dims:
      type: object
      additionalProperties: false
      required: [length_in, width_in, height_in, weight_lbs]
      properties:
        length_in: { type: number, minimum: 0, maximum: 6000 }
        width_in: { type: number, minimum: 1, maximum: 1200 }
        height_in: { type: number, minimum: 1, maximum: 600 }
        weight_lbs: { type: number, minimum: 1, maximum: 2000000 }
        overhang_front_in: { type: number, minimum: 0, maximum: 600, default: 0 }
        overhang_rear_in: { type: number, minimum: 0, maximum: 600, default: 0 }
        trailer_length_in:
          type: [number, 'null']
          minimum: 0
          maximum: 6000
          description: Omitted/null → length_in − 240 (20 ft), floored at 0; echoed in assumptions[].
        kingpin_in: { type: number, minimum: 0, maximum: 1200, default: 0 }
        axles: { type: integer, minimum: 5, maximum: 30, description: Required when weight_lbs > 80000. }
    OwInputs:
      type: object
      additionalProperties: false
      description: State-specific overweight inputs. Absent values use documented defaults and are echoed in assumptions[].
      properties:
        wa_excess_lbs: { type: [number, 'null'], minimum: 0, maximum: 2000000 }
        mt_excess_axle_lbs: { type: [number, 'null'], minimum: 0, maximum: 2000000 }
        mn_groups:
          type: array
          maxItems: 12
          items:
            type: object
            additionalProperties: false
            required: [type, weight]
            properties:
              type: { type: string, enum: [tandem, tridem, quad] }
              weight: { type: number, minimum: 0, maximum: 200000 }
        mn_drive_lbs: { type: number, minimum: 0, maximum: 200000 }
        mn_trailer_lbs: { type: number, minimum: 0, maximum: 200000 }
        mn_trailer_type: { type: string, maxLength: 50 }
        il_front_lbs: { type: number, minimum: 0, maximum: 100000 }
        il_rear_lbs: { type: number, minimum: 0, maximum: 100000 }
        ax_steer_kg: { type: number, minimum: 0, maximum: 60000 }
        ax_drive_kg: { type: number, minimum: 0, maximum: 60000 }
        ax_trailer_kg: { type: number, minimum: 0, maximum: 60000 }
    Rates:
      type: object
      additionalProperties: false
      description: |
        Partial rate-card override. Omitted fields fall back to the account's saved Bid Builder
        configuration, EXCEPT service_fee_per_state which defaults to 0. The resolved values
        actually used are returned in meta.rate_card.
      properties:
        escort_dpm: { type: number, minimum: 0, maximum: 50, description: Escort charge in dollars per route mile, per active escort. }
        escort_day_base: { type: number, minimum: 0, maximum: 5000, description: Base retainer per escort per day. }
        escort_day_min: { type: number, minimum: 0, maximum: 5000, description: Minimum an escort earns per day; applied as a true-up when mileage pay falls short. }
        hotel_per_night: { type: number, minimum: 0, maximum: 2000, description: Hotel charge per escort per overnight on multi-day lanes. }
        miles_per_day: { type: number, minimum: 50, maximum: 1500, description: Assumed miles covered per travel day; drives escort days and hotel nights. }
        esl_mode: { type: integer, enum: [0, 1, 2], default: 1, description: 'Escort smoothing across the lane: 0 = Raw (each state exactly as reported), 1 = Conservative (fills interior dips between escort-requiring states), 2 = Full Carry (peak escort count rides the whole lane). Defaults to 1 regardless of the account Bid Builder setting; only an explicit value here changes it.' }
        survey_dpm: { type: number, minimum: 0, maximum: 50, description: Route-survey charge in dollars per mile, per state requiring a survey. }
        survey_minimum: { type: number, minimum: 0, maximum: 5000, description: Minimum survey charge per surveyed state. }
        survey_report_fee: { type: number, minimum: 0, maximum: 5000, description: Flat report fee per surveyed state. }
        service_fee_per_state: { type: number, minimum: 0, maximum: 500, description: Flat service fee multiplied by the number of states crossed. Does NOT inherit from the Bid Builder config; omitted = 0. }
    Options:
      type: object
      additionalProperties: false
      properties:
        return_polyline: { type: boolean, default: false }
        return_tooltips: { type: boolean, default: false }
        alternatives: { type: boolean, default: false, description: Also price HERE's alternate route when one exists (still 1 unit). }
        compare_mi: { type: boolean, default: false, description: Add the other engine's mileage in meta.compare_mi (not billed). }
        avoid_us: { type: boolean, default: false }
        route_preference: { type: integer, minimum: -100, maximum: 100, multipleOf: 5, description: Google only; ignored on HERE with a warning. }
        exclude_states: { type: array, maxItems: 12, items: { type: string, pattern: '^[A-Z]{2}$' }, description: HERE only; hard wall. }
    QuoteRequest:
      type: object
      additionalProperties: false
      required: [idempotency_key, origin, destination, dims]
      properties:
        idempotency_key: { type: string, minLength: 1, maxLength: 128 }
        client_ref: { type: [string, 'null'], maxLength: 200, description: Echoed untouched. }
        metadata: { type: [object, 'null'], propertyNames: { maxLength: 40 }, additionalProperties: { oneOf: [{ type: string, maxLength: 200 }, { type: number }] }, description: 'Echoed untouched. Keys max 40 chars, string values max 200 chars, whole object max 1KB.' }
        origin: { $ref: '#/components/schemas/Location' }
        destination: { $ref: '#/components/schemas/Location' }
        waypoints: { type: array, maxItems: 3, items: { $ref: '#/components/schemas/Location' } }
        vias: { type: array, maxItems: 5, items: { $ref: '#/components/schemas/LatLng' } }
        engine: { allOf: [{ $ref: '#/components/schemas/Engine' }], default: here }
        is_interstate: { type: boolean, default: true, enum: [true], description: 'Deprecated — omit it. Must be true if sent (false returns 422): lanes are always priced on the interstate rulebook, and road-class differences at the endpoints are reported in orig_tier_flag / dest_tier_flag.' }
        dims: { $ref: '#/components/schemas/Dims' }
        ow_inputs: { $ref: '#/components/schemas/OwInputs' }
        options: { $ref: '#/components/schemas/Options' }
        rates: { $ref: '#/components/schemas/Rates' }
    PermitStatus:
      type: string
      enum: [priced, agency_priced, not_permittable, unavailable]
      description: |
        priced — we priced it. agency_priced — the state prices it case-by-case (billed).
        not_permittable — the load as described cannot be permitted there; change the rig (billed).
        unavailable — our data/engine could not answer (NOT billed).
    PermitReason:
      type: string
      enum: [mn_group_over_ceiling, mn_gvw_over_max_for_axles, or_too_few_axles_for_gvw,
             or_over_240k_no_axle_rate, or_gvw_not_in_table, wa_excess_out_of_range,
             gvw_out_of_range, il_matrix_out_of_range, state_system_priced,
             data_unavailable, data_error, no_ow_data, internal_error]
    TooltipBlock:
      type: object
      additionalProperties: false
      required: [bullets, notes, legend]
      properties:
        bullets: { type: array, items: { type: string } }
        notes: { type: [string, 'null'] }
        legend: { type: array, items: { type: string } }
    Tooltips:
      type: object
      additionalProperties: false
      description: Per-load, per-dimension explanations — the same words the dashboard shows on hover.
      required: [escort, police, superload, survey, engineering, utility, alerts, oversize, general]
      properties:
        escort: { oneOf: [{ $ref: '#/components/schemas/TooltipBlock' }, { type: 'null' }] }
        police: { oneOf: [{ $ref: '#/components/schemas/TooltipBlock' }, { type: 'null' }] }
        superload: { oneOf: [{ $ref: '#/components/schemas/TooltipBlock' }, { type: 'null' }] }
        survey: { oneOf: [{ $ref: '#/components/schemas/TooltipBlock' }, { type: 'null' }] }
        engineering: { oneOf: [{ $ref: '#/components/schemas/TooltipBlock' }, { type: 'null' }] }
        utility: { oneOf: [{ $ref: '#/components/schemas/TooltipBlock' }, { type: 'null' }] }
        alerts: { oneOf: [{ $ref: '#/components/schemas/TooltipBlock' }, { type: 'null' }] }
        oversize: { oneOf: [{ $ref: '#/components/schemas/TooltipBlock' }, { type: 'null' }] }
        general: { oneOf: [{ $ref: '#/components/schemas/TooltipBlock' }, { type: 'null' }] }
    StateBlock:
      type: object
      additionalProperties: false
      description: One state on the route, in travel order. Cell names match the RFQ workbook per-state block.
      required: [abbr, mi, permit, permit_status, permit_label, permit_reason, permit_note, extras,
                 escRequired, escRequiredQty, escTravelingQty, superload, police, policeQty, survey, engr, engrType, util, utilType, tooltips]
      properties:
        abbr: { type: string, pattern: '^[A-Z]{2}$' }
        mi: { type: number, minimum: 0 }
        permit: { type: [number, 'null'], minimum: 0, description: OS + settled OW, deduplicated. null iff permit_status != priced. }
        permit_status: { $ref: '#/components/schemas/PermitStatus' }
        permit_label: { type: [string, 'null'] }
        permit_reason: { oneOf: [{ $ref: '#/components/schemas/PermitReason' }, { type: 'null' }] }
        permit_note: { type: [string, 'null'], description: Plain-English reason + citation. Never states a threshold value. }
        extras: { type: string, description: Small add-on charges (bridge fees etc.) when applicable; "" otherwise. }
        escRequired: { $ref: '#/components/schemas/YCN', description: 'What the state itself requires: Yes, CBC (case-by-case), or No.' }
        escRequiredQty: { type: string, pattern: '^$|^\d+$', description: 'The state-required escort count as a string ("" when none or CBC).' }
        escTravelingQty: { type: integer, minimum: 0, description: 'The escort count the money was computed with: ESL-smoothed under rate_card.esl_mode, so escorts carried through a gap state appear here while escRequired stays No. CBC states price as 0 unless escorts are carried through. Matches the calculator per-state cell. Always present.' }
        superload: { type: string, enum: [Yes, No] }
        police: { $ref: '#/components/schemas/YCN' }
        policeQty: { type: string, pattern: '^$|^\d+\+?$' }
        survey: { $ref: '#/components/schemas/YCN' }
        engr: { $ref: '#/components/schemas/YCN' }
        engrType: { type: string, enum: ['', Automated, State Agency, Third-Party] }
        util: { $ref: '#/components/schemas/YCN' }
        utilType: { type: string, enum: ['', Notify, Permission, Personnel, Bucket Truck] }
        tooltips: { oneOf: [{ $ref: '#/components/schemas/Tooltips' }, { type: 'null' }] }
    Assumption:
      type: object
      additionalProperties: false
      required: [field, applied, rule]
      properties:
        field: { type: string }
        applied: {}
        rule: { type: string }
    Warning:
      type: object
      required: [code, message]
      properties:
        code: { type: string }
        state: { type: string, pattern: '^[A-Z]{2}$' }
        message: { type: string }
      additionalProperties: true
    Meta:
      type: object
      additionalProperties: false
      required: [data_version, rate_card, routed_at, provider, resolved, unpriced_states, compare_mi, usage]
      properties:
        data_version: { type: string, example: statesv3@2026-08-20 }
        rate_card: { type: object, additionalProperties: { type: number }, description: The resolved rates actually used. }
        routed_at: { type: string, format: date-time }
        provider:
          type: object
          additionalProperties: false
          required: [engine, alternatives_offered]
          properties:
            engine: { $ref: '#/components/schemas/Engine' }
            alternatives_offered: { type: integer, minimum: 0 }
        resolved:
          type: object
          additionalProperties: false
          required: [origin, destination]
          properties:
            origin: { type: string }
            destination: { type: string }
            waypoints: { type: array, items: { type: string } }
        unpriced_states: { type: array, items: { type: string, pattern: '^[A-Z]{2}$' } }
        compare_mi:
          oneOf:
            - type: object
              additionalProperties: false
              required: [engine, route_mi]
              properties:
                engine: { $ref: '#/components/schemas/Engine' }
                route_mi: { type: number, minimum: 0 }
            - type: 'null'
        usage:
          type: object
          additionalProperties: false
          required: [charge, remaining_allowance, remaining_lot_credits]
          properties:
            charge: { type: string, enum: [allowance, lot, none] }
            remaining_allowance: { type: integer, minimum: 0 }
            remaining_lot_credits: { type: integer, minimum: 0 }
    QuoteCore:
      type: object
      required: [engine, route_mi, total_permits, total_escorts, total_survey, total_service,
                 any_super, super_qty, any_police, police_qty, any_survey, survey_qty,
                 any_engr, engr_qty, any_util, util_qty,
                 orig_tier_flag, orig_tier_note, dest_tier_flag, dest_tier_note, states, polyline]
      properties:
        engine: { $ref: '#/components/schemas/Engine' }
        route_mi: { type: number, minimum: 0 }
        total_permits: { type: [number, 'null'], minimum: 0, description: null iff meta.unpriced_states is non-empty. }
        total_escorts: { type: number, minimum: 0 }
        total_survey: { type: number, minimum: 0 }
        total_service: { type: number, minimum: 0 }
        any_super: { $ref: '#/components/schemas/YCN' }
        super_qty: { $ref: '#/components/schemas/Qty' }
        any_police: { $ref: '#/components/schemas/YCN' }
        police_qty: { $ref: '#/components/schemas/Qty' }
        any_survey: { $ref: '#/components/schemas/YCN' }
        survey_qty: { $ref: '#/components/schemas/Qty' }
        any_engr: { $ref: '#/components/schemas/YCN' }
        engr_qty: { $ref: '#/components/schemas/Qty' }
        any_util: { $ref: '#/components/schemas/YCN' }
        util_qty: { $ref: '#/components/schemas/Qty' }
        orig_tier_flag: { type: string, enum: [yes, no] }
        orig_tier_note: { type: string }
        dest_tier_flag: { type: string, enum: [yes, no] }
        dest_tier_note: { type: string }
        states: { type: array, items: { $ref: '#/components/schemas/StateBlock' } }
        polyline: { type: [string, 'null'], description: Google-encoded; only when options.return_polyline. }
    AlternateQuote:
      allOf:
        - { $ref: '#/components/schemas/QuoteCore' }
        - type: object
          required: [warnings]
          properties:
            warnings: { type: array, items: { $ref: '#/components/schemas/Warning' } }
      description: Same shape as the main quote minus ids/assumptions/meta. Never contains another alternate.
    Quote:
      allOf:
        - { $ref: '#/components/schemas/QuoteCore' }
        - type: object
          required: [quote_id, idempotency_key, client_ref, metadata, alternate, assumptions, warnings, meta]
          properties:
            quote_id: { type: string, pattern: '^q_[0-9A-HJKMNP-TV-Z]{26}$' }
            idempotency_key: { type: string }
            client_ref: { type: [string, 'null'] }
            metadata: { type: [object, 'null'], additionalProperties: { type: [string, number] } }
            alternate: { oneOf: [{ $ref: '#/components/schemas/AlternateQuote' }, { type: 'null' }] }
            assumptions: { type: array, items: { $ref: '#/components/schemas/Assumption' } }
            warnings: { type: array, items: { $ref: '#/components/schemas/Warning' } }
            meta: { $ref: '#/components/schemas/Meta' }
    ErrorCode:
      type: string
      enum: [unauthorized, key_revoked, org_suspended, validation_failed, unknown_field, unsupported_region,
             geocode_failed, no_route, idempotency_key_reused, request_in_progress, insufficient_balance,
             rate_limited_minute, rate_limited_day, suspended_for_review, provider_unavailable,
             metering_unavailable, internal]
    ApiError:
      type: object
      additionalProperties: false
      required: [error]
      properties:
        error:
          type: object
          additionalProperties: false
          required: [code, message, request_id]
          properties:
            code: { $ref: '#/components/schemas/ErrorCode' }
            message: { type: string }
            details: {}
            request_id: { type: string }
