openapi: 3.1.0
info:
  title: Lanesurf Agent API
  version: 0.4.0
  summary: Let any AI agent get a freight load covered, or get a truck loaded, through Lanesurf.
  description: |
    Lanesurf finds carriers for freight loads by email, phone and text, and negotiates the rate.
    This API lets AI agents (Claude, ChatGPT, Grok, Cursor and others) and scripts use Lanesurf directly.

    There are two kinds of caller:

    - **Broker agents** ("cover this load for me"): post a load as JSON fields, send any fields
      Lanesurf says are missing, then read carrier offers and pick one. Paths under `/v1/loads`.
    - **Carrier / dispatcher agents** ("find a load for my truck"): post trucks and lanes, search
      loads, and bid on a load. Paths under `/v1/carrier`.

    Every request (except signup and `/health`) uses `Authorization: Bearer ls_live_...`.

    - A **broker key** belongs to one rep at a brokerage. It reaches only loads that brokerage posted
      through this API, whichever of the brokerage's reps posted them. It never reaches loads that came
      from the brokerage's TMS or from other brokerages.
    - A **carrier key** belongs to one carrier signup. Its trucks, lanes and bids are its own; no other
      key sees them. Load search covers active loads from Lanesurf brokers that show their loads to carriers' agents,
      without broker identity or contacts. Only **verified** carrier keys can read loads (search, one load, matches) and bid;
      keys whose carrier account is under review (`needs_review`) get `403 verification_required` there.

    Anyone can get a key with `POST /v1/signup` (carrier or broker, a work email, an MC and/or DOT number)
    and `POST /v1/signup/{signup_id}/verify`. Every signup reply has `next_step`, which tells an agent what
    to do next. A brokerage that signs up starts with carrier outreach off until Lanesurf reviews it.

    Broker keys can't call `/v1/carrier/*` and carrier keys can't call `/v1/loads/*` (`403 wrong_role`).

    Every response has an `X-Request-Id` header, and every error body a `request_id`. Send your own
    `X-Request-Id` (8-64 letters, digits, `.`, `_` or `-`) to have it used instead. Quote it to
    Lanesurf support when something fails.

    Every authenticated `/v1` route is limited to 120 requests per minute per API key, counted on each Lanesurf
    server (treat it as the most to send). Over the limit you
    get `429 rate_limited` with a `Retry-After` header.

    **Lists.** Your loads (`GET /v1/loads`) come a page at a time: `limit` (1 to 100, default 25), then send
    `next_cursor` back as `cursor` until it is `null`. Load search (`GET /v1/carrier/loads/search`) and truck matches
    (`GET /v1/carrier/trucks/{truck_id}/matches`) take `limit` (1 to 100, default 25) and return the closest
    loads first (see each route), with `total` = the number returned. There is no `offset` or cursor: to see other loads,
    narrow or change the search. The other lists have no paging and return the newest items up to a cap: trucks
    200, lanes 500, bids 200, and a carrier's outreach 50 loads (from the last 30 days). A load's outreach lists
    every carrier on that load.

    Lanesurf doesn't send webhooks: poll a load (`GET /v1/loads/{id}`) or your bids (`GET /v1/carrier/bids`)
    every few minutes.

    Versioning, limits and changes: see https://docs.lanesurf.com/versioning,
    https://docs.lanesurf.com/rate-limits and https://docs.lanesurf.com/changelog.
  contact:
    name: Lanesurf support
    url: https://docs.lanesurf.com
    email: support@lanesurf.com
  license:
    name: Proprietary. Use is subject to the Lanesurf Terms of Service
    url: https://www.lanesurf.com/terms
servers:
  - url: https://agent.lanesurf.com
    description: Production

security:
  - bearerAuth: []

tags:
  - name: Broker loads
    description: A broker's agent posts a load and gets it covered.
  - name: Authentication
    description: Sign up as a carrier or a brokerage with a work email and a one-time code, and see who a key belongs to.
  - name: Broker setup
    description: What Lanesurf's agents tell carriers about a brokerage, and whether carrier outreach is on.
  - name: Carrier trucks
    description: A carrier's agent says where its trucks are and when they're free.
  - name: Carrier lanes
    description: A carrier's agent saves the lanes the carrier runs (its terminals).
  - name: Carrier loads
    description: A carrier's agent finds loads, bids on one, and follows its bids.
  - name: System
    description: Service health.

