openapi: 3.0.3
info:
  title: Navexa API
  version: "1.0.0"
  description: |
    Create and manage orders, buy and void shipping labels, read tracking and read stock in your
    Navexa account.

    **Authentication.** Send an API token from **API tokens** in Navexa as a bearer token:
    `Authorization: Bearer nfx_...`. A token acts for the account it was made in and only for the
    scopes it was given. A missing, expired or revoked token gets 401; a token without the scope an
    endpoint needs gets 403. Another account's order, box or label id answers 404, exactly like an
    id that does not exist.

    **Test mode.** A test-mode token buys labels on EasyPost's test system: real rates from your
    boxes and addresses, but no postage is spent, nothing is charged, and test labels never reach
    the order, Pack Station or your sales channels. Test label ids start with `atl_`. Order
    endpoints act on real orders in test mode too; cancel the orders you create. Return labels and
    a Medusa order's labels are live only: a test-mode token can't have the `returns:labels` or
    `orders:labels` scope, and those endpoints answer a test-mode token 409 `WRONG_MODE`.

    **Limits.** Each token may make 300 requests a minute (429 with `Retry-After` past it). Quotes
    that call the carriers also draw on your account's daily rating budget, which all its tokens
    share: 100 carrier rating calls a day (UTC), plus 3 for every label the account bought in the
    last 24 hours, in Navexa or through this API. A quote served from Navexa's rate cache costs
    nothing. Past the budget, quotes answer 429 `RATING_BUDGET_EXCEEDED` until midnight UTC or until
    the account buys more labels.

    **Errors** are JSON: `{"error": "...", "message": "...", "code": "..."}`.
servers:
  - url: https://api.navexa.co/api/v1/external
security:
  - bearerAuth: []
tags:
  - name: Orders
  - name: Labels
  - name: Tracking
  - name: Inventory
  - name: Shipping
  - name: Returns
  - name: Account