paths:
  /health:
    get:
      tags: [System]
      summary: Health check
      operationId: health
      security: []
      x-mint:
        metadata:
          title: Health check
          sidebarTitle: Health check
          description: Check that the service is up.
        content: |
          Returns `{"status": "ok"}` when the service is up.
      description: 'Returns `{"status": "ok"}` when the service is up. No API key needed.'
      responses:
        "200":
          description: The service is up.
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, example: ok }

  # ---------------------------------------------------------------- broker side
  /v1/loads:
    get:
      tags: [Broker loads]
      summary: List your loads
      operationId: listLoads
      x-mint:
        metadata:
          title: List your loads
          sidebarTitle: List your loads
          description: The loads your brokerage posted through the API, newest first, a page at a time.
        content: |
          The loads your brokerage posted through this API, newest first: each one's status, lane, equipment,
          dates, rates and outreach counts. Any rep's key at the brokerage sees all of them; loads from your TMS or
          the dashboard aren't listed. For a load's offers, call `GET /v1/loads/{id}`.

          Pages: `limit` loads at a time (1 to 100, default 25). When there are more, `next_cursor` is set: send it
          back as `cursor` for the next page. It is `null` on the last page.
      description: |
        The loads the caller's brokerage posted through this API, newest first, with the same statuses as
        `GET /v1/loads/{id}`. Any rep's key at the brokerage sees all of them; loads from the TMS or the dashboard
        aren't listed. Paged with `limit` and `cursor`: send `next_cursor` back as `cursor` until it is `null`.
      parameters:
        - name: limit
          in: query
          description: How many loads per page, a whole number from 1 to 100. Defaults to 25. Anything else answers 400.
          schema: { type: integer, default: 25, minimum: 1, maximum: 100 }
        - name: cursor
          in: query
          description: The `next_cursor` from the previous page, sent with the same `status` and `needs_decision` as that page (other filters answer 400). Leave it out for the first page.
          schema: { type: string, maxLength: 500 }
        - name: status
          in: query
          description: |
            Only loads with this status. With `sourcing` or `stalled`, a page can hold fewer than `limit` loads
            even when more follow: keep paging until `next_cursor` is `null`.
          schema: { type: string, enum: [sourcing, stalled, covered, inactive, deleted] }
        - name: needs_decision
          in: query
          description: |
            `true`: only open loads with offers waiting for your answer (`offers.pending` above 0). One call answers
            "anything new on my loads?" instead of reading every load. Send the cursor with the same filter.
          schema: { type: boolean }
      responses:
        "200":
          description: One page of loads, newest first.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/LoadList" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/WrongRole" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "500": { $ref: "#/components/responses/Internal" }
        "503": { $ref: "#/components/responses/AuthUnavailable" }
    post:
      tags: [Broker loads]
      summary: Post a load
      operationId: postLoad
      x-mint:
        metadata:
          title: Post a load
          sidebarTitle: Post a load
          description: Send a load as JSON fields. Lanesurf starts finding carriers.
        content: |
          Send the load as JSON fields (the `LoadFields` schema). The minimum is: `equipment_type`, the
          first origin's `city`, `state` and `pickup_ready_date`, and the last destination's `city`, `state`
          and `delivery_ready_date`. Commodity defaults to "General freight" (returned in `assumed`).
          Rate is optional. Numbers may be sent as numbers or strings; one that isn't a number answers 400.

          If something required is missing you get **422** `missing_fields` with a `missing` list of exact
          field paths (e.g. `destinations[0].delivery_ready_date`). Ask the person only for those, then send
          the whole load again.

          When the load is complete you get **201** with `status: sourcing` and a `load_id`. Lanesurf starts
          reaching carriers. `post_with`, when the answer has it, is the email and phone to put on the load
          wherever it is advertised, so carrier replies reach Lanesurf. It comes back only when Lanesurf has set
          a posting email and phone for your key; keys made by signing in from an AI app don't have one yet.

          A brokerage that signed up through the API starts with carrier outreach off: its loads are saved
          with `status: stalled` and a `status_reason` saying outreach starts after Lanesurf's review.

          Every reply, and the `missing_fields` error, has `next_step`: what the agent should do next.

          **Retries.** The `Idempotency-Key` header is **required** (up to 200 characters; a new UUID per load).
          Without it the post is refused with **400** `idempotency_key_required`, because a retry after a
          timeout would otherwise create a second load and Lanesurf would contact the same carriers twice.
          The key is scoped to the API key that sends it. With the same API key and the same `Idempotency-Key`:

          - First post: **201**, a new load.
          - Retry with the same load: **200** with the original load and its current `status` (`sourcing`,
            `covered`, …). No second load.
          - Retry with a different load: **422** `idempotency_key_reused`. Nothing is created or changed.
          - Retry while the first post is still running: **409** `idempotency_in_progress` with `Retry-After: 5`.
            Wait, then retry.

          The same `Idempotency-Key` from a different API key, even at the same brokerage, creates a different
          load. Use a new key for each new load.
      description: |
        Send the load as JSON fields (the `LoadFields` schema). The minimum is: `equipment_type`, the
        first origin's `city`, `state` and `pickup_ready_date`, and the last destination's `city`, `state`
        and `delivery_ready_date`. Commodity defaults to "General freight" (returned in `assumed`).
        Rate is optional. Numbers may be sent as numbers or strings; one that isn't a number answers 400.

        If something required is missing you get **422** `missing_fields` with a `missing` list of exact
        field paths. Ask the person only for those, then send the whole load again.

        When the load is complete you get **201** with `status: sourcing` and a `load_id`. Lanesurf starts
        reaching carriers. `post_with`, when the answer has it, is the email and phone to put on the load
        wherever it is advertised (only when Lanesurf has set them for your key).

        The `Idempotency-Key` header is required (up to 200 characters, a new UUID per load); without it
        the post answers 400 `idempotency_key_required`. If Lanesurf can't process the load (bad stops or
        dates) the answer is 400 `load_rejected`: fix the load rather than retrying it unchanged. The key is
        scoped to the API key that sends it. The first call answers 201. A retry of the same load with the
        same key from the same API key answers 200 with the original load and its current `status`. A
        different load with a used key answers 422 `idempotency_key_reused`. A retry while the first post is
        still running answers 409 `idempotency_in_progress` with `Retry-After: 5`. The same `Idempotency-Key`
        from a different API key, even at the same brokerage, creates a different load. Use a new key for
        each new load.
      parameters:
        - { $ref: "#/components/parameters/IdempotencyKey" }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/LoadFields" }
            example:
              equipment_type: Dry Van
              weight_lbs: 40000
              rate_max: 1800
              origins: [{ city: Dallas, state: TX, pickup_ready_date: "2026-10-23" }]
              destinations: [{ city: Atlanta, state: GA, delivery_ready_date: "2026-10-24" }]
      responses:
        "201":
          description: The load was created and Lanesurf is contacting carriers.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/PostLoadResult" }
              example:
                status: sourcing
                load_id: 3f2a9c4e-1d7b-4c55-8a7e-0b9d6e5f1234
                tms_id: api-7c1e0b5a2f4d4c8e9a3b6d1f0e2c4a8b
                post_with: { email: "jane@loads.example.com", phone: "+14155550100" }
                assumed: { commodity: "General freight" }
                message: "Lanesurf is contacting carriers. Put jane@loads.example.com / +14155550100 on the load wherever you advertise it so carrier replies reach Lanesurf. Check GET /v1/loads/3f2a9c4e-1d7b-4c55-8a7e-0b9d6e5f1234 for offers."
                next_step: "Tell the person what Lanesurf assumed (assumed). Tell them to advertise the load with the post_with email and phone so carrier replies reach Lanesurf. Then call get_load with load_id every 2 to 5 minutes to see carrier offers (GET /v1/loads/3f2a9c4e-1d7b-4c55-8a7e-0b9d6e5f1234)."
        "200":
          description: |
            A retry of the same load with an `Idempotency-Key` this API key already used. This is the original
            load with its current `status`; no second load was created. `assumed` is not set.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/PostLoadResult" }
              example:
                status: covered
                load_id: 3f2a9c4e-1d7b-4c55-8a7e-0b9d6e5f1234
                tms_id: api-7c1e0b5a2f4d4c8e9a3b6d1f0e2c4a8b
                post_with: { email: "jane@loads.example.com", phone: "+14155550100" }
                message: "This Idempotency-Key was already used for this load: this is the original load, not a new one. Check GET /v1/loads/3f2a9c4e-1d7b-4c55-8a7e-0b9d6e5f1234."
                next_step: "The load is covered by the accepted offer (winner); stop polling. Lanesurf does not contact the carrier: tell the person to reach out to the winner (contact_email / contact_phone) to book, and to send the rate confirmation from their own TMS. Lanesurf has also emailed the brokerage's reps the carrier's details."
        "400":
          description: |
            `invalid_request`: the body isn't JSON load fields, or the `Idempotency-Key` is over 200 characters.

            `idempotency_key_required`: no `Idempotency-Key` header. Nothing was created.

            `load_rejected`: Lanesurf couldn't process the load (check the stop cities, states, zip codes and
            dates). Don't retry it unchanged; fix it and send it with a new `Idempotency-Key`.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/WrongRole" }
        "409":
          description: |
            `idempotency_in_progress`: a post with this `Idempotency-Key` from this API key is still being
            processed. Wait `Retry-After` seconds (5), then retry with the same key and the same load.
          headers:
            Retry-After: { $ref: "#/components/headers/RetryAfter" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
              example:
                error: a post with this Idempotency-Key is still being processed; retry in a few seconds
                code: idempotency_in_progress
        "422":
          description: |
            `missing_fields`: required fields are missing. Send the whole load again with them.

            `idempotency_key_reused`: this API key already used this `Idempotency-Key` for a different load.
            Nothing was created or changed. Use a new key for a new load.
          content:
            application/json:
              schema:
                oneOf:
                  - { $ref: "#/components/schemas/MissingFields" }
                  - type: object
                    required: [error, code]
                    properties:
                      error: { type: string }
                      code: { type: string, enum: [idempotency_key_reused] }
              examples:
                missing_fields:
                  summary: Required fields are missing
                  value:
                    error: the load is missing required fields; send them and try again
                    code: missing_fields
                    missing: ["destinations[0].delivery_ready_date"]
                idempotency_key_reused:
                  summary: The key was used for a different load
                  value:
                    error: this Idempotency-Key was already used for a different load; use a new key for a new load
                    code: idempotency_key_reused
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "500": { $ref: "#/components/responses/Internal" }
        "502": { $ref: "#/components/responses/Upstream" }
        "503": { $ref: "#/components/responses/AuthUnavailable" }

  /v1/loads/{id}:
    parameters:
      - { $ref: "#/components/parameters/LoadRef" }
    get:
      tags: [Broker loads]
      summary: Load status and carrier offers
      operationId: getLoad
      x-mint:
        metadata:
          title: Get load status
          sidebarTitle: Get load status
          description: Status, carrier offers and the agreed rate.
        content: |
          The load's status, carrier offers (cheapest first), the winner if one was accepted, and how many
          carriers were contacted. An offer is a carrier that named a rate. Poll this every few minutes.

          `{id}` is the `load_id` or the `tms_id` from `POST /v1/loads`. Any rep's key at the brokerage that
          posted the load can read it. Other loads answer `404`.
      description: |
        The load's status, carrier offers (cheapest first), the winner if one was accepted, and how many
        carriers were contacted. An offer is a carrier that named a rate. Poll this every few minutes. `{id}` is the `load_id` or the `tms_id`. Any rep's key at the brokerage that
        posted the load through this API can read it; other loads answer 404.
      responses:
        "200":
          description: Current status.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/LoadStatus" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/WrongRole" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "500": { $ref: "#/components/responses/Internal" }
        "502": { $ref: "#/components/responses/Upstream" }
        "503": { $ref: "#/components/responses/AuthUnavailable" }

    patch:
      tags: [Broker loads]
      summary: Edit a load
      operationId: editLoad
      x-mint:
        metadata:
          title: Edit a load
          sidebarTitle: Edit load
          description: Change what was posted, as the dashboard can.
        content: |
          Change what was posted: send only the fields that change, in the same shape as `POST /v1/loads`
          (plus `reference_number`). `origins` and `destinations` go together and replace every stop: send all
          of them, as `GET /v1/loads/{id}` shows them in `load`.

          - Lanesurf re-reads the load. Carriers already contacted keep the details they were given.
          - On a load with no rate yet, one of `rate_min` / `rate_max` sets both, as on posting.
          - Numbers may be numbers or strings; one that isn't a number answers `400`.
          - Works on open loads. A paused (`inactive`) load can change everything but its stops, temperature and
            pallets (resume it first, or change those in the Lanesurf dashboard).
          - `409 invalid_state`: the load is covered (reopen it first) or deleted.
          - `422 missing_fields`: new stops miss a required city, state or date.
      description: |
        Change what was posted: only the fields sent change, in the same shape as POST /v1/loads (plus
        reference_number). origins and destinations go together and replace every stop. Works on open loads; a
        paused load can change everything but its stops. 409 invalid_state when the load is covered or deleted.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/LoadPatch" }
            example: { rate_max: 1300, reference_number: PO-4471 }
      responses:
        "200":
          description: Updated status, with the updated `load`.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/LoadStatus" }
        "422":
          description: "`missing_fields`: new stops miss a required city, state or date (`missing` lists the paths). Nothing changed."
          content:
            application/json:
              schema: { $ref: "#/components/schemas/MissingFields" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/WrongRole" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/InvalidState" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "500": { $ref: "#/components/responses/Internal" }
        "502": { $ref: "#/components/responses/Upstream" }
        "503": { $ref: "#/components/responses/AuthUnavailable" }
    delete:
      tags: [Broker loads]
      summary: Delete a load
      operationId: deleteLoad
      x-mint:
        metadata:
          title: Delete a load
          sidebarTitle: Delete load
          description: The shipper cancelled it.
        content: |
          Deletes the load, as the Lanesurf dashboard does: Lanesurf stops all work on it and it becomes
          `deleted`. It can't be brought back through the API.

          - Not for a load covered elsewhere (`/covered`) or on hold (`/pause`).
          - Calling it again is safe.
      description: |
        Deletes the load, as the Lanesurf dashboard does: Lanesurf stops all work on it and it becomes deleted.
        Not for a load covered elsewhere or on hold. Calling it again is safe.
      responses:
        "200":
          description: "Updated status: `deleted`."
          content:
            application/json:
              schema: { $ref: "#/components/schemas/LoadStatus" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/WrongRole" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/InvalidState" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "500": { $ref: "#/components/responses/Internal" }
        "502": { $ref: "#/components/responses/Upstream" }
        "503": { $ref: "#/components/responses/AuthUnavailable" }

  /v1/loads/{id}/outreach:
    parameters:
      - { $ref: "#/components/parameters/LoadRef" }
    get:
      tags: [Broker loads]
      summary: Who was contacted on a load
      operationId: getLoadOutreach
      x-mint:
        metadata:
          title: Load outreach
          sidebarTitle: Load outreach
          description: Every carrier on a load, as the dashboard lists them.
        content: |
          Every carrier on the load, as the Lanesurf dashboard's Book Board lists them: the carriers Lanesurf
          contacted (`direction: outbound`) and those who reached out or bid (`inbound`), the channel (`call`,
          `email`, `sms`), when, where each stands (`status`, the dashboard's label), the rate for carriers who
          named one (with its `session_id`, for accept and decline), and the carrier's contact.

          Use it to tell the person what happened on the load, for example which carriers were emailed or
          called. Same access as `GET /v1/loads/{id}`; other loads answer `404`.
      description: |
        Every carrier on the load, as the Lanesurf dashboard's Book Board lists them: who Lanesurf contacted
        (direction outbound) and who reached out or bid (inbound), the channel, when, the status label, the rate
        for carriers who named one (with its session_id) and the contact. Same access as GET /v1/loads/{id}.
      responses:
        "200":
          description: The carriers on the load, best rate first, then newest.
          content:
            application/json:
              schema:
                type: object
                required: [load_id, outreach]
                properties:
                  load_id: { type: string, description: "The id you passed in the path: the load_id, or the tms_id if you used that." }
                  outreach:
                    type: array
                    items: { $ref: "#/components/schemas/LoadOutreach" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/WrongRole" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "500": { $ref: "#/components/responses/Internal" }
        "502": { $ref: "#/components/responses/Upstream" }
        "503": { $ref: "#/components/responses/AuthUnavailable" }

  /v1/loads/{id}/offers/{session_id}/accept:
    parameters:
      - { $ref: "#/components/parameters/LoadRef" }
      - { $ref: "#/components/parameters/SessionID" }
    post:
      tags: [Broker loads]
      summary: Accept a carrier's offer
      operationId: acceptOffer
      x-mint:
        metadata:
          title: Accept an offer
          sidebarTitle: Accept an offer
          description: Pick this carrier. Lanesurf stops looking.
        content: |
          Records that the rep picks this carrier. Lanesurf takes the load off the market (it goes inactive) and
          stops looking. The load becomes `covered` with `covered_by: lanesurf` and `winner` set.

          Lanesurf does **not** contact the carrier or send a rate confirmation: the broker reaches out to the winner
          (`contact_email` / `contact_phone` on `winner`) to book, and sends the rate confirmation from their own TMS.
          Lanesurf also emails the brokerage's reps (the load's CC list, else the brokerage's CC list, plus the rep who
          posted it) the carrier's details. Confirm with the
          person before calling this.

          - Accepting the same offer again is safe: you get the same answer.
          Send the offer's `rate` that the person agreed to. If the carrier changed its rate since, nothing is
          accepted and you get `409 offer_changed` with `current_rate`: ask the person again.

          - `409 already_covered`: another offer was already accepted, or the load was marked covered
            (`POST /v1/loads/{id}/covered`) or deleted. A load can't be both accepted and marked covered.
          - `409 offer_changed`: the offer's rate isn't the one sent (the carrier changed it, or the rate sent was
            wrong); `current_rate` has it and `error` names both. Nothing was accepted.
          - A declined offer can still be accepted later. An offer on a paused load can be accepted too; the load
            becomes covered. Reopening a covered load puts it back to sourcing, even if it was paused before.
          - `400 invalid_request`: no `rate` was sent, or this carrier hasn't named a usable rate yet.
      description: |
        Records that the rep picks this carrier. Lanesurf takes the load off the market (it goes inactive) and
        stops looking. The load becomes `covered` with `covered_by: lanesurf` and `winner` set. Lanesurf does
        **not** contact the carrier or send a rate confirmation: the broker reaches out to the winner (contact_email /
        contact_phone) and sends the rate con from their TMS; Lanesurf emails the brokerage's reps the carrier's
        details. Confirm with the person first.

        Accepting the same offer again is safe and returns the same answer. 409 `already_covered`: another
        offer was already accepted, or the load was marked covered or deleted. A load can't be both accepted
        and marked covered.
        400 `invalid_request`: no `rate` was sent, or the carrier hasn't named a usable rate yet.
        409 `offer_changed`: the carrier changed its rate since; `current_rate` has the new one.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [rate]
              properties:
                rate:
                  type: number
                  description: The offer's rate in USD as `GET /v1/loads/{id}` showed it and the person agreed to.
                  example: 1850
      responses:
        "200":
          description: Updated status, with `winner` set.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/LoadStatus" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/WrongRole" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/AlreadyCovered" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "500": { $ref: "#/components/responses/Internal" }
        "502": { $ref: "#/components/responses/Upstream" }
        "503": { $ref: "#/components/responses/AuthUnavailable" }

  /v1/loads/{id}/offers/{session_id}/decline:
    parameters:
      - { $ref: "#/components/parameters/LoadRef" }
      - { $ref: "#/components/parameters/SessionID" }
    post:
      tags: [Broker loads]
      summary: Decline a carrier's offer
      operationId: declineOffer
      x-mint:
        metadata:
          title: Decline an offer
          sidebarTitle: Decline an offer
          description: Record that the rep passes on this offer.
        content: |
          Records that the rep passes on this offer. The offer shows `decision: declined`.

          Declining only records the choice. It does **not** stop Lanesurf's outreach to that carrier, and the
          load keeps `sourcing`.

          - `409 already_covered`: an offer was already accepted, or the load was marked covered or deleted.
          - `400 invalid_request`: this carrier hasn't named a usable rate yet.
      description: |
        Records that the rep passes on this offer (`decision: declined`). It only records the choice: it does
        not stop Lanesurf's outreach to that carrier, and the load keeps sourcing. 409 `already_covered`: an
        offer was already accepted, or the load was marked covered or deleted. 400 `invalid_request`: the
        carrier hasn't named a usable rate yet.
      responses:
        "200":
          description: Updated status.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/LoadStatus" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/WrongRole" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/AlreadyCovered" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "500": { $ref: "#/components/responses/Internal" }
        "502": { $ref: "#/components/responses/Upstream" }
        "503": { $ref: "#/components/responses/AuthUnavailable" }

  /v1/loads/{id}/covered:
    parameters:
      - { $ref: "#/components/parameters/LoadRef" }
    post:
      tags: [Broker loads]
      summary: Mark the load covered elsewhere
      operationId: markCovered
      x-mint:
        metadata:
          title: Mark covered
          sidebarTitle: Mark covered
          description: You covered it elsewhere. Lanesurf stops.
        content: |
          The rep covered the load outside Lanesurf. Lanesurf takes the load off the market and stops looking.
          The load becomes `covered` with `covered_by: rep`.

          - Calling it again is safe: you get the same answer.
          - `409 already_covered`: an offer was already accepted, or the load was deleted. A load can't be both
            accepted and marked covered.
      description: |
        The rep covered the load outside Lanesurf. Lanesurf takes the load off the market and stops looking.
        The load becomes `covered` with `covered_by: rep`. Calling it again is safe. 409 `already_covered`:
        an offer was already accepted, or the load was deleted.
      responses:
        "200":
          description: "Updated status: `covered`, with `covered_by` = `rep`."
          content:
            application/json:
              schema: { $ref: "#/components/schemas/LoadStatus" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/WrongRole" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/AlreadyCovered" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "500": { $ref: "#/components/responses/Internal" }
        "502": { $ref: "#/components/responses/Upstream" }
        "503": { $ref: "#/components/responses/AuthUnavailable" }

  /v1/loads/{id}/pause:
    parameters:
      - { $ref: "#/components/parameters/LoadRef" }
    post:
      tags: [Broker loads]
      summary: Pause a load
      operationId: pauseLoad
      x-mint:
        metadata:
          title: Pause a load
          sidebarTitle: Pause load
          description: "Stop sourcing a load on hold; resume it later."
        content: |
          Lanesurf stops contacting carriers for the load and takes it off the market, as the dashboard's Deactivate
          does. The load becomes `inactive`. `POST /v1/loads/{id}/resume` starts it again.

          - Use it when the shipper put the load on hold. Cancelled for good: `DELETE /v1/loads/{id}`.
          - Calling it again is safe: you get the same answer.
          - `409 invalid_state`: the load is covered or deleted.
      description: |
        Lanesurf stops contacting carriers for the load and takes it off the market, as the dashboard's Deactivate
        does. The load becomes `inactive`. `POST /v1/loads/{id}/resume` starts it again.

        - Use it when the shipper put the load on hold. Cancelled for good: `DELETE /v1/loads/{id}`.
        - Calling it again is safe: you get the same answer.
        - `409 invalid_state`: the load is covered or deleted.
      responses:
        "200":
          description: "Updated status: `inactive`."
          content:
            application/json:
              schema: { $ref: "#/components/schemas/LoadStatus" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/WrongRole" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/InvalidState" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "500": { $ref: "#/components/responses/Internal" }
        "502": { $ref: "#/components/responses/Upstream" }
        "503": { $ref: "#/components/responses/AuthUnavailable" }

  /v1/loads/{id}/resume:
    parameters:
      - { $ref: "#/components/parameters/LoadRef" }
    post:
      tags: [Broker loads]
      summary: Resume a load
      operationId: resumeLoad
      x-mint:
        metadata:
          title: Resume a load
          sidebarTitle: Resume load
          description: Put a paused load back on the market.
        content: |
          Puts a paused (`inactive`) load back on the market. **Lanesurf starts contacting carriers again** (real
          emails and calls), with outreach as it was before the pause: a load that had no outreach still has none.

          - Calling it again is safe.
          - `409 invalid_state`: the load is covered (use `/reopen`), deleted, or its first pickup has passed (change
            the dates in the Lanesurf dashboard, or post the load again).
      description: |
        Puts a paused (`inactive`) load back on the market. **Lanesurf starts contacting carriers again** (real
        emails and calls), with outreach as it was before the pause: a load that had no outreach still has none.

        - Calling it again is safe.
        - `409 invalid_state`: the load is covered (use `/reopen`), deleted, or its first pickup has passed (change
          the dates in the Lanesurf dashboard, or post the load again).
      responses:
        "200":
          description: "Updated status: `sourcing` (or `stalled`)."
          content:
            application/json:
              schema: { $ref: "#/components/schemas/LoadStatus" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/WrongRole" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/InvalidState" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "500": { $ref: "#/components/responses/Internal" }
        "502": { $ref: "#/components/responses/Upstream" }
        "503": { $ref: "#/components/responses/AuthUnavailable" }

  /v1/loads/{id}/reopen:
    parameters:
      - { $ref: "#/components/parameters/LoadRef" }
    post:
      tags: [Broker loads]
      summary: Reopen a covered load
      operationId: reopenLoad
      x-mint:
        metadata:
          title: Reopen a covered load
          sidebarTitle: Reopen load
          description: "The carrier fell off: look for a carrier again."
        content: |
          For a covered load whose carrier fell off, or whose cover fell through. The accepted offer becomes declined
          (or the covered-elsewhere mark is removed), and the load looks for a carrier again: **Lanesurf starts
          contacting carriers again**, with outreach as it was before. The other offers can be accepted.

          - On a paused load that isn't covered, it does what `/resume` does.
          - `409 invalid_state`: the load is deleted, or its first pickup has passed. Nothing changes then.
      description: |
        For a covered load whose carrier fell off, or whose cover fell through. The accepted offer becomes declined
        (or the covered-elsewhere mark is removed), and the load looks for a carrier again: **Lanesurf starts
        contacting carriers again**, with outreach as it was before. The other offers can be accepted.

        - On a paused load that isn't covered, it does what `/resume` does.
        - `409 invalid_state`: the load is deleted, or its first pickup has passed. Nothing changes then.
      responses:
        "200":
          description: "Updated status: `sourcing` (or `stalled`)."
          content:
            application/json:
              schema: { $ref: "#/components/schemas/LoadStatus" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/WrongRole" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/InvalidState" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "500": { $ref: "#/components/responses/Internal" }
        "502": { $ref: "#/components/responses/Upstream" }
        "503": { $ref: "#/components/responses/AuthUnavailable" }

  # --------------------------------------------------------------- signup
  /v1/signup:
    post:
      tags: [Authentication]
      summary: Start signup (carrier or broker)
      operationId: startSignup
      security: []
      x-mint:
        metadata:
          title: Start signup
          sidebarTitle: Start signup
          description: Say carrier or broker, give an MC and/or DOT number and a work email. We email a code.
        content: |
          Step 1 of 2 to get an API key, for a carrier or a brokerage, with no human at Lanesurf involved.
          Send `role` (`carrier` or `broker`), the company's `mc_number` and/or `dot_number`, and the person's
          **work email**. Lanesurf emails an 8-digit code there.

          - **Work email only.** Free mail (Gmail, Yahoo, Outlook, Hotmail, Live, MSN, iCloud, me.com, mac.com, AOL,
            Proton, GMX, Zoho, Yandex, mail.com, and their country versions such as yahoo.co.uk) is refused with `400 invalid_request` ("use your work email").
          - **Email is required.** There is no phone-only signup.
          - Missing `role`, `email`, or both `mc_number` and `dot_number`: `422 missing_fields` with a `missing` list.

          The reply only says where the code went. How a carrier's check went is shown by
          `POST /v1/signup/{signup_id}/verify`, once the person has proved the inbox with the code.
          Every reply, including errors, has `next_step`: what the agent should do next.

          Limits: 5 starts per email per hour and 10 per day, 10 per MC/DOT number per day, and per-IP limits.
      description: |
        Step 1 of 2 to get an API key, for a carrier or a brokerage. Send role (carrier or broker), the
        company's mc_number and/or dot_number, and the person's work email. Lanesurf emails an 8-digit code.
        Free mail (Gmail, Yahoo, Outlook, iCloud, AOL and the other listed providers) is refused with 400 invalid_request ("use your work email"); email is
        required (no phone-only signup). Missing fields answer 422 missing_fields. The reply only says where
        the code went; verify reports how the carrier check went. Every reply has next_step.
        Limits: 5 starts per email per hour and 10 per day, 10 per MC/DOT number per day, and per-IP limits.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/SignupRequest" }
            examples:
              carrier:
                summary: A carrier
                value: { role: carrier, dot_number: "1234567", email: "dispatch@exampletrucking.example.com" }
              broker:
                summary: A brokerage
                value: { role: broker, mc_number: "654321", email: "dana@acmefreight.example.com" }
      responses:
        "201":
          description: Code sent. Follow `next_step`.
          headers:
            Cache-Control: { $ref: "#/components/headers/NoStore" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Signup" }
              example:
                signup_id: 5d0e8a3c-6f1b-4e2a-9c7d-1b2a3c4d5e6f
                role: carrier
                channel: email
                sent_to: "d*******@exampletrucking.example.com"
                expires_at: "2026-10-23T14:10:00Z"
                next_step: "Ask the person for the 8-digit code we emailed to d*******@exampletrucking.example.com, then call verify_signup with signup_id and code (POST /v1/signup/{signup_id}/verify). The code expires in 10 minutes."
        "400":
          description: "`invalid_request`: a value is invalid, e.g. a free or personal email (\"use your work email\")."
          content:
            application/json:
              schema: { $ref: "#/components/schemas/SignupError" }
              example:
                error: "use your work email: free and personal mail (Gmail, Yahoo, Outlook, iCloud, AOL and the like) is not accepted"
                code: invalid_request
                next_step: "Ask the person for their work email at the company (not Gmail, Yahoo, Outlook, iCloud or another personal mailbox), then call start_signup again."
        "422":
          description: "`missing_fields`: `missing` lists what to ask for."
          content:
            application/json:
              schema: { $ref: "#/components/schemas/SignupError" }
              example:
                error: "signup needs email"
                code: missing_fields
                missing: [email]
                next_step: "Ask the person for email, then call start_signup again with role, email and mc_number and/or dot_number."
        "429": { $ref: "#/components/responses/SignupRateLimited" }
        "500": { $ref: "#/components/responses/SignupInternal" }
        "503": { $ref: "#/components/responses/SignupUnavailable" }

  /v1/signup/{signup_id}/verify:
    post:
      tags: [Authentication]
      summary: Finish signup with the code
      operationId: verifySignup
      security: []
      x-mint:
        metadata:
          title: Verify the code
          sidebarTitle: Verify the code
          description: Send the 8-digit code (brokers also say who they are). Get your API key.
        content: |
          Step 2 of 2. Send the 8-digit code the person received. Returns the API key **once**; store it
          securely. Codes expire after 10 minutes and allow 5 attempts. If the answer was lost on the way, send
          the same code again while it's valid: you get a working key, or `409 already_redeemed` once the first
          key has been used.

          **Brokers** also send `name`, `job_title` and `company_name` (and optionally `phone` and `website`).
          If any required one is missing you get `422 missing_fields` **before** an attempt is used, so ask for
          them and send the same code again. The MC/DOT number and email come from step 1. The brokerage is
          created unverified with carrier outreach **off** (`activated: false`): Lanesurf reviews it, then turns
          outreach on. Until then loads you post are saved and show `stalled`.

          **Carriers**: Lanesurf checks the carrier by DOT/MC number and compares the email's domain with the
          carrier's record. `principal.verification` is:

          - `verified`: the check confirmed the carrier (the email's domain matches an email on the carrier's
            record, and the record doesn't show the carrier's authority as inactive). Full access: trucks, lanes, load search, load details, truck
            matches and bids.
          - `needs_review`: the check couldn't confirm it: no match, carrier not found, check unavailable, inactive or
            broker-only authority, an MC and a DOT of different carriers, or a shared mailbox domain (free
            mail, ISP mail such as att.net or comcast.net, disposable inboxes), which never counts as a
            match because anyone can hold a mailbox there. Your carrier account is under review until Lanesurf's
            team verifies it; the same key then gets full access. Until then the key can post and manage trucks and lanes and list its own bids, but
            **can't** search loads, read a load, get truck matches, list outreach or bid (`403 verification_required`).

          Nobody is refused for not matching. Every reply has `next_step`.
      description: |
        Step 2 of 2. Send the 8-digit code. Returns the API key once; store it securely. Codes expire after
        10 minutes and allow 5 attempts; re-sending a valid code after a lost answer returns a working key, or
        409 already_redeemed once that key has been used. Brokers also send name, job_title and company_name
        (phone, website optional); missing ones answer 422 missing_fields before an attempt is used. A new
        brokerage starts with carrier outreach off until Lanesurf reviews it (activated false). Carriers get
        principal.verification verified (the email's domain matches an email on the carrier's
        record) or needs_review (until Lanesurf's team verifies the account) (under review: trucks, lanes and bids list only). Every reply has next_step.
      parameters:
        - name: signup_id
          in: path
          required: true
          description: The `signup_id` from `POST /v1/signup`.
          schema: { type: string, format: uuid }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/VerifySignupRequest" }
            examples:
              carrier:
                summary: A carrier
                value: { code: "48291307" }
              broker:
                summary: A brokerage
                value:
                  code: "48291307"
                  name: Dana Reyes
                  job_title: Carrier Sales Manager
                  company_name: Acme Freight LLC
                  phone: "+12145550100"
                  website: https://acmefreight.example.com
      responses:
        "201":
          description: The new API key. Follow `next_step`.
          headers:
            Cache-Control: { $ref: "#/components/headers/NoStore" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/IssuedKey" }
              examples:
                carrier_needs_review:
                  summary: A carrier under review
                  value:
                    api_key: "ls_live_..."
                    principal:
                      key_id: 0b1c2d3e-4f50-4617-8293-a4b5c6d7e8f9
                      key_prefix: ls_live_1a2b
                      role: carrier
                      verification: needs_review
                      carrier: { legal_name: EXAMPLE TRUCKING LLC, mc_number: "123456", dot_number: "1234567", contact_email: dispatch@exampletrucking.example.com }
                    message: "your carrier account is under review; until Lanesurf verifies it you can manage trucks and lanes and list your bids, but you can't search loads, view loads, get matches or bid"
                    next_step: "Give the person api_key now to store securely (for example as this connector's Authorization: Bearer header): it is shown only once; never repeat it afterwards. Tell the person: your carrier account is under review; ... Meanwhile post trucks with post_truck, save lanes with save_lane and follow bids with list_bids."
                broker:
                  summary: A new brokerage
                  value:
                    api_key: "ls_live_..."
                    principal: { key_id: 1c2d3e4f-5061-4728-93a4-b5c6d7e8f9a0, key_prefix: ls_live_9z8y, role: broker, broker_id: 6a7b8c9d-0e1f-4a2b-8c3d-4e5f6a7b8c9d, rep_name: Dana Reyes }
                    activated: false
                    message: "Lanesurf reviews new brokerages before carrier outreach starts; loads you post are saved and start sourcing once outreach is on"
                    next_step: "Give the person api_key now to store securely ... Meanwhile review the brokerage setup with get_broker_setup and fill in working hours, payment terms and CC emails with update_broker_setup."
        "400":
          description: "`invalid_request`: e.g. the code isn't 8 digits, a website that isn't http(s), a job title over 100 characters."
          content:
            application/json:
              schema: { $ref: "#/components/schemas/SignupError" }
        "404":
          description: "`not_found`: no such signup."
          content:
            application/json:
              schema: { $ref: "#/components/schemas/SignupError" }
        "409":
          description: "`existing_customer`: the brokerage already works with Lanesurf; don't sign up, their Lanesurf admin or account manager gives them access. `already_redeemed`: this signup was already completed and its key is in use. `broker_name_taken`: a brokerage with this `company_name` is already set up with Lanesurf. `broker_name_mismatch`: this signup already created the brokerage under another name."
          content:
            application/json:
              schema: { $ref: "#/components/schemas/SignupError" }
        "410":
          description: "`signup_expired`: the code expired (10 minutes) or ran out of attempts (5). Start again."
          content:
            application/json:
              schema: { $ref: "#/components/schemas/SignupError" }
        "422":
          description: |
            `missing_fields` (brokers: `name`, `job_title` or `company_name` is missing; no attempt was used) or
            `invalid_code` (wrong code; the message says how many attempts are left).
          content:
            application/json:
              schema: { $ref: "#/components/schemas/SignupError" }
              examples:
                missing_fields:
                  value:
                    error: "to set up the brokerage, send job_title, company_name with the code; no attempt was used"
                    code: missing_fields
                    missing: [job_title, company_name]
                    next_step: "Ask the person for job_title, company_name, then call verify_signup again with the same signup_id and code plus name, job_title and company_name (phone and website optional). No attempt was used."
                invalid_code:
                  value:
                    error: "wrong code; 4 attempt(s) left"
                    code: invalid_code
                    next_step: "Ask the person to check the code in the email and call verify_signup again with the same signup_id."
        "429": { $ref: "#/components/responses/SignupRateLimited" }
        "500": { $ref: "#/components/responses/SignupInternal" }
        "503": { $ref: "#/components/responses/SignupUnavailable" }

  /v1/me:
    get:
      tags: [Authentication]
      summary: Who this key belongs to
      operationId: getMe
      x-mint:
        metadata:
          title: Who am I
          sidebarTitle: Who am I
          description: See who this API key belongs to.
        content: |
          Returns the principal behind the key: its role, and for carriers the verification and carrier
          record; for brokers the brokerage id and rep name. Works for both roles.
      description: |
        Returns the principal behind the key: its role, and for carriers the verification and carrier record;
        for brokers the brokerage id and rep name. Works for both roles.
      responses:
        "200":
          description: The principal behind this key.
          headers:
            Cache-Control: { $ref: "#/components/headers/NoStore" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Principal" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "500": { $ref: "#/components/responses/Internal" }
        "503": { $ref: "#/components/responses/AuthUnavailable" }

  # --------------------------------------------------------------- broker setup
  /v1/broker/setup:
    get:
      tags: [Broker setup]
      summary: Your brokerage's setup
      operationId: getBrokerSetup
      x-mint:
        metadata:
          title: Get brokerage setup
          sidebarTitle: Get setup
          description: What Lanesurf's agents tell carriers about your brokerage, and whether outreach is on.
        content: |
          What Lanesurf's agents tell carriers about the brokerage: company name, MC/DOT, working hours,
          payment terms, who is copied on carrier emails, `ask_first_pricing`, and whether carriers' AI agents
          can see your loads (`loads_visible_to_carriers`). `outreach.activated` is
          `false` for a brokerage that signed up through the API until Lanesurf reviews it and turns carrier
          outreach on.
      description: |
        What Lanesurf's agents tell carriers about the brokerage (company name, MC/DOT, working hours, payment
        terms, CC emails, ask_first_pricing), whether carriers' AI agents see its loads
        (loads_visible_to_carriers) and whether carrier outreach is on. outreach.activated is false
        for a brokerage that signed up through the API until Lanesurf reviews it.
      responses:
        "200":
          description: The setup.
          headers:
            Cache-Control: { $ref: "#/components/headers/NoStore" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/BrokerSetup" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/WrongRole" }
        "404":
          description: "`not_found`: Lanesurf has no setup for this brokerage yet."
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "500": { $ref: "#/components/responses/Internal" }
        "502": { $ref: "#/components/responses/Upstream" }
        "503":
          description: "`unavailable`: Lanesurf couldn't check the API key right now (the key may be fine; don't ask for a new one): wait `Retry-After` (30) seconds and send the same request again. Or brokerage setup can't run right now; retry later."
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
    patch:
      tags: [Broker setup]
      summary: Change your brokerage's setup
      operationId: updateBrokerSetup
      x-mint:
        metadata:
          title: Update brokerage setup
          sidebarTitle: Update setup
          description: Change company name, working hours, payment terms, CC emails, ask_first_pricing or carrier visibility.
        content: |
          Send only the fields that change. Only `company_name`, `working_hours`, `payment_terms`, `cc_emails`
          (replaces the list), `ask_first_pricing` and `loads_visible_to_carriers` (whether carriers' AI agents
          can search and bid on your loads; off until you turn it on) can change. Any other field, including every outreach
          switch, is refused with `400 invalid_request`: Lanesurf turns carrier outreach on after review.
      description: |
        Send only the fields that change: company_name, working_hours, payment_terms, cc_emails (replaces the
        list), ask_first_pricing and loads_visible_to_carriers. Any other field, including every outreach switch, answers 400
        invalid_request; Lanesurf turns carrier outreach on after review.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/BrokerSetupPatch" }
            example: { working_hours: "Mon-Fri 7am-6pm CT", payment_terms: "Net 30; quick pay 2%", cc_emails: [ops@acmefreight.example.com] }
      responses:
        "200":
          description: The setup after the change.
          headers:
            Cache-Control: { $ref: "#/components/headers/NoStore" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/BrokerSetup" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/WrongRole" }
        "404":
          description: "`not_found`: Lanesurf has no setup for this brokerage yet."
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "409":
          description: "`broker_name_taken`: another brokerage on Lanesurf already uses this `company_name`. Send the brokerage's own legal or trading name."
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "500": { $ref: "#/components/responses/Internal" }
        "502": { $ref: "#/components/responses/Upstream" }
        "503":
          description: "`unavailable`: Lanesurf couldn't check the API key right now (the key may be fine; don't ask for a new one): wait `Retry-After` (30) seconds and send the same request again. Or brokerage setup can't run right now; retry later."
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }

  # --------------------------------------------------------------- carrier side
  /v1/carrier/trucks:
    post:
      tags: [Carrier trucks]
      summary: Post an available truck
      operationId: postTruck
      x-mint:
        metadata:
          title: Post a truck
          sidebarTitle: Post a truck
          description: Say where a truck is, when it's free, and where it wants to go.
        content: |
          Post a truck as fields: where it is, when it's free, the equipment, and where it wants to go.
          Required: `equipment_type`, `current_city`, `current_state` and `available_from`.

          - `422 missing_fields` lists what's missing. Ask the dispatcher only for those and post again.
          - `400 invalid_request` when a value is wrong: a state that isn't a US state or Canadian province code
            (e.g. `TX`, `ON`), `available_from` more than 60 days out, `available_to` before `available_from`, or a
            window that is already over.
          - `422 idempotency_key_reused`: this `Idempotency-Key` already posted a different truck. Nothing was posted.

          Cities are trimmed and states upper-cased (`" Dallas "`, `tx` → `Dallas`, `TX`).

          The post lapses at `available_to`, or 72 hours after `available_from` if there's no end. Then its
          status is `expired`. Use `GET /v1/carrier/trucks/{truck_id}/matches` to find loads for it.
      description: |
        Post a truck as fields. Required: `equipment_type`, `current_city`, `current_state`, `available_from`.
        422 `missing_fields` lists what's missing; 422 `idempotency_key_reused` when the key already posted a
        different truck. 400 `invalid_request` when a value is wrong: a state that isn't a US state or Canadian
        province code, `available_from` more than 60 days out, `available_to` before `available_from`, or a
        window that is already over. The post lapses at `available_to`, or 72 hours after `available_from`
        if there's no end. While the truck is active, Lanesurf's reps email or call a verified carrier about
        matching loads (near the truck, same equipment, inside its window, headed toward its destinations).
      parameters:
        - { $ref: "#/components/parameters/TruckIdempotencyKey" }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/TruckInput" }
            example:
              equipment_type: Reefer
              trailer_length_ft: 53
              current_city: Dallas
              current_state: TX
              available_from: "2026-10-23T08:00:00-05:00"
              destinations: [{ city: Atlanta, state: GA }, { state: TN }]
              rate_ask: { amount: 2.10, unit: per_mile }
      responses:
        "201":
          description: The truck is posted.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Truck" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/WrongRole" }
        "422":
          description: "`missing_fields`: required fields are missing. `idempotency_key_reused`: the key already posted a different truck; nothing was posted."
          content:
            application/json:
              schema:
                oneOf:
                  - { $ref: "#/components/schemas/MissingFields" }
                  - type: object
                    required: [error, code]
                    properties:
                      error: { type: string }
                      code: { type: string, enum: [idempotency_key_reused] }
                      next_step: { $ref: "#/components/schemas/NextStep" }
              examples:
                missing_fields:
                  value:
                    error: required fields are missing; send them and try again
                    code: missing_fields
                    missing: [current_city, available_from]
                idempotency_key_reused:
                  value:
                    error: this idempotency_key was already used for a different truck; use a new key for a new truck
                    code: idempotency_key_reused
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "500": { $ref: "#/components/responses/Internal" }
        "502": { $ref: "#/components/responses/Upstream" }
        "503": { $ref: "#/components/responses/CarrierUnavailable" }
    get:
      tags: [Carrier trucks]
      summary: List my trucks
      operationId: listTrucks
      x-mint:
        metadata:
          title: List my trucks
          sidebarTitle: List my trucks
          description: Your posted trucks, newest first.
        content: |
          This key's trucks, newest first (up to 200). By default only active ones; `active=false` lists all,
          including removed and expired trucks.
      description: |
        This key's trucks, newest first (up to 200). By default only active ones; `active=false` lists all,
        including removed and expired trucks.
      parameters:
        - name: active
          in: query
          description: "`true` (default): only active trucks. `false`: every truck."
          schema: { type: boolean, default: true }
      responses:
        "200":
          description: The carrier's trucks.
          content:
            application/json:
              schema:
                type: object
                required: [trucks]
                properties:
                  trucks:
                    type: array
                    items: { $ref: "#/components/schemas/Truck" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/WrongRole" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "500": { $ref: "#/components/responses/Internal" }
        "502": { $ref: "#/components/responses/Upstream" }
        "503": { $ref: "#/components/responses/CarrierUnavailable" }

  /v1/carrier/trucks/{truck_id}:
    parameters:
      - { $ref: "#/components/parameters/TruckID" }
    get:
      tags: [Carrier trucks]
      summary: Get a truck
      operationId: getTruck
      x-mint:
        metadata:
          title: Get a truck
          sidebarTitle: Get a truck
          description: One of your trucks, in any status.
        content: |
          One of this key's trucks, in any status. Another key's truck answers `404`.
      description: One of this key's trucks, in any status. Another key's truck answers 404.
      responses:
        "200":
          description: The truck.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Truck" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/WrongRole" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "500": { $ref: "#/components/responses/Internal" }
        "502": { $ref: "#/components/responses/Upstream" }
        "503": { $ref: "#/components/responses/CarrierUnavailable" }
    patch:
      tags: [Carrier trucks]
      summary: Update a truck
      operationId: updateTruck
      x-mint:
        metadata:
          title: Update a truck
          sidebarTitle: Update a truck
          description: Change a truck's location, dates or details.
        content: |
          Send only what changed. Fields you leave out stay as they are. New dates move `expires_at`, with the
          same date rules as posting. A removed truck can't be updated (`404`); post a new one.
      description: |
        Send only what changed; fields you leave out stay as they are. New dates move `expires_at`, with the
        same date rules as posting. A removed truck can't be updated (404).
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/TruckInput" }
            example: { current_city: Memphis, current_state: TN, available_from: "2026-10-24T06:00:00-05:00" }
      responses:
        "200":
          description: The updated truck.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Truck" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/WrongRole" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "500": { $ref: "#/components/responses/Internal" }
        "502": { $ref: "#/components/responses/Upstream" }
        "503": { $ref: "#/components/responses/CarrierUnavailable" }
    delete:
      tags: [Carrier trucks]
      summary: Remove a truck
      operationId: removeTruck
      x-mint:
        metadata:
          title: Remove a truck
          sidebarTitle: Remove a truck
          description: The truck got loaded or isn't available.
        content: |
          Take the truck down. Its status becomes `removed` and `removed_reason` is set. A truck that is
          already removed answers `404`.
      description: |
        Take the truck down. Its status becomes `removed` and `removed_reason` is set. A truck that is already
        removed answers 404.
      parameters:
        - name: reason
          in: query
          description: Why it came down. Defaults to `other`.
          schema: { type: string, enum: [loaded, unavailable, other], default: other }
      responses:
        "204": { description: Removed. }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/WrongRole" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "500": { $ref: "#/components/responses/Internal" }
        "502": { $ref: "#/components/responses/Upstream" }
        "503": { $ref: "#/components/responses/CarrierUnavailable" }

  /v1/carrier/trucks/{truck_id}/matches:
    parameters:
      - { $ref: "#/components/parameters/TruckID" }
    get:
      tags: [Carrier loads]
      summary: Loads that fit this truck
      operationId: truckMatches
      x-mint:
        metadata:
          title: Loads for a truck
          sidebarTitle: Loads for a truck
          description: Loads that fit one of your posted trucks.
        content: |
          Loads that fit an active truck:

          - pickup near the truck's `current_city`: within 50, 100 and 150 miles, then anywhere in its
            `current_state` (just the state when the city can't be placed),
          - equipment that matches the truck's `equipment_type`,
          - pickup date inside the truck's window: `available_from` to `available_to`, or to 48 hours after
            `available_from` if there's no end.

          Closest first, each with `deadhead_miles` and `match`. Within each distance, loads going to one of the
          truck's `destinations` states come first. A removed or expired truck gets an empty list. `total` is the
          number of loads returned.

          Only **verified** carriers can get matches (`403 verification_required`).
      description: |
        Loads that fit an active truck: pickup near its `current_city` (within 50, 100 and 150 miles, then its
        `current_state`), equipment that matches its `equipment_type`, and a pickup date from `available_from`
        to `available_to` (or 48 hours after `available_from` if there's no end). Closest first, each with
        `deadhead_miles` and `match`; within each distance, loads going to one of the truck's `destinations`
        states come first. A removed or expired truck gets an empty list. `total` is the number of
        loads returned. Only verified carriers can get matches (403 `verification_required`).
      parameters:
        - { $ref: "#/components/parameters/Limit" }
      responses:
        "200":
          description: Matching loads.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/CarrierLoadList" }
        "400":
          description: "`invalid_request`: `limit` isn't a whole number from 1 to 100."
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/VerifiedCarrierOnly" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "500": { $ref: "#/components/responses/Internal" }
        "502": { $ref: "#/components/responses/Upstream" }
        "503": { $ref: "#/components/responses/CarrierUnavailable" }

  /v1/carrier/lanes:
    post:
      tags: [Carrier lanes]
      summary: Save a lane
      operationId: createLane
      x-mint:
        metadata:
          title: Save a lane
          sidebarTitle: Save a lane
          description: Save a lane the carrier runs.
        content: |
          Save a lane the carrier runs regularly: its terminals, with no specific truck. `origin_state` and
          `destination_state` are required (2-letter codes); cities and equipment are optional.

          Safe to repeat: saving the same lane again (same states, cities and equipment, any letter case)
          returns the lane already saved.
      description: |
        Save a lane the carrier runs regularly (its terminals). `origin_state` and `destination_state` are
        required 2-letter codes; cities and equipment are optional. Saving the same lane again (same states,
        cities and equipment, any letter case) returns the lane already saved.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/LaneInput" }
            example: { origin_city: Dallas, origin_state: TX, destination_city: Atlanta, destination_state: GA, equipment_type: Reefer }
      responses:
        "200":
          description: The lane (new, or the one already saved).
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Lane" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/WrongRole" }
        "422":
          description: "`origin_state` or `destination_state` is missing (`missing_fields`)."
          content:
            application/json:
              schema: { $ref: "#/components/schemas/MissingFields" }
              example:
                error: required fields are missing; send them and try again
                code: missing_fields
                missing: [destination_state]
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "500": { $ref: "#/components/responses/Internal" }
        "502": { $ref: "#/components/responses/Upstream" }
        "503": { $ref: "#/components/responses/CarrierUnavailable" }
    get:
      tags: [Carrier lanes]
      summary: List my lanes
      operationId: listLanes
      x-mint:
        metadata:
          title: List my lanes
          sidebarTitle: List my lanes
          description: Your saved lanes, newest first.
        content: |
          This key's saved lanes, newest first (up to 500). Removed lanes are not listed.
      description: This key's saved lanes, newest first (up to 500). Removed lanes are not listed.
      responses:
        "200":
          description: The carrier's lanes.
          content:
            application/json:
              schema:
                type: object
                required: [lanes]
                properties:
                  lanes:
                    type: array
                    items: { $ref: "#/components/schemas/Lane" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/WrongRole" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "500": { $ref: "#/components/responses/Internal" }
        "502": { $ref: "#/components/responses/Upstream" }
        "503": { $ref: "#/components/responses/CarrierUnavailable" }

  /v1/carrier/lanes/{lane_id}:
    parameters:
      - name: lane_id
        in: path
        required: true
        description: The `lane_id` from `POST /v1/carrier/lanes`.
        schema: { type: string, format: uuid }
    delete:
      tags: [Carrier lanes]
      summary: Remove a lane
      operationId: removeLane
      x-mint:
        metadata:
          title: Remove a lane
          sidebarTitle: Remove a lane
          description: The carrier no longer runs this lane.
        content: |
          Remove a saved lane. A lane that is already removed, or is another key's, answers `404`.
      description: Remove a saved lane. A lane that is already removed, or is another key's, answers 404.
      responses:
        "204": { description: Removed. }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/WrongRole" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "500": { $ref: "#/components/responses/Internal" }
        "502": { $ref: "#/components/responses/Upstream" }
        "503": { $ref: "#/components/responses/CarrierUnavailable" }

  /v1/carrier/loads/search:
    get:
      tags: [Carrier loads]
      summary: Search loads near a ZIP or city, or by state
      operationId: searchLoads
      x-mint:
        metadata:
          title: Search loads
          sidebarTitle: Search loads
          description: Find loads near a ZIP or city, or by state, equipment and pickup date.
        content: |
          Active loads from Lanesurf brokers that show their loads to carriers. Give `origin_zip` (best),
          `origin_city` with `origin_state`, `origin_state`, or `destination_state`; a search with none answers `400`.

          **Near a ZIP or city** (`origin_zip`, or `origin_city` + `origin_state`), loads come closest first:

          1. same ZIP (`match: same_zip`),
          2. within 50, 100 and 150 miles (`within_50_miles`, `within_100_miles`, `within_150_miles`),
          3. anywhere else in the same state (`same_state`).

          Each load has `deadhead_miles`: straight-line miles from the ZIP or city to the pickup (left out when
          the pickup can't be placed). An unknown `origin_zip` answers `400` unless `origin_state` is also given.

          **By state** (`origin_state` alone): that state's pickups, newest first.

          - `destination_city` matches the last delivery exactly (any letter case).
          - `equipment_type` matches when the load's equipment contains it (e.g. `Reefer`).
          - `pickup_date` (`YYYY-MM-DD`) is one day; `pickup_from` / `pickup_to` give a range and win over `pickup_date`.
          - `total` is the number of loads returned.

          Results never include the broker's name, contacts or max rate. `posted_rate` is shown only when the
          broker priced the load (`rate_type: priced`). To go for a load, bid on it with `POST /v1/carrier/loads/{load_id}/bids`.

          Only **verified** carriers can search (`403 verification_required`).
      description: |
        Active loads from Lanesurf brokers that show their loads to carriers. Give `origin_zip` (best),
        `origin_city` with `origin_state`, `origin_state`, or `destination_state`; a search with none answers
        400. Near a ZIP or city, loads come closest first: same ZIP, then within 50, 100 and 150 miles, then
        the same state, each with `deadhead_miles` and `match`. `origin_state` alone lists that state's
        pickups, newest first. `destination_city` matches the last delivery exactly (any letter case). `equipment_type` matches when the load's
        equipment contains it. `pickup_date` is one day; `pickup_from` / `pickup_to` give a range and win over
        `pickup_date`. `total` is the number of loads returned. Results never include the broker's name,
        contacts or max rate; `posted_rate` only when the broker priced the load. Only verified carriers can
        search (403 `verification_required`).
      parameters:
        - { name: origin_zip, in: query, description: "5-digit ZIP where the truck is. Ranks loads near it.", schema: { type: string, pattern: "^[0-9]{5}$", example: "79901" } }
        - { name: origin_state, in: query, description: "2-letter state. With `origin_city`: loads near the city. Alone: that state's pickups.", schema: { type: string, minLength: 2, maxLength: 2, example: TX } }
        - { name: origin_city, in: query, description: "Where the truck is, with `origin_state`. Ranks loads near it; a city we can't place matches only that exact city.", schema: { type: string, example: Dallas } }
        - { name: destination_state, in: query, description: "2-letter delivery state.", schema: { type: string, minLength: 2, maxLength: 2, example: GA } }
        - { name: destination_city, in: query, description: "Last delivery city (exact, any case).", schema: { type: string } }
        - { name: equipment_type, in: query, description: Matches loads whose equipment contains this text., schema: { type: string, example: Reefer } }
        - { name: pickup_date, in: query, description: "One pickup day (YYYY-MM-DD).", schema: { type: string, format: date } }
        - { name: pickup_from, in: query, description: "Earliest pickup day (YYYY-MM-DD). Wins over `pickup_date`.", schema: { type: string, format: date } }
        - { name: pickup_to, in: query, description: "Latest pickup day (YYYY-MM-DD). Wins over `pickup_date`. Before `pickup_from` answers 400.", schema: { type: string, format: date } }
        - { $ref: "#/components/parameters/Limit" }
      responses:
        "200":
          description: Matching loads.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/CarrierLoadList" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/VerifiedCarrierOnly" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "500": { $ref: "#/components/responses/Internal" }
        "502": { $ref: "#/components/responses/Upstream" }
        "503": { $ref: "#/components/responses/CarrierUnavailable" }

  /v1/carrier/loads/{load_id}:
    parameters:
      - { $ref: "#/components/parameters/CarrierLoadID" }
    get:
      tags: [Carrier loads]
      summary: One load's details
      operationId: carrierGetLoad
      x-mint:
        metadata:
          title: Get a load
          sidebarTitle: Get a load
          description: One load's details, without broker identity or contacts.
        content: |
          One load as a carrier sees it. A load that is gone, inactive, deleted, or from a broker that is left
          out of carrier search answers `404`.

          Only **verified** carriers can read a load (`403 verification_required`).
      description: |
        One load as a carrier sees it. A load that is gone, inactive, deleted, or from a broker that is left
        out of carrier search answers 404. Only verified carriers can read a load (403 `verification_required`).
      responses:
        "200":
          description: The load.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/CarrierLoad" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/VerifiedCarrierOnly" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "500": { $ref: "#/components/responses/Internal" }
        "502": { $ref: "#/components/responses/Upstream" }
        "503": { $ref: "#/components/responses/CarrierUnavailable" }

  /v1/carrier/loads/{load_id}/bids:
    parameters:
      - { $ref: "#/components/parameters/CarrierLoadID" }
    post:
      tags: [Carrier loads]
      summary: Bid on a load
      operationId: bidOnLoad
      x-mint:
        metadata:
          title: Bid on a load
          sidebarTitle: Bid on a load
          description: Tell the broker you want to run this load.
        content: |
          Tells the broker who owns the load that this carrier wants it, optionally at a rate. The bid shows up
          on the broker's Lanesurf dashboard as an inbound bid, with the carrier's MC/DOT and dispatch contact. The
          broker's team follows up with the dispatcher by email or phone. **A bid is not a booking**: Lanesurf
          does not book the load. Confirm with the dispatcher before calling this. Bids that look like a mistake
          (for example an absurd rate) may be held for review instead of reaching the broker straight away.

          - Only **verified** carriers can bid (`403 verification_required`).
          - The body is optional. No body, an empty body or `{}` means "I want to run this, send me your rate".
          - `rate` is all-in USD. Leave it out to ask for the broker's rate.
          - `truck_id` must be one of this key's active trucks.
          - **One bid per carrier per load**, across all of that carrier's keys. The carrier is its DOT number,
            or its MC number if it has no DOT. **Sending again updates that bid** (latest terms win): the reply
            is `200` with `action: updated`, or `action: unchanged` if nothing changed. Fields you leave out
            keep their current value. The broker sees the new rate on the same bid.
          - Once the broker has accepted the bid, it can't change (`409 bid_decided`). A declined bid can be
            sent again: a new rate reopens it for the broker.
          - If a bid shows `incomplete` in `GET /v1/carrier/bids`, it was recorded but never reached the broker.
            Send the bid again to finish it. Within 2 minutes only the key that placed it can, and its original rate,
            truck and note are used; after that any key of the carrier can, with the new request's terms.

          Follow the outcome with `GET /v1/carrier/bids`.
      description: |
        Tells the broker who owns the load that this carrier wants it, optionally at a rate. The bid shows up
        on the broker's Lanesurf dashboard as an inbound bid, with the carrier's MC/DOT and dispatch contact. The
        broker's team follows up with the dispatcher by email or phone. A bid is not a booking: Lanesurf does
        not book the load. Confirm with the dispatcher first. Bids that look like a mistake (e.g. an absurd rate)
        may be held for review.

        Only verified carriers can bid (403 `verification_required`). The body is optional: no body, an empty
        body or `{}` means "I want to run this, send me your rate". `rate` is all-in USD; leave it out to ask
        for the broker's rate. `truck_id` must be one of this key's active trucks. One bid per carrier per
        load across all of that carrier's keys (the carrier is its DOT number, or its MC number if it has no
        DOT): sending again updates that bid (200, `action: updated` or `unchanged`), until the broker has
        accepted it (409 `bid_decided`). A declined bid can be sent again; a new rate reopens it. Follow the
        outcome with `GET /v1/carrier/bids`.
      requestBody:
        required: false
        description: "Optional. No body, an empty body or `{}` bids with no rate, truck or note, which asks for the broker's rate."
        content:
          application/json:
            schema: { $ref: "#/components/schemas/BidInput" }
            example: { rate: 1900, truck_id: "c1d2e3f4-0000-4000-8000-000000000001", note: "Empty in Dallas Fri 8am" }
      responses:
        "201":
          description: "Bid placed (`action: created`). Its status is `submitted`."
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Bid" }
              example: { bid_id: "b7e1c2d3-0000-4000-8000-0000000000b1", load_id: "8f14e45f-0000-4000-8000-0000000000a1", rate: 1900, truck_id: "c1d2e3f4-0000-4000-8000-000000000001", note: "Empty in Dallas Fri 8am", status: submitted, created_at: "2026-10-20T09:00:00Z", action: created }
        "200":
          description: |
            This carrier already had a bid on this load: it was updated with the new terms (`action: updated`),
            or nothing changed (`action: unchanged`). Same `bid_id`; the broker sees the new rate on it.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Bid" }
              example: { bid_id: "b7e1c2d3-0000-4000-8000-0000000000b1", load_id: "8f14e45f-0000-4000-8000-0000000000a1", rate: 1850, status: submitted, created_at: "2026-10-20T09:00:00Z", action: updated }
        "400":
          description: |
            `invalid_request`: `rate` isn't a positive amount up to 1,000,000, `note` is over 1,000 bytes,
            or `truck_id` isn't one of this key's active trucks.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/VerifiedCarrierOnly" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409":
          description: |
            - `bid_decided`: the broker already accepted this carrier's bid, so it can't change.
            - `already_bid`: another key of the same carrier is placing a bid on this load right now. Try again
              in a few seconds; your request then updates that bid.
            - `bid_update_limit`: this bid was already changed the most times allowed (5). It stands as it is.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
              examples:
                decided:
                  summary: The broker already accepted
                  value: { error: "the broker already accepted your bid on this load; it can't be changed", code: bid_decided }
                in_progress:
                  summary: Another key's bid is still being placed
                  value: { error: "you already bid on this load (by another key of your carrier, still being placed; try again shortly)", code: already_bid }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "500": { $ref: "#/components/responses/Internal" }
        "502": { $ref: "#/components/responses/Upstream" }
        "503": { $ref: "#/components/responses/CarrierUnavailable" }

  /v1/carrier/bids:
    get:
      tags: [Carrier loads]
      summary: My bids and their status
      operationId: listBids
      x-mint:
        metadata:
          title: My bids
          sidebarTitle: My bids
          description: Your bids and where each one stands.
        content: |
          This carrier's bids (from any of its keys), newest first (up to 200). This is how a carrier learns the outcome: poll it.
          Keys under review (`needs_review`) can call this too.

          | `status` | Meaning |
          |---|---|
          | `submitted` | The broker has it. The load is still open |
          | `accepted` | The broker accepted this bid |
          | `declined` | The broker passed on this bid. Send it again with a new rate to reopen it |
          | `load_closed` | The load was covered some other way, closed, or booked to someone else, and the broker made no choice on this bid |
          | `incomplete` | The bid was recorded but never reached the broker (a failure mid-bid). `POST` it again to finish it (see [Bidding](/carriers/bid-on-a-load)) |

          The broker's choice wins: an `accepted` or `declined` bid keeps that status after the load closes.
      description: |
        This carrier's bids (from any of its keys), newest first (up to 200). This is how a carrier learns the outcome: poll it.
        Keys under review (`needs_review`) can call this too. `submitted`: the broker has it and the load is still
        open. `accepted` / `declined`: the broker's choice on this bid. `load_closed`:
        the load was covered some other way, closed, or booked to someone else, and the broker made no choice
        on this bid. `incomplete`: the bid was recorded but never reached the broker (a failure mid-bid); POST
        it again to finish it (within 2 minutes from the same key with its original terms, after that from any of
        the carrier's keys with the new terms).
      responses:
        "200":
          description: The carrier's bids, newest first.
          content:
            application/json:
              schema:
                type: object
                required: [bids]
                properties:
                  bids:
                    type: array
                    items: { $ref: "#/components/schemas/Bid" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/WrongRole" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "500": { $ref: "#/components/responses/Internal" }
        "502": { $ref: "#/components/responses/Upstream" }
        "503": { $ref: "#/components/responses/CarrierUnavailable" }

  /v1/carrier/outreach:
    get:
      tags: [Carrier loads]
      summary: Lanesurf outreach I received
      operationId: carrierOutreach
      x-mint:
        metadata:
          title: My outreach
          sidebarTitle: My outreach
          description: The loads Lanesurf contacted you about in the last 30 days.
        content: |
          Up to 50 loads Lanesurf contacted this carrier about in the last 30 days (by email, call or text), newest
          first: the brokerage the outreach came from, the latest channel and when, the load (lane, dates,
          equipment), and `open`: whether the load is still on offer, so you can bid on it with
          `POST /v1/carrier/loads/{load_id}/bids`. A load that is no longer open keeps its lane and dates but
          not its rate.

          Outreach is matched by the carrier's MC and the email and phone the key was verified with. Only
          **verified** carriers can read it (`403 verification_required`).
      description: |
        Up to 50 loads Lanesurf contacted this carrier about in the last 30 days, newest first: brokerage, latest
        channel and time, the load, and open (still on offer, so it can be bid on). Matched by the carrier's MC
        and the email and phone the key was verified with. Verified carriers only.
      responses:
        "200":
          description: The loads, newest first.
          content:
            application/json:
              schema:
                type: object
                required: [outreach]
                properties:
                  outreach:
                    type: array
                    items: { $ref: "#/components/schemas/CarrierOutreach" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/VerifiedCarrierOnly" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "500": { $ref: "#/components/responses/Internal" }
        "502": { $ref: "#/components/responses/Upstream" }
        "503": { $ref: "#/components/responses/CarrierUnavailable" }

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: ls_live_<key>
      description: One key per broker rep or per carrier signup. Keep it secret.

  parameters:
    LoadRef:
      name: id
      in: path
      required: true
      description: The load's `load_id`, or its `tms_id`, from `POST /v1/loads`.
      schema: { type: string, example: 3f2a9c4e-1d7b-4c55-8a7e-0b9d6e5f1234 }
    SessionID:
      name: session_id
      in: path
      required: true
      description: The offer's `session_id` from `GET /v1/loads/{id}`.
      schema: { type: string, format: uuid }
    TruckID:
      name: truck_id
      in: path
      required: true
      description: The `truck_id` from `POST /v1/carrier/trucks`.
      schema: { type: string, format: uuid }
    CarrierLoadID:
      name: load_id
      in: path
      required: true
      description: The `load_id` from a search or matches result.
      schema: { type: string, format: uuid }
    Limit:
      name: limit
      in: query
      description: How many loads to return, a whole number from 1 to 100. Defaults to 25. Anything else answers 400.
      schema: { type: integer, default: 25, minimum: 1, maximum: 100 }
    TruckIdempotencyKey:
      name: Idempotency-Key
      in: header
      required: false
      description: |
        Optional. Up to 200 characters, a new UUID per truck post. Posting the same truck again with the same key
        (from the same API key) returns the existing truck, with the same `truck_id`, instead of a duplicate. The
        answer is `201` either way. The same key with a different truck (other equipment, place or start) answers
        `422 idempotency_key_reused` and posts nothing.
      schema: { type: string, maxLength: 200 }
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: true
      description: |
        Required. Any string up to 200 characters, unique per load (a UUID works). Scoped to the API key that sends
        it. A retry of the same load with the same key from the same API key answers 200 with the original
        load, so a retry never creates a second one. A different load with a used key answers 422
        `idempotency_key_reused`; a retry while the first post is still running answers 409
        `idempotency_in_progress`. The same key from a different API key, even at the same brokerage, creates
        a different load.
      schema: { type: string, maxLength: 200, example: 6b1f0c2e-load-4471 }

  headers:
    NoStore:
      description: Responses that carry secrets are never cached.
      schema: { type: string, example: no-store }
    RetryAfter:
      description: Seconds to wait before trying again.
      schema: { type: integer }

  responses:
    BadRequest:
      description: "`invalid_request`: the request is malformed or a value is invalid. The message says which. Don't retry unchanged."
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    Unauthorized:
      description: "`missing_api_key` (no `Authorization` header) or `invalid_api_key` (wrong, malformed or revoked key)."
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          example: { error: invalid API key, code: invalid_api_key }
    WrongRole:
      description: "`wrong_role`: this key's role can't use this route (a carrier key on a broker route, or the reverse)."
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          example: { error: "this API key can't use broker routes", code: wrong_role }
    NotFound:
      description: "`not_found`: no such load, offer, truck, lane or signup for this key."
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    AlreadyCovered:
      description: |
        `already_covered`: the load can't take this decision. Another offer was already accepted, the load was
        marked covered, or it was deleted. A load can't be both accepted and marked covered. Read the load again.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          example: { error: an offer on this load is already accepted, code: already_covered }
    InvalidState:
      description: |
        `invalid_state`: the load's state doesn't allow this change. It was deleted, it is covered (reopen it
        first), it is paused (resume it before changing its stops), or its first pickup has passed. `error`
        says which; follow `next_step`.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          example: { error: "the load is covered; reopen it before editing", code: invalid_state, next_step: "Tell the person why (error). A covered load needs reopen_load…" }
    TooManyRequests:
      description: |
        `rate_limited`: too many requests. Signup has limits per contact, per carrier and per IP (the 300 per 10 minutes
        limit groups IPv6 addresses by /64). Every authenticated `/v1` route is limited to 120 requests per minute
        per API key, counted on each Lanesurf server. Wait `Retry-After`
        seconds, then retry.
      headers:
        Retry-After: { $ref: "#/components/headers/RetryAfter" }
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          example: { error: "too many requests for this API key; slow down", code: rate_limited }
    VerifiedCarrierOnly:
      description: |
        `verification_required`: only verified carriers can do this. A carrier account under review
        (`needs_review`) can post and manage trucks and lanes and list its own bids, but can't search loads,
        read a load, get truck matches, list outreach or bid. A broker key gets `wrong_role`.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          example:
            error: "your carrier account is under review; until Lanesurf verifies it you can manage trucks and lanes and list your bids, but you can't search loads, view loads, get matches or bid"
            code: verification_required
    SignupUnavailable:
      description: |
        `unavailable`: signup can't run right now (code sending or a Lanesurf system is down). Retry later;
        when `Retry-After` is set, wait that many seconds. If the carrier check can't run, signup still works:
        the carrier is `needs_review`.
      headers:
        Retry-After: { $ref: "#/components/headers/RetryAfter" }
      content:
        application/json:
          schema: { $ref: "#/components/schemas/SignupError" }
    SignupRateLimited:
      description: "`rate_limited`: too many signups for this email, this MC/DOT number or this network. Wait `Retry-After` seconds."
      headers:
        Retry-After: { $ref: "#/components/headers/RetryAfter" }
      content:
        application/json:
          schema: { $ref: "#/components/schemas/SignupError" }
    SignupInternal:
      description: "`internal`: unexpected error. Safe to retry later."
      content:
        application/json:
          schema: { $ref: "#/components/schemas/SignupError" }
    CarrierUnavailable:
      description: "`unavailable`: Lanesurf couldn't check the API key right now (the key may be fine; don't ask for a new one): wait `Retry-After` (30) seconds and send the same request again. Or carrier routes can't run right now; retry later."
      headers:
        Retry-After: { $ref: "#/components/headers/RetryAfter" }
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    AuthUnavailable:
      description: "`unavailable`: Lanesurf couldn't check the API key right now (the key may be fine; don't ask for a new one): wait `Retry-After` (30) seconds and send the same request again."
      headers:
        Retry-After: { $ref: "#/components/headers/RetryAfter" }
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    Internal:
      description: "`internal`: unexpected error. Safe to retry later."
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    Upstream:
      description: "`upstream_error`: a Lanesurf system behind the API failed. Safe to retry later."
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          example: { error: a Lanesurf system behind the API failed; try again later, code: upstream_error }

  schemas:
    Error:
      type: object
      required: [error, code]
      properties:
        error:
          type: string
          description: A message for people.
          example: invalid API key
        code:
          type: string
          description: A stable machine-readable code. Branch on this, not on `error`.
          enum:
            - missing_api_key
            - invalid_api_key
            - wrong_role
            - verification_required
            - invalid_request
            - missing_fields
            - not_found
            - already_covered
            - invalid_state
            - already_bid
            - bid_decided
            - idempotency_in_progress
            - idempotency_key_reused
            - idempotency_key_required
            - load_rejected
            - offer_changed
            - bid_update_limit
            - existing_customer
            - already_redeemed
            - rate_limited
            - invalid_code
            - signup_expired
            - broker_name_taken
            - broker_name_mismatch
            - unavailable
            - upstream_error
            - internal
          example: invalid_api_key
        missing:
          type: array
          description: "`missing_fields` only: exact field names or paths still needed."
          items: { type: string }
        next_step:
          allOf: [{ $ref: "#/components/schemas/NextStep" }]
          description: On signup errors and on every broker load tool's errors (`list_loads`, `post_load`, `get_load`, `load_outreach`, accept, decline, covered).
        current_rate:
          type: number
          description: "`offer_changed` only: the offer's rate now."
        request_id:
          type: string
          description: This request's id (also the `X-Request-Id` response header). Quote it to Lanesurf support.
          example: req_0192f3a4-7b1c-7d2e-9f00-1a2b3c4d5e6f

    MissingFields:
      type: object
      required: [error, code, missing]
      properties:
        error: { type: string }
        code: { type: string, enum: [missing_fields] }
        missing:
          type: array
          description: Exact field names or paths still needed.
          items: { type: string, example: "destinations[0].delivery_ready_date" }
        next_step: { $ref: "#/components/schemas/NextStep" }

    # ---------------- broker side
    LoadFields:
      type: object
      description: |
        A load as fields. Required: `equipment_type`, the first origin's `city`, `state` and
        `pickup_ready_date` (or `pickup_by_date`), and the last destination's `city`, `state` and
        `delivery_ready_date` (or `delivery_by_date`). Fields typed string-or-number accept either
        (`40000` or `"40000"`; `$` and `,` are ignored). A value that isn't a number (`"about 1250"`) answers
        **400** `invalid_request` naming the field; nothing is posted.
      required: [equipment_type, origins, destinations]
      properties:
        equipment_type: { type: string, example: "Dry Van", description: "e.g. Dry Van, Reefer, Flatbed, Step Deck." }
        origins:
          type: array
          minItems: 1
          description: Pickup stops, in order.
          items:
            type: object
            required: [city, state]
            properties:
              city: { type: string, example: Dallas }
              state: { type: string, example: TX, description: Two-letter state or province. }
              zip_code: { type: string }
              pickup_ready_date: { type: string, example: "2026-10-23", description: "Earliest pickup, date (YYYY-MM-DD) or date-time. On the first origin, this or pickup_by_date is required." }
              pickup_by_date: { type: string, description: "Latest pickup, date or date-time." }
              pickup_appointment_type: { type: string, description: "FCFS or APPOINTMENT. Other values are ignored.", example: FCFS }
        destinations:
          type: array
          minItems: 1
          description: Delivery stops, in order.
          items:
            type: object
            required: [city, state]
            properties:
              city: { type: string, example: Atlanta }
              state: { type: string, example: GA }
              zip_code: { type: string }
              delivery_ready_date: { type: string, example: "2026-10-24", description: "Earliest delivery, date or date-time. On the last destination, this or delivery_by_date is required." }
              delivery_by_date: { type: string, description: "Latest delivery, date or date-time." }
              delivery_appointment_type: { type: string, description: "FCFS or APPOINTMENT. Other values are ignored.", example: APPOINTMENT }
        weight_lbs: { type: [string, number], example: 40000 }
        commodity: { type: string, description: Defaults to "General freight". }
        rate_min: { type: [string, number], description: Posted rate. If only one of rate_min / rate_max is given it is used for both. }
        rate_max: { type: [string, number], description: Most the broker will pay. }
        currency: { type: string, example: USD, description: "USD, CAD or MXN (any letter case). Over REST any other value is ignored, not rejected; the MCP tool's schema allows only these three." }
        temperature: { type: [string, number], description: Reefer setpoint (°F). }
        is_palletized: { type: boolean }
        pallet_count: { type: [string, number] }
        driver_requirements: { type: string, description: 'Say "team" for a team load. Other text is not used.' }
        special_requirements: { type: string, description: Used as the special instructions when explicit_special_instructions is empty. }
        explicit_special_instructions: { type: string, description: Special instructions for carriers. }
        reference_number:
          type: string
          maxLength: 100
          description: Your own load number (your TMS or customer reference). Shown on the load and in the Lanesurf dashboard.
          example: PO-4471

    LoadPatch:
      type: object
      description: |
        An edit: the fields of `LoadFields`, every one optional. Only the fields sent change. `origins` and
        `destinations` go together and replace every stop (the same rules as posting: the first origin needs
        `city`, `state` and `pickup_ready_date`, the last destination `city`, `state` and `delivery_ready_date`).
        On a load with no rate yet, one of `rate_min` / `rate_max` sets both. A number that isn't a number, or a
        currency other than USD, CAD or MXN, answers 400.
      properties:
        reference_number: { type: string, maxLength: 100 }
        equipment_type: { type: string, example: Flatbed }
        origins: { type: array, items: { $ref: "#/components/schemas/LoadFields/properties/origins/items" } }
        destinations: { type: array, items: { $ref: "#/components/schemas/LoadFields/properties/destinations/items" } }
        weight_lbs: { type: [string, number] }
        commodity: { type: string }
        rate_min: { type: [string, number] }
        rate_max: { type: [string, number], example: 1300 }
        currency: { type: string, enum: [USD, CAD, MXN] }
        temperature: { type: [string, number] }
        is_palletized: { type: boolean }
        pallet_count: { type: [string, number] }
        driver_requirements: { type: string, description: '"team" for a team load; anything else makes it solo.' }
        special_requirements: { type: string }
        explicit_special_instructions: { type: string }

    PostedLoad:
      type: object
      description: |
        What was posted, as Lanesurf stored it, in the fields `POST /v1/loads` takes, so you can send them back to
        `PATCH /v1/loads/{id}`. `state` is the code for US states and Canadian provinces. A stop posted with a date
        shows the date; one posted with a time shows the stop's local time with its UTC offset. Temperature and
        pallets aren't shown.
      required: [origins, destinations]
      properties:
        reference_number: { type: string }
        equipment_type: { type: string }
        driver_requirements: { type: string, enum: [team] }
        weight_lbs: { type: integer }
        commodity: { type: string }
        rate_min: { type: number, description: The posted rate. }
        rate_max: { type: number }
        currency: { type: string }
        explicit_special_instructions: { type: string }
        origins: { type: array, items: { $ref: "#/components/schemas/LoadFields/properties/origins/items" } }
        destinations: { type: array, items: { $ref: "#/components/schemas/LoadFields/properties/destinations/items" } }

    PostLoadResult:
      type: object
      required: [status, load_id, next_step]
      properties:
        status:
          type: string
          enum: [sourcing, stalled, covered, inactive, deleted]
          description: |
            A new load is `sourcing`, or `stalled` when the brokerage signed up through the API and Lanesurf
            hasn't turned carrier outreach on yet (`status_reason` says so). On an `Idempotency-Key` retry (200)
            this is the original load's current status; see `LoadStatus`.
        load_id: { type: string, format: uuid }
        tms_id:
          type: string
          description: Lanesurf's name for the load (starts with `api-`). Works in place of `load_id` in `/v1/loads/{id}`.
          example: api-7c1e0b5a2f4d4c8e9a3b6d1f0e2c4a8b
        post_with: { $ref: "#/components/schemas/PostWith" }
        assumed:
          type: object
          additionalProperties: { type: string }
          description: Values Lanesurf filled in because they weren't given (e.g. commodity). Not set on a retry.
        message:
          type: string
          description: |
            What happens next, for people. On an `Idempotency-Key` retry it says the key was already used for
            this load and this is the original load, not a new one.
        status_reason:
          type: string
          description: Set when `status` is `stalled`.
          example: "Carrier outreach starts after Lanesurf's review of your brokerage. This load is saved and starts sourcing once outreach is on; nothing to do until then."
        next_step: { $ref: "#/components/schemas/NextStep" }

    PostWith:
      type: object
      description: The email and phone to put on the load wherever it is advertised, so carrier replies reach Lanesurf. Only set when Lanesurf has set a posting email and phone for the key; keys made by signing in from an AI app don't have one yet.
      properties:
        email: { type: string, format: email }
        phone: { type: string }

    LoadList:
      type: object
      required: [loads, next_cursor, next_step]
      properties:
        loads:
          type: array
          description: Newest first.
          items: { $ref: "#/components/schemas/LoadSummary" }
        next_cursor:
          type: [string, "null"]
          description: Send it as `cursor` for the next page. `null` on the last page.
        next_step: { $ref: "#/components/schemas/NextStep" }
    LoadSummary:
      type: object
      required: [load_id, status, origin, destination, outreach]
      properties:
        load_id: { type: string, format: uuid }
        tms_id: { type: string, description: "Lanesurf's id for the load in the dashboard, e.g. `api-7c1e…`. Also works as `{id}`." }
        reference_number: { type: string, description: "Your own load number, when you sent one." }
        status:
          type: string
          enum: [sourcing, stalled, covered, inactive, deleted]
          description: The same status `GET /v1/loads/{id}` answers with.
        status_reason: { type: string, description: "Why, when the status alone would mislead. Set for `stalled`." }
        origin: { $ref: "#/components/schemas/LoadPlace" }
        destination: { $ref: "#/components/schemas/LoadPlace" }
        equipment_type: { type: string, example: Dry Van }
        pickup_date: { type: string, format: date, description: "The first pickup's date (YYYY-MM-DD), as posted.", example: "2026-11-03" }
        delivery_date: { type: string, format: date, description: "The last delivery's date (YYYY-MM-DD), as posted.", example: "2026-11-04" }
        max_rate: { type: number }
        agreed_rate: { type: number, description: "The accepted offer's rate, once there is one." }
        covered_by:
          type: string
          enum: [lanesurf, rep]
          description: "Set for `covered`: `lanesurf` when you accepted one of the offers, `rep` when you marked it covered elsewhere."
        outreach:
          type: object
          required: [sessions]
          properties:
            sessions: { type: integer, description: "Carrier conversations on this load (calls, emails and inbound bids). `GET /v1/loads/{id}` has the offers." }
        offers:
          type: object
          description: Offers waiting for your answer (accepted or declined ones aren't counted). Only open loads have them.
          required: [pending]
          properties:
            pending: { type: integer, description: Offers with no answer yet. }
            best_rate: { type: number, description: The lowest of them. }
            latest_at: { type: string, format: date-time, description: When the newest one came in. }
        created_at: { type: string, format: date-time }
    LoadPlace:
      type: object
      description: The first pickup (origin) or last delivery (destination).
      properties:
        city: { type: string, example: Dallas }
        state: { type: string, description: "The two-letter code for US states and Canadian provinces; anything else as posted.", example: TX }
        zip_code: { type: string, example: "75201" }
    LoadStatus:
      type: object
      required: [load_id, status, offers, outreach, next_step]
      properties:
        load_id: { type: string, format: uuid }
        status:
          type: string
          enum: [sourcing, stalled, covered, inactive, deleted]
          description: |
            `sourcing`: Lanesurf is reaching carriers.
            `stalled`: the load is open, but no carrier has been contacted 15 minutes after posting (sourcing may
            not be switched on for this brokerage), or the brokerage signed up through the API and carrier
            outreach starts after Lanesurf's review. `status_reason` says which.
            `covered`: done; see `covered_by`.
            `inactive`: the load is no longer active and wasn't covered through this API.
            `deleted`: the load was deleted.
        agreed_rate: { type: number, description: "Once an offer is accepted: its rate (what was booked at)." }
        max_rate: { type: number }
        status_reason:
          type: string
          description: Why, when the status alone would mislead. Set for `stalled`.
          example: "No carrier has been contacted 15 minutes after posting. Sourcing may not be switched on for this brokerage; contact Lanesurf."
        covered_by:
          type: string
          enum: [lanesurf, rep]
          description: "`lanesurf`: the rep accepted an offer here. `rep`: the rep marked it covered elsewhere."
        offers:
          type: array
          description: Carriers that named a rate, cheapest first.
          items: { $ref: "#/components/schemas/Offer" }
        winner:
          description: The accepted offer, once there is one.
          allOf: [{ $ref: "#/components/schemas/Offer" }]
        outreach:
          type: object
          required: [sessions, bids]
          properties:
            sessions: { type: integer, description: "Carrier conversations on this load (calls, emails and inbound bids)." }
            bids: { type: integer, description: How many carriers named a rate (the length of `offers`). }
        post_with: { $ref: "#/components/schemas/PostWith" }
        load:
          description: What was posted, as stored. On `GET /v1/loads/{id}` and every change (edit, pause, resume, reopen, delete, accept, decline, covered).
          allOf: [{ $ref: "#/components/schemas/PostedLoad" }]
        next_step: { $ref: "#/components/schemas/NextStep" }

    Offer:
      type: object
      required: [session_id, rate]
      properties:
        session_id: { type: string, format: uuid, description: Use it to accept or decline. }
        channel: { type: string, description: "How the carrier was reached, e.g. `email` or `call`.", example: email }
        status:
          type: string
          description: |
            Lanesurf's raw outreach status for this carrier, as on the broker's board. Show it, don't branch
            on it. Examples: `NEGOTIATION_LOGGED | WITHIN_RANGE`, `NEGOTIATION_LOGGED | OUT_OF_RANGE`,
            `BID_LOGGED | WE_ASKED_FIRST`.
          example: NEGOTIATION_LOGGED | WITHIN_RANGE
        carrier_name: { type: string }
        carrier_mc: { type: string }
        carrier_dot: { type: string }
        contact: { type: string, description: "The carrier's email, or phone if there's no email." }
        contact_email: { type: string, description: "The carrier's email, when known." }
        contact_phone: { type: string, description: "The carrier's phone, when known." }
        rate: { type: number, description: The carrier's rate (always above 0). }
        decision: { type: string, enum: [accepted, declined], description: "The rep's choice, if any." }
        updated_at: { type: string, description: When the carrier conversation started (ISO 8601). }

    # ---------------- signup
    SignupRequest:
      type: object
      required: [role, email]
      description: "`role`, a work `email`, and at least one of `mc_number` / `dot_number`."
      properties:
        role: { type: string, enum: [carrier, broker], description: "`carrier` (has trucks) or `broker` (posts loads)." }
        email:
          type: string
          format: email
          description: The person's work email at the company. Free mail (Gmail, Yahoo, Outlook and the other providers listed on signup) is refused. The code goes here.
          example: dispatch@exampletrucking.example.com
        mc_number: { type: string, example: "123456", description: "Digits; an MC- prefix is accepted. Give this and/or `dot_number`." }
        dot_number: { type: string, example: "1234567", description: "Digits; a DOT or USDOT prefix is accepted. Give this and/or `mc_number`." }

    Signup:
      type: object
      required: [signup_id, role, channel, sent_to, expires_at, next_step]
      description: Where the code went. It says nothing about the carrier check; verify does.
      properties:
        signup_id: { type: string, format: uuid }
        role: { type: string, enum: [carrier, broker] }
        channel: { type: string, enum: [email] }
        sent_to: { type: string, example: "d*******@exampletrucking.example.com", description: Masked email. }
        expires_at: { type: string, format: date-time, description: The code stops working at this time (10 minutes). }
        next_step: { $ref: "#/components/schemas/NextStep" }

    VerifySignupRequest:
      type: object
      required: [code]
      description: |
        Carriers send `code`. Brokers also send `name`, `job_title` and `company_name` (required) and
        optionally `phone` and `website`. MC/DOT and the email come from `POST /v1/signup`.
      properties:
        code: { type: string, pattern: "^[0-9]{8}$", example: "48291307" }
        role: { type: string, enum: [carrier, broker], description: Optional. If sent it must match the signup. }
        name: { type: string, maxLength: 200, description: "Brokers: the person's full name.", example: Dana Reyes }
        job_title: { type: string, maxLength: 100, description: "Brokers: the person's job title at the brokerage.", example: Carrier Sales Manager }
        company_name: { type: string, maxLength: 200, description: "Brokers: the brokerage's name, as carriers should hear it.", example: Acme Freight LLC }
        phone: { type: string, description: "Brokers, optional: E.164; a 10-digit US number is accepted.", example: "+12145550100" }
        website:
          type: string
          maxLength: 300
          description: "Brokers, optional: an http(s) address; `https://` is added when no scheme is given."
          example: https://acmefreight.example.com

    IssuedKey:
      type: object
      required: [api_key, principal, next_step]
      properties:
        api_key: { type: string, example: "ls_live_...", description: Shown once. Store it securely. }
        principal: { $ref: "#/components/schemas/Principal" }
        activated:
          type: boolean
          description: |
            Brokers only. `false` until Lanesurf reviews the new brokerage and turns carrier outreach on. Loads
            posted before then are saved and show `stalled`.
        message:
          type: string
          description: |
            For people. A carrier under review: "your carrier account is under review; ...". A new brokerage:
            outreach starts after Lanesurf's review.
        next_step: { $ref: "#/components/schemas/NextStep" }

    NextStep:
      type: string
      description: For the agent. Exactly what to do next and which MCP tool to call (REST path in brackets).

    SignupError:
      type: object
      required: [error, code, next_step]
      properties:
        error: { type: string, description: A message for people. }
        code:
          type: string
          enum: [invalid_request, missing_fields, not_found, invalid_code, signup_expired, broker_name_taken, broker_name_mismatch,
            existing_customer, already_redeemed, rate_limited, unavailable, internal]
        missing:
          type: array
          description: "`missing_fields` only: the fields to ask for."
          items: { type: string, example: job_title }
        next_step: { $ref: "#/components/schemas/NextStep" }
        retry_after_seconds: { type: integer, description: MCP only; REST sends the `Retry-After` header. }

    Principal:
      type: object
      required: [key_id, key_prefix, role]
      properties:
        key_id: { type: string, format: uuid }
        key_prefix: { type: string, example: "ls_live_1a2b" }
        role: { type: string, enum: [broker, carrier] }
        verification:
          type: string
          enum: [verified, needs_review]
          description: |
            Carriers only. `verified`: confirmed automatically (the signup email's domain matches an email on the
            carrier's record; shared mailbox domains such as ISP mail never count; the authority isn't shown as
            inactive) or by Lanesurf's team.
            `needs_review`: the carrier account is under review (trucks, lanes and the bid list only).
        carrier: { $ref: "#/components/schemas/CarrierProfile" }
        broker_id: { type: string, format: uuid, description: Brokers only. The brokerage. }
        rep_name: { type: string, description: Brokers only. }

    # ---------------- broker setup
    BrokerSetup:
      type: object
      required: [company_name, cc_emails, ask_first_pricing, loads_visible_to_carriers, outreach]
      properties:
        company_name: { type: string, description: What Lanesurf's agents call the brokerage. }
        mc_number: { type: [string, "null"] }
        dot_number: { type: [string, "null"] }
        working_hours: { type: [string, "null"], example: "Mon-Fri 7am-6pm CT" }
        payment_terms: { type: [string, "null"], example: "Net 30; quick pay 2%" }
        cc_emails:
          type: array
          items: { type: string, format: email }
          description: Who Lanesurf copies on carrier email threads.
        ask_first_pricing: { type: boolean, description: "`true`: the agents ask the carrier for their rate before quoting one." }
        loads_visible_to_carriers:
          type: boolean
          description: "`true`: carriers' AI agents can search and bid on this brokerage's loads through the Agent API. Off until the broker turns it on."
        outreach:
          type: object
          required: [activated, email, phone]
          properties:
            activated:
              type: boolean
              description: "`false` until Lanesurf reviews a brokerage that signed up through the API. Always `true` for brokerages Lanesurf onboarded."
            email: { type: boolean, description: Whether Lanesurf emails carriers when a load is posted. }
            phone: { type: boolean, description: Whether Lanesurf calls carriers when a load is posted. }

    BrokerSetupPatch:
      type: object
      additionalProperties: false
      minProperties: 1
      description: Only these fields. Outreach can't be changed here.
      properties:
        company_name: { type: string, minLength: 1, maxLength: 200 }
        working_hours: { type: string, maxLength: 500, description: Empty clears it. }
        payment_terms: { type: string, maxLength: 500, description: Empty clears it. }
        cc_emails:
          type: array
          maxItems: 10
          items: { type: string, format: email }
          description: Replaces the list; `[]` clears it.
        ask_first_pricing: { type: boolean }
        loads_visible_to_carriers: { type: boolean, description: "Let carriers' AI agents search and bid on this brokerage's loads (opt-in)." }

    CarrierProfile:
      type: object
      description: Carriers only. The carrier record from the identity check, and the contact proved with the code. Empty fields are left out.
      properties:
        legal_name: { type: string }
        mc_number: { type: string }
        dot_number: { type: string }
        contact_email: { type: string }
        contact_phone: { type: string }

    # ---------------- carrier side
    Place:
      type: object
      required: [state]
      properties:
        city: { type: string, example: Atlanta }
        state: { type: string, example: GA, description: "US state or Canadian province code (any case; stored upper-case)." }

    RateAsk:
      type: object
      required: [amount, unit]
      description: What the carrier wants for the truck.
      properties:
        amount: { type: number, exclusiveMinimum: 0, example: 2.10 }
        unit: { type: string, enum: [flat, per_mile] }

    TruckInput:
      type: object
      description: |
        A truck as fields. To post, `equipment_type`, `current_city`, `current_state` and `available_from`
        are required. To update, send only what changed.
      properties:
        equipment_type: { type: string, example: Reefer }
        trailer_length_ft: { type: integer, minimum: 1, maximum: 60, example: 53 }
        current_city: { type: string, example: Dallas }
        current_state: { type: string, minLength: 2, maxLength: 2, example: TX, description: "US state or Canadian province code (any case; stored upper-case)." }
        available_from:
          type: string
          format: date-time
          description: When the truck is free (RFC 3339, e.g. `2026-10-23T08:00:00-05:00`). Up to 60 days out.
        available_to:
          type: string
          format: date-time
          description: When it stops being free. Not before `available_from`. Leave it out and the post lapses 72 hours after `available_from`.
        destinations:
          type: array
          description: Where the truck wants to go. Each needs a 2-letter `state`; `city` is optional.
          items: { $ref: "#/components/schemas/Place" }
        rate_ask: { $ref: "#/components/schemas/RateAsk" }
        notes: { type: string }

    Truck:
      type: object
      required: [truck_id, status, equipment_type, current_city, current_state, available_from, destinations, expires_at, created_at]
      properties:
        truck_id: { type: string, format: uuid }
        status:
          type: string
          enum: [active, removed, expired]
          description: "`active`: posted. `removed`: taken down (see `removed_reason`). `expired`: past `expires_at`."
        equipment_type: { type: string }
        trailer_length_ft: { type: integer }
        current_city: { type: string }
        current_state: { type: string }
        available_from: { type: string, format: date-time }
        available_to: { type: string, format: date-time }
        destinations:
          type: array
          items: { $ref: "#/components/schemas/Place" }
        rate_ask: { $ref: "#/components/schemas/RateAsk" }
        notes: { type: string }
        removed_reason: { type: string, enum: [loaded, unavailable, other] }
        expires_at:
          type: string
          format: date-time
          description: "`available_to`, or 72 hours after `available_from` when there's no end."
        created_at: { type: string, format: date-time }

    LaneInput:
      type: object
      required: [origin_state, destination_state]
      properties:
        origin_city: { type: string, example: Dallas }
        origin_state: { type: string, minLength: 2, maxLength: 2, example: TX }
        destination_city: { type: string, example: Atlanta }
        destination_state: { type: string, minLength: 2, maxLength: 2, example: GA }
        equipment_type: { type: string, example: Reefer }

    Lane:
      type: object
      required: [lane_id, origin_state, destination_state, created_at]
      properties:
        lane_id: { type: string, format: uuid }
        origin_city: { type: string }
        origin_state: { type: string }
        destination_city: { type: string }
        destination_state: { type: string }
        equipment_type: { type: string }
        created_at: { type: string, format: date-time }

    LoadOutreach:
      type: object
      required: [session_id, carrier_name, channel, direction, status, contacted_at]
      properties:
        session_id: { type: string, description: "For accept and decline when the carrier named a rate." }
        carrier_name: { type: string }
        mc: { type: string }
        dot: { type: string }
        channel: { type: string, enum: [call, email, sms] }
        direction: { type: string, enum: [outbound, inbound], description: "outbound: Lanesurf contacted the carrier. inbound: the carrier reached out or bid." }
        status: { type: string, description: "Where the carrier stands, as the dashboard labels it." }
        rate: { type: number, description: "All-in USD, when the carrier named one." }
        contact_phone: { type: string }
        contact_email: { type: string }
        contacted_at: { type: string }
    CarrierOutreach:
      type: object
      required: [load_id, broker_name, channel, contacted_at, open]
      properties:
        load_id: { type: string, format: uuid }
        broker_name: { type: string, description: "The brokerage the outreach came from, as its agents introduced it." }
        channel: { type: string, enum: [call, email, sms], description: "The latest touch about this load." }
        contacted_at: { type: string }
        open: { type: boolean, description: "Still on offer to carriers: you can bid on it." }
        load: { $ref: "#/components/schemas/CarrierLoad" }
    CarrierLoad:
      type: object
      description: A load as a carrier sees it. Never includes the broker's name, contacts or max rate.
      required: [load_id, equipment_type, origin, destination, rate_type]
      properties:
        load_id: { type: string, format: uuid }
        equipment_type: { type: string, example: Reefer }
        origin:
          description: The first pickup.
          allOf: [{ $ref: "#/components/schemas/Place" }]
        destination:
          description: The last delivery.
          allOf: [{ $ref: "#/components/schemas/Place" }]
        pickup_date: { type: string, description: "The first pickup's time window start (or end), as the broker gave it." }
        delivery_date: { type: string, description: "The last delivery's time window start (or end), as the broker gave it." }
        weight_lbs: { type: integer }
        commodity: { type: string }
        total_miles: { type: number }
        posted_rate:
          type: number
          description: Only when the broker priced the load.
        rate_type:
          type: string
          enum: [priced, ask_first]
          description: "`priced`: `posted_rate` is set. `ask_first`: no rate shown; bid without a rate to ask for one."
        deadhead_miles:
          type: integer
          description: Near-searches and truck matches only. Straight-line miles from the searched ZIP or city (or the truck) to the pickup.
        match:
          type: string
          enum: [same_zip, within_50_miles, within_100_miles, within_150_miles, same_state]
          description: Near-searches and truck matches only. How close the pickup is.
      example:
        load_id: 9e8d7c6b-5a49-4382-9172-6f5e4d3c2b1a
        equipment_type: Reefer
        origin: { city: Dallas, state: TX }
        destination: { city: Atlanta, state: GA }
        pickup_date: "2026-10-23T08:00:00-05:00"
        delivery_date: "2026-10-24T14:00:00-04:00"
        weight_lbs: 38000
        commodity: Produce
        total_miles: 781
        posted_rate: 2100
        rate_type: priced

    CarrierLoadList:
      type: object
      required: [loads, total]
      properties:
        loads:
          type: array
          items: { $ref: "#/components/schemas/CarrierLoad" }
        total: { type: integer, description: The number of loads in this answer. }

    BidInput:
      type: object
      properties:
        rate:
          type: number
          exclusiveMinimum: 0
          maximum: 1000000
          description: All-in USD. Optional; leave it out to ask for the broker's rate.
        truck_id:
          type: string
          format: uuid
          description: Which of this key's active trucks would run it.
        note: { type: string, description: "Up to 1,000 bytes: about 1,000 characters of plain text; accented letters and emoji count as more than one." }

    Bid:
      type: object
      required: [bid_id, load_id, status, created_at]
      properties:
        bid_id: { type: string, format: uuid }
        load_id: { type: string, format: uuid }
        rate: { type: number }
        truck_id: { type: string, format: uuid }
        note: { type: string }
        status:
          type: string
          enum: [submitted, accepted, declined, load_closed, incomplete]
          description: |
            `submitted`: the broker has it and the load is open. `accepted` / `declined`: the broker's choice on
            this bid. `load_closed`: the load was covered some other way, closed, or
            booked to someone else, and the broker made no choice on this bid. `incomplete`: the bid was recorded
            but never reached the broker (a failure mid-bid); POST it again to finish it (within 2 minutes from
            the same key with its original terms, after that from any of the carrier's keys with the new terms).
        created_at: { type: string, format: date-time }
        action:
          type: string
          enum: [created, updated, unchanged]
          description: |
            Only in the reply to `POST /v1/carrier/loads/{load_id}/bids`. `created`: a new bid. `updated`: this
            carrier's existing bid now has the new terms. `unchanged`: same terms as before.