paths:
  /openapi.yaml:
    get:
      tags: [Account]
      summary: This OpenAPI description
      security: []
      responses:
        "200":
          description: The spec, as YAML.
          content:
            application/yaml:
              schema:
                type: string
  /locations:
    get:
      tags: [Account]
      summary: Ship-from locations
      x-scopes: [location]
      responses:
        "200":
          description: Your active ship-from locations.
          content:
            application/json:
              schema:
                type: array
                items: { $ref: "#/components/schemas/Location" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /warehouses:
    get:
      tags: [Account]
      summary: Warehouses
      description: The warehouses orders ship from (`warehouse_id`) and stock is counted in.
      x-scopes: [location]
      responses:
        "200":
          description: Your warehouses.
          content:
            application/json:
              schema:
                type: object
                properties:
                  warehouses:
                    type: array
                    items: { $ref: "#/components/schemas/Warehouse" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /packages:
    get:
      tags: [Shipping]
      summary: Package templates
      x-scopes: ["shipping:rates"]
      responses:
        "200":
          description: Your boxes, polybags, tubes and pallets. Inches and grams.
          content:
            application/json:
              schema:
                type: array
                items: { $ref: "#/components/schemas/Package" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /shipping/bin-pack:
    post:
      tags: [Shipping]
      summary: Fit items into your packages
      x-scopes: ["shipping:rates"]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/BinPackRequest" }
      responses:
        "200":
          description: Which packages the items fit in.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/BinPackResponse" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /shipping/rates:
    post:
      tags: [Shipping]
      summary: Rates for packages you describe
      description: |
        Carrier rates totalled per service, for display (not buyable; to buy, create an order and
        quote it). US addresses only; up to 100 packages; weights in ounces. Each package not in
        the rate cache costs one rating call from the account's daily budget.
      x-scopes: ["shipping:rates"]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/RatesRequest" }
      responses:
        "200":
          description: Rates, cheapest first.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/RatesResponse" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "502":
          description: A carrier failed to quote (`RATE_PROVIDER_UNAVAILABLE`).
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
  /returns/{fulfillmentId}/rates:
    post:
      tags: [Returns]
      summary: Quote a Medusa return label
      description: Live tokens only (a test-mode token gets 409 `WRONG_MODE`).
      x-scopes: ["returns:labels"]
      parameters:
        - $ref: "#/components/parameters/FulfillmentId"
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                order_id: { type: string, description: The return's Medusa order id. }
      responses:
        "200":
          description: Return label rates, cheapest first.
          content:
            application/json:
              schema:
                type: object
                properties:
                  rates:
                    type: array
                    items: { type: object }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/WrongMode" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /returns/{fulfillmentId}/buy-label:
    post:
      tags: [Returns]
      summary: Buy a Medusa return label
      description: Charges your account. Live tokens only (a test-mode token gets 409 `WRONG_MODE`).
      x-scopes: ["returns:labels"]
      parameters:
        - $ref: "#/components/parameters/FulfillmentId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [rate_id]
              properties:
                rate_id: { type: string }
      responses:
        "200":
          description: The return, with its label.
          content:
            application/json:
              schema: { type: object }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409":
          description: |
            Already bought, or the rate expired (`RETURN_LABEL_ALREADY_BOUGHT`,
            `RETURN_RATE_STALE`), or the token is a test-mode token (`WRONG_MODE`).
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /returns/{fulfillmentId}/cancel:
    post:
      tags: [Returns]
      summary: Cancel a Medusa return
      description: |
        Refunds its label first if one was bought. Live tokens only (a test-mode token gets 409
        `WRONG_MODE`).
      x-scopes: ["returns:labels"]
      parameters:
        - $ref: "#/components/parameters/FulfillmentId"
      responses:
        "200":
          description: The cancelled return.
          content:
            application/json:
              schema:
                type: object
                properties:
                  id: { type: string }
                  status: { type: string }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/WrongMode" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /orders:
    get:
      tags: [Orders]
      summary: List orders
      description: Your custom orders (orders created in Navexa or through this API), newest first.
      x-scopes: ["orders:read"]
      parameters:
        - name: external_reference
          in: query
          schema: { type: string }
          description: Only the order created under this reference.
        - name: limit
          in: query
          schema: { type: integer, minimum: 1, maximum: 100, default: 50 }
        - name: offset
          in: query
          schema: { type: integer, minimum: 0, default: 0 }
      responses:
        "200":
          description: A page of orders (items are not included; get the order for them).
          content:
            application/json:
              schema: { $ref: "#/components/schemas/OrderList" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/RateLimited" }
    post:
      tags: [Orders]
      summary: Create an order
      description: |
        Give `items` (Navexa packs them into your packages) or `packages` (the boxes as you packed
        them), and `warehouse_id` or a one-off `ship_from_address`. With an `external_reference`
        the call is safe to retry: a reference you already used returns that order with 200
        instead of creating another.
      x-scopes: ["orders:write"]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/CreateOrderRequest" }
      responses:
        "200":
          description: An order with this external_reference already existed; it is returned unchanged.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Order" }
        "201":
          description: The order was created.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Order" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /orders/{orderId}:
    parameters:
      - $ref: "#/components/parameters/OrderId"
    get:
      tags: [Orders]
      summary: Get an order
      x-scopes: ["orders:read"]
      responses:
        "200":
          description: The order with its items.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Order" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }
    patch:
      tags: [Orders]
      summary: Update an order
      description: |
        Change the customer, ship-to, origin or declared value while the order has no label.
        Fields you leave out keep their value. Items and packages are fixed once created: cancel
        and create the order again to change them.
      x-scopes: ["orders:write"]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/UpdateOrderRequest" }
      responses:
        "200":
          description: The updated order.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Order" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/OrderHasLabel" }
        "429": { $ref: "#/components/responses/RateLimited" }
    delete:
      tags: [Orders]
      summary: Cancel an order
      description: |
        Deletes an order that has no label. Void its labels first. A voided label's postage is
        credited back to you when the carrier confirms the refund (its `refund_status` goes from
        `submitted` to `refunded`); until then the order can't be deleted (409
        `LABEL_REFUND_PENDING`), so retry the cancel after the refund completes.
      x-scopes: ["orders:write"]
      responses:
        "204":
          description: The order was cancelled.
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409":
          description: |
            `ORDER_HAS_LABEL`: the order has a label; void it first. `LABEL_REFUND_PENDING`: a
            voided label's postage refund is still pending with the carrier; retry once its
            `refund_status` is `refunded`. `LABEL_ADJUSTED`: the carrier billed an adjustment on one
            of the order's labels, so the order can't be deleted.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /orders/{orderId}/rates:
    parameters:
      - $ref: "#/components/parameters/OrderId"
    post:
      tags: [Labels]
      summary: Quote an order's boxes
      description: |
        Buyable rates for every box of the order that has no label yet, for any of your orders
        (custom or from a sales channel). A box quoted in the last 24 hours, and unchanged since,
        comes from the rate cache at no cost; each other box costs one rating call from the
        account's daily budget. Use a box's `box_id` and a rate's `id` to buy its label.
      x-scopes: ["labels:write"]
      responses:
        "200":
          description: The boxes and their rates.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/OrderQuote" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409":
          description: The order is cancelled (`ORDER_CANCELLED`).
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "422":
          description: A freight (LTL/FTL) order (`FREIGHT_NOT_SUPPORTED`); book it in Navexa.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "429": { $ref: "#/components/responses/RatingBudget" }
  /orders/{orderId}/tracking:
    parameters:
      - $ref: "#/components/parameters/OrderId"
    get:
      tags: [Tracking]
      summary: Track an order
      description: Every label bought for the order, with its carrier scans, newest first.
      x-scopes: ["tracking:read"]
      responses:
        "200":
          description: The order's shipments.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/OrderTracking" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /orders/{orderId}/labels:
    parameters:
      - name: orderId
        in: path
        required: true
        schema: { type: string }
        description: The **Medusa** order id (this endpoint serves the Navexa Medusa plugin).
    get:
      tags: [Labels]
      summary: A Medusa order's labels and cost
      description: Live tokens only (a test-mode token gets 409 `WRONG_MODE`).
      x-scopes: ["orders:labels"]
      responses:
        "200":
          description: The live labels bought for the Medusa order. Empty when nothing has shipped.
          content:
            application/json:
              schema:
                type: object
                properties:
                  labels:
                    type: array
                    items:
                      type: object
                      properties:
                        tracking_number: { type: string }
                        carrier: { type: string }
                        postage_cents: { type: integer, nullable: true }
                        insurance_cents: { type: integer }
                        is_freight: { type: boolean }
                        orders_on_shipment: { type: integer }
                        purchased_at: { type: string, format: date-time }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/WrongMode" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /labels:
    post:
      tags: [Labels]
      summary: Buy a label
      description: |
        Buys a box's label at a rate from the order's quote. Your account is charged as for a label
        bought in Navexa: the postage, the per-label fee and the payment fee, on your default
        payment method (bank accounts on the nightly charge). A label the carrier refuses is not
        charged. The `Idempotency-Key` header is required: retrying with the same key returns the
        label the first request bought (200, `Idempotent-Replayed: true`) instead of buying another.
        A test-mode token buys a test label on a test rate and is never charged.
      x-scopes: ["labels:write"]
      parameters:
        - name: Idempotency-Key
          in: header
          required: true
          schema: { type: string, maxLength: 255 }
          description: A unique key per purchase you mean to make, such as a UUID.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/BuyLabelRequest" }
      responses:
        "200":
          description: The label an earlier request with this Idempotency-Key bought.
          headers:
            Idempotent-Replayed:
              schema: { type: string, enum: ["true"] }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Label" }
        "201":
          description: The label was bought.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Label" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402":
          description: Billing refused the purchase (no payment method, past due, card declined). `code` says which.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409":
          description: |
            `LABEL_EXISTS` (the box has a label), `RATE_EXPIRED` (quote again),
            `IDEMPOTENCY_IN_PROGRESS` (a request with this key is still buying),
            `LABEL_PURCHASE_IN_PROGRESS`, `ORDER_CANCELLED` or `ORDER_ON_HOLD`.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "422":
          description: |
            `LABEL_PURCHASE_FAILED` (the carrier refused it; any charge is refunded),
            `RATE_NOT_FOR_BOX`, `IDEMPOTENCY_KEY_REUSED` (the key was used for another box or
            rate) or `FREIGHT_NOT_SUPPORTED`.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /labels/{labelId}:
    parameters:
      - $ref: "#/components/parameters/LabelId"
    get:
      tags: [Labels]
      summary: Get a label
      x-scopes: ["labels:write", "tracking:read"]
      responses:
        "200":
          description: The label.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Label" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/WrongMode" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /labels/{labelId}/void:
    parameters:
      - $ref: "#/components/parameters/LabelId"
    post:
      tags: [Labels]
      summary: Void a label
      description: |
        Cancels a label the carrier has not scanned and asks for the postage back. The postage is
        refunded once the carrier confirms it (`refund_status` goes from `submitted` to
        `refunded`); the per-label fee is kept. The box's items go back into stock and the box can
        be bought again.
      x-scopes: ["labels:write"]
      responses:
        "200":
          description: The voided label.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Label" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409":
          description: The label cannot be voided (`LABEL_NOT_VOIDABLE`, e.g. it has carrier scans or was voided already), or `WRONG_MODE`.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /inventory:
    get:
      tags: [Inventory]
      summary: Stock by SKU and warehouse
      description: |
        From Navexa's inventory ledger: on hand, committed to open orders, unavailable (damaged or
        quarantined), available to sell, and incoming on purchase orders and transfers.
      x-scopes: ["inventory:read"]
      parameters:
        - name: sku
          in: query
          schema: { type: string }
          description: Only this SKU (exact match).
        - name: warehouse_id
          in: query
          schema: { type: string }
        - name: limit
          in: query
          schema: { type: integer, minimum: 1, maximum: 500, default: 100 }
        - name: offset
          in: query
          schema: { type: integer, minimum: 0, default: 0 }
      responses:
        "200":
          description: One row per SKU and warehouse.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/InventoryList" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/RateLimited" }
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: An API token (`nfx_...`) from API tokens in Navexa.
  parameters:
    OrderId:
      name: orderId
      in: path
      required: true
      schema: { type: string }
      description: The Navexa order id (`ord_...`).
    LabelId:
      name: labelId
      in: path
      required: true
      schema: { type: string }
      description: A label id (`lbl_...` live, `atl_...` test).
    FulfillmentId:
      name: fulfillmentId
      in: path
      required: true
      schema: { type: string }
      description: The Medusa return fulfillment id.
  responses:
    BadRequest:
      description: The request is invalid; `message` says why.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    Unauthorized:
      description: Missing, malformed, expired or revoked token.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    Forbidden:
      description: The token lacks the scope this endpoint needs.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    NotFound:
      description: No such id in your account.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    OrderHasLabel:
      description: The order has a label (`ORDER_HAS_LABEL`); void it first.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    WrongMode:
      description: |
        A test-mode token used a live label or a live-only endpoint (return labels, a Medusa
        order's labels), or a live token a test label (`WRONG_MODE`).
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    RateLimited:
      description: Too many requests for this token this minute (`RATE_LIMITED`).
      headers:
        Retry-After:
          schema: { type: integer }
          description: Seconds to wait.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    RatingBudget:
      description: |
        `RATE_LIMITED` (requests per minute), or `RATING_BUDGET_EXCEEDED`: the quote would pass
        your account's daily rating budget. `details` has `used`, `allowance` and `resets_at`.
      headers:
        Retry-After:
          schema: { type: integer }
          description: Seconds to wait.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
  schemas:
    Error:
      type: object
      required: [error, message, code]
      properties:
        error: { type: string }
        message: { type: string }
        code: { type: string }
        details: { type: object, additionalProperties: true }
    Location:
      type: object
      properties:
        id: { type: string }
        name: { type: string }
        city: { type: string }
        province: { type: string }
        zip: { type: string }
        country: { type: string }
        is_active: { type: boolean }
    Warehouse:
      type: object
      properties:
        id: { type: string, description: "The warehouse_id (`phl_...`)." }
        name: { type: string }
        is_default: { type: boolean }
        is_active: { type: boolean }
    Package:
      type: object
      properties:
        id: { type: string }
        name: { type: string }
        type: { type: string }
        length_inches: { type: number }
        width_inches: { type: number }
        height_inches: { type: number }
        weight_grams: { type: number }
    BinPackRequest:
      type: object
      required: [items]
      properties:
        items:
          type: array
          maxItems: 500
          items:
            type: object
            required: [id, quantity]
            properties:
              id: { type: string }
              name: { type: string }
              quantity: { type: integer, minimum: 1 }
              sku: { type: string, description: Navexa uses the product's dimensions. }
              weight_grams: { type: number }
              length_inches: { type: number }
              width_inches: { type: number }
              height_inches: { type: number }
    BinPackResponse:
      type: object
      properties:
        package_assignments:
          type: array
          items:
            type: object
            properties:
              package_name: { type: string }
              length_inches: { type: number }
              width_inches: { type: number }
              height_inches: { type: number }
              weight_grams: { type: number }
              item_count: { type: integer }
        skipped_items:
          type: array
          items:
            type: object
            properties:
              item_id: { type: string }
              name: { type: string }
              reason: { type: string }
        partial_quote: { type: boolean }
        total_packages: { type: integer }
    RatesRequest:
      type: object
      required: [destination, packages]
      properties:
        location_id: { type: string }
        destination:
          type: object
          required: [address1, city, state, zip, country]
          properties:
            address1: { type: string }
            address2: { type: string }
            city: { type: string }
            state: { type: string }
            zip: { type: string }
            country: { type: string, enum: [US] }
        destination_is_residential: { type: boolean }
        ship_date: { type: string }
        packages:
          type: array
          minItems: 1
          maxItems: 100
          items:
            type: object
            properties:
              length_inches: { type: number }
              width_inches: { type: number }
              height_inches: { type: number }
              weight_oz: { type: number }
        items:
          type: array
          items:
            type: object
            properties:
              sku: { type: string }
              quantity: { type: integer, minimum: 1 }
    RatesResponse:
      type: object
      properties:
        rates:
          type: array
          items:
            type: object
            properties:
              id: { type: string }
              carrier: { type: string }
              service: { type: string }
              service_code: { type: string }
              cost_cents: { type: integer }
              currency: { type: string }
              estimated_days: { type: integer }
              package_count: { type: integer }
        location_id: { type: string }
        quoted_at: { type: string, format: date-time }
        expires_at: { type: string, format: date-time }
        quote_id: { type: string }
        inventory_warnings:
          type: array
          items:
            type: object
            properties:
              sku: { type: string }
              requested_quantity: { type: integer }
              available_quantity: { type: integer }
    Address:
      type: object
      required: [address_1, city, country, zip]
      properties:
        address_1: { type: string }
        address_2: { type: string, nullable: true }
        city: { type: string }
        province: { type: string, description: State or province code. }
        country: { type: string, description: ISO 3166-1 alpha-2 code. }
        zip: { type: string }
    ShipFromAddress:
      type: object
      required: [address_1, city, province, country, zip]
      properties:
        name: { type: string }
        company: { type: string }
        phone: { type: string }
        address_1: { type: string }
        address_2: { type: string, nullable: true }
        city: { type: string }
        province: { type: string }
        country: { type: string }
        zip: { type: string }
    OrderItem:
      type: object
      properties:
        id: { type: string }
        name: { type: string }
        sku: { type: string }
        product_id: { type: string }
        variant_id: { type: string }
        quantity: { type: integer }
        weight_grams: { type: number }
    Order:
      type: object
      properties:
        id: { type: string }
        order_number: { type: string, description: "Navexa's order number, CUS-00001." }
        external_reference: { type: string, nullable: true }
        customer_name: { type: string }
        customer_email: { type: string }
        customer_phone: { type: string }
        shipping_address: { $ref: "#/components/schemas/Address" }
        declared_value_cents: { type: integer }
        fulfillment_status: { type: string, enum: [unfulfilled, partial, fulfilled] }
        editable: { type: boolean, description: False once a label is bought (in a list, always false). }
        items:
          type: array
          items: { $ref: "#/components/schemas/OrderItem" }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
    OrderList:
      type: object
      properties:
        orders:
          type: array
          items: { $ref: "#/components/schemas/Order" }
        total: { type: integer }
        limit: { type: integer }
        offset: { type: integer }
    CreateOrderRequest:
      type: object
      required: [customer_name, shipping_address]
      properties:
        external_reference:
          type: string
          maxLength: 255
          description: Your own id for the order, unique in your account. Makes the call safe to retry.
        customer_name: { type: string }
        customer_email: { type: string }
        customer_phone: { type: string }
        shipping_address: { $ref: "#/components/schemas/Address" }
        warehouse_id: { type: string, description: "Ship from this warehouse (GET /warehouses)." }
        ship_from_address: { $ref: "#/components/schemas/ShipFromAddress" }
        declared_value_cents: { type: integer, minimum: 0 }
        signature_confirmation: { type: string, enum: [NONE, SIGNATURE, ADULT_SIGNATURE], description: "Empty: your account's setting." }
        items:
          type: array
          maxItems: 250
          description: Products to ship; Navexa packs them into your packages. Each needs a sku or a product_id.
          items:
            type: object
            required: [quantity]
            properties:
              sku: { type: string }
              product_id: { type: string }
              variant_id: { type: string, description: Needed with product_id when the product has several variants. }
              quantity: { type: integer, minimum: 1 }
        packages:
          type: array
          maxItems: 50
          description: The boxes as you packed them; each line becomes `quantity` identical boxes.
          items:
            type: object
            required: [weight_grams, quantity]
            properties:
              package_id: { type: string, description: "One of your packages (GET /packages), or give the dimensions." }
              length_inches: { type: number }
              width_inches: { type: number }
              height_inches: { type: number }
              weight_grams: { type: number }
              quantity: { type: integer, minimum: 1, maximum: 100 }
    UpdateOrderRequest:
      type: object
      properties:
        customer_name: { type: string }
        customer_email: { type: string }
        customer_phone: { type: string }
        shipping_address:
          type: object
          properties:
            address_1: { type: string }
            address_2: { type: string }
            city: { type: string }
            province: { type: string }
            country: { type: string }
            zip: { type: string }
        warehouse_id: { type: string }
        ship_from_address: { $ref: "#/components/schemas/ShipFromAddress" }
        declared_value_cents: { type: integer, minimum: 0 }
    Rate:
      type: object
      properties:
        id: { type: string, description: The rate_id to buy with. }
        carrier: { type: string }
        service: { type: string }
        cost_cents: { type: integer, description: Postage before insurance and fees. }
        currency: { type: string }
        estimated_days: { type: integer }
        delivery_date: { type: string, format: date-time }
        recommended: { type: boolean, description: "The rate your shipping rules (or the cheapest) pick." }
        billed_by: { type: string, enum: [navexa, carrier], description: "carrier: your own carrier account pays the postage." }
    BoxQuote:
      type: object
      properties:
        box_id: { type: string }
        status: { type: string, enum: [quoted, failed, on_hold, labelled] }
        package: { type: string }
        dimensions: { type: string }
        weight_grams: { type: number }
        rates:
          type: array
          items: { $ref: "#/components/schemas/Rate" }
        error: { type: string, description: Why a failed box has no rates. }
    OrderQuote:
      type: object
      properties:
        order_id: { type: string }
        boxes:
          type: array
          items: { $ref: "#/components/schemas/BoxQuote" }
        test_mode: { type: boolean }
    BuyLabelRequest:
      type: object
      required: [box_id, rate_id]
      properties:
        box_id: { type: string }
        rate_id: { type: string }
    Label:
      type: object
      properties:
        id: { type: string }
        order_id: { type: string }
        box_id: { type: string }
        tracking_number: { type: string }
        carrier: { type: string }
        service: { type: string }
        status: { type: string, description: "label_created, pre_transit, in_transit, out_for_delivery, delivered, exception, cancelled, ..." }
        postage_cents: { type: integer, nullable: true }
        insurance_cents: { type: integer }
        label_url: { type: string }
        label_zpl_url: { type: string }
        voided: { type: boolean }
        refund_status: { type: string, description: "submitted while the carrier reviews a void, then refunded or rejected." }
        estimated_delivery_date: { type: string, format: date-time }
        delivered_at: { type: string, format: date-time }
        is_freight: { type: boolean }
        test_mode: { type: boolean }
        created_at: { type: string, format: date-time }
    TrackingEvent:
      type: object
      properties:
        status: { type: string }
        detail: { type: string }
        message: { type: string }
        occurred_at: { type: string, format: date-time }
        city: { type: string }
        state: { type: string }
        country: { type: string }
        zip: { type: string }
    Shipment:
      allOf:
        - $ref: "#/components/schemas/Label"
        - type: object
          properties:
            events:
              type: array
              items: { $ref: "#/components/schemas/TrackingEvent" }
    OrderTracking:
      type: object
      properties:
        order_id: { type: string }
        shipments:
          type: array
          items: { $ref: "#/components/schemas/Shipment" }
    Stock:
      type: object
      properties:
        sku: { type: string }
        title: { type: string }
        warehouse_id: { type: string }
        warehouse_name: { type: string }
        on_hand: { type: integer }
        committed: { type: integer }
        unavailable: { type: integer }
        available: { type: integer }
        incoming: { type: integer }
    InventoryList:
      type: object
      properties:
        inventory:
          type: array
          items: { $ref: "#/components/schemas/Stock" }
        total: { type: integer }
        limit: { type: integer }
        offset: { type: integer }
