{
  "openapi": "3.1.0",
  "info": {
    "title": "Novil API",
    "version": "1.0.0",
    "summary": "Push dispatch routes into Novil and read back vehicles, drivers and fuel plans.",
    "description": "The Novil API lets a transportation management system (TMS) or any other system push dispatch routes into Novil and read back the vehicles, drivers and fuel plans behind them.\n\nEvery request is authenticated with an API key created under **Settings › API Keys** in the Novil app. Keys are shown once, at creation, and carry the scopes they were created with.\n\nA pushed route becomes a draft route in Novil. When the truck arrives at the route's first stop, Novil builds the fuel plan and sends it to the driver, exactly as it does for routes that arrive through a telematics or TMS integration.",
    "contact": {
      "name": "Novil",
      "url": "https://app.novilfleet.com"
    }
  },
  "servers": [
    {
      "url": "https://app.novilfleet.com",
      "description": "Production"
    }
  ],
  "tags": [
    {
      "name": "Keys",
      "description": "Inspect the key a request is authenticated with."
    },
    {
      "name": "Routes",
      "description": "Dispatch routes pushed from your TMS."
    },
    {
      "name": "Vehicles",
      "description": "Read-only access to the fleet's vehicles, for matching unit numbers."
    },
    {
      "name": "Drivers",
      "description": "Read-only access to drivers, for matching names."
    }
  ],
  "security": [
    {
      "apiKey": []
    }
  ],
  "paths": {
    "/v1/me": {
      "get": {
        "tags": [
          "Keys"
        ],
        "operationId": "getMe",
        "summary": "Describe the current key",
        "description": "Returns the company the key belongs to, the key's scopes and environment, and the rate limit that applies to it. Use it to verify a key after creating it.",
        "responses": {
          "200": {
            "description": "The key and its company.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Me"
                },
                "example": {
                  "company": {
                    "id": "titus_transportation",
                    "name": "Titus Transportation",
                    "timeZone": "America/Chicago"
                  },
                  "key": {
                    "id": "ak_7Hq2mV9sKd3pL0aX",
                    "name": "McLeod dispatch",
                    "prefix": "nvl_live_7Hq2",
                    "environment": "live",
                    "scopes": [
                      "routes:write",
                      "vehicles:read",
                      "drivers:read"
                    ],
                    "createdAt": "2026-09-02T15:04:11Z",
                    "expiresAt": null
                  },
                  "rateLimit": {
                    "limit": 600,
                    "window": "1m"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/v1/routes": {
      "post": {
        "tags": [
          "Routes"
        ],
        "operationId": "createOrUpdateRoute",
        "summary": "Create or update a route",
        "description": "Push a dispatch route from your TMS. Novil matches the vehicle and driver, creates the route as a draft, and plans fuel stops automatically when the truck arrives at the first stop.\n\n**Idempotent by `externalId`.** Sending the same `externalId` again updates the route and returns `200`. If a load you already pushed comes back under a new `externalId` but the same `reference`, Novil supersedes the old draft instead of creating a duplicate (when the company has supersede enabled).\n\n**Matching.** `vehicle.unitNumber` is matched against the unit numbers in Novil, and `driver.name` against driver names, through the same mappings fuel-card transactions use. When either is unknown the request is accepted with `202` and the route is held as `pending_mapping`; once someone maps the unit or driver under Settings › Vehicle Matching / Driver Matching, Novil imports the route on its next five-minute pass. Nothing needs to be resent.\n\n**Import window.** Routes scheduled further out than the company's import window (7 days by default) are accepted with `202` as `pending_window` and imported when they enter the window.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RouteInput"
              },
              "example": {
                "externalId": "ord_88213",
                "reference": "Load 23787",
                "vehicle": {
                  "unitNumber": "4471"
                },
                "driver": {
                  "name": "Dana Whitfield"
                },
                "scheduledStart": "2026-09-14T06:00:00-05:00",
                "stops": [
                  {
                    "name": "Dallas DC",
                    "address": "2200 Irving Blvd, Dallas, TX 75207",
                    "latitude": 32.7876,
                    "longitude": -96.8302
                  },
                  {
                    "name": "Memphis Yard",
                    "address": "4100 Air Park St, Memphis, TN 38118",
                    "latitude": 35.0512,
                    "longitude": -89.9613,
                    "arrivalWindow": {
                      "start": "2026-09-14T14:00:00-05:00",
                      "end": "2026-09-14T18:00:00-05:00"
                    }
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The route was created and the vehicle and driver matched immediately.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Route"
                },
                "example": {
                  "$ref": "#/components/examples/RouteDraft/value"
                }
              }
            }
          },
          "200": {
            "description": "An existing `externalId` was sent again and the route was updated.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Route"
                }
              }
            }
          },
          "202": {
            "description": "Accepted and held. `status` says why: `pending_mapping` (vehicle or driver not matched yet) or `pending_window` (scheduled outside the import window). Novil retries automatically.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Route"
                },
                "example": {
                  "id": "b4f1c1e2-6f4b-4a8c-9a3e-2c5d8e7f1a90",
                  "externalId": "ord_88214",
                  "reference": "Load 23788",
                  "name": "Load 23788",
                  "status": "pending_mapping",
                  "environment": "live",
                  "pending": {
                    "reason": "vehicle_unmapped",
                    "detail": "Unit number 4488 is not mapped to a vehicle. Map it under Settings › Vehicle Matching."
                  },
                  "vehicle": {
                    "id": null,
                    "unitNumber": "4488"
                  },
                  "driver": {
                    "id": "drv_a17c",
                    "name": "Dana Whitfield"
                  },
                  "scheduledStart": "2026-09-15T11:00:00Z",
                  "stops": [
                    {
                      "sequence": 0,
                      "name": "Dallas DC",
                      "type": "waypoint"
                    },
                    {
                      "sequence": 1,
                      "name": "Memphis Yard",
                      "type": "destination"
                    }
                  ],
                  "distanceMiles": null,
                  "fuelPlan": null,
                  "createdAt": "2026-09-13T14:42:17Z"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ValidationError"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "409": {
            "description": "The route already started. Stops on an active route cannot change; cancel it and push a new one.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                },
                "example": {
                  "type": "about:blank",
                  "title": "Route is active",
                  "status": 409,
                  "code": "route_active",
                  "detail": "Route b4f1c1e2… is active and cannot be changed. Cancel it and push a new route."
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      },
      "get": {
        "tags": [
          "Routes"
        ],
        "operationId": "listRoutes",
        "summary": "List routes",
        "description": "Routes created through the API, newest scheduled start first. Filter by status, external id, vehicle, or a scheduled-start range.",
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "schema": {
              "$ref": "#/components/schemas/RouteStatus"
            },
            "description": "Only routes in this status."
          },
          {
            "name": "externalId",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Your identifier for the load. Returns at most one route."
          },
          {
            "name": "vehicleId",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Only routes assigned to this Novil vehicle id."
          },
          {
            "name": "from",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date-time"
            },
            "description": "Scheduled start at or after this time."
          },
          {
            "name": "to",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date-time"
            },
            "description": "Scheduled start before this time."
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Offset"
          }
        ],
        "responses": {
          "200": {
            "description": "A page of routes.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RouteList"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/routes/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          },
          "description": "The route id returned when the route was pushed."
        }
      ],
      "get": {
        "tags": [
          "Routes"
        ],
        "operationId": "getRoute",
        "summary": "Retrieve a route",
        "description": "The route, its current status, its stops with any actual arrival times, and its fuel plan once one exists. Poll this after pushing: `draft` becomes `planned` when Novil has built the plan and `active` once it has been sent to the driver.",
        "responses": {
          "200": {
            "description": "The route.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Route"
                },
                "examples": {
                  "draft": {
                    "$ref": "#/components/examples/RouteDraft"
                  },
                  "active": {
                    "$ref": "#/components/examples/RoutePlanned"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      },
      "delete": {
        "tags": [
          "Routes"
        ],
        "operationId": "cancelRoute",
        "summary": "Cancel a route",
        "description": "Cancels a route that has not started. A held (`pending_*`) route is dropped; a draft or planned route is retired with the reason \"Canceled via API\". Pushing the same `externalId` again afterwards creates a fresh route. Active and completed routes cannot be canceled here.",
        "responses": {
          "204": {
            "description": "Canceled."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "The route is active or already finished.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/vehicles": {
      "get": {
        "tags": [
          "Vehicles"
        ],
        "operationId": "listVehicles",
        "summary": "List vehicles",
        "description": "The company's vehicles as Novil knows them. Use `unitNumber` or `vin` to check how a truck will match before pushing a route.",
        "parameters": [
          {
            "name": "unitNumber",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Exact unit number."
          },
          {
            "name": "vin",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Exact 17-character VIN."
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Offset"
          }
        ],
        "responses": {
          "200": {
            "description": "A page of vehicles.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/VehicleList"
                },
                "example": {
                  "data": [
                    {
                      "id": "veh_3f9a",
                      "unitNumber": "4471",
                      "vin": "3AKJHHDR5NSMK1234",
                      "licensePlate": "TX R47-1122",
                      "make": "Freightliner",
                      "model": "Cascadia",
                      "year": "2022",
                      "fuelTankCapacityGallons": 240
                    }
                  ],
                  "total": 1,
                  "limit": 25,
                  "offset": 0
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/drivers": {
      "get": {
        "tags": [
          "Drivers"
        ],
        "operationId": "listDrivers",
        "summary": "List drivers",
        "description": "The company's drivers. Use `name` to check how a driver will match before pushing a route; the match is on the driver's full name as it appears in Novil.",
        "parameters": [
          {
            "name": "name",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Case-insensitive substring of the driver's full name."
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Offset"
          }
        ],
        "responses": {
          "200": {
            "description": "A page of drivers.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DriverList"
                },
                "example": {
                  "data": [
                    {
                      "id": "drv_a17c",
                      "name": "Dana Whitfield",
                      "displayName": "Dana W.",
                      "externalId": null
                    }
                  ],
                  "total": 1,
                  "limit": 25,
                  "offset": 0
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "apiKey": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "nvl_live_… or nvl_test_…",
        "description": "An API key from Settings › API Keys, sent as `Authorization: Bearer nvl_live_…`. Live keys act on your fleet; `nvl_test_` keys act on your sandbox copy and never message a driver."
      }
    },
    "parameters": {
      "Limit": {
        "name": "limit",
        "in": "query",
        "schema": {
          "type": "integer",
          "minimum": 1,
          "maximum": 100,
          "default": 25
        },
        "description": "Page size, at most 100."
      },
      "Offset": {
        "name": "offset",
        "in": "query",
        "schema": {
          "type": "integer",
          "minimum": 0,
          "default": 0
        },
        "description": "Number of items to skip."
      }
    },
    "responses": {
      "Unauthorized": {
        "description": "The key is missing, malformed, revoked or expired.",
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            },
            "example": {
              "type": "about:blank",
              "title": "Unauthorized",
              "status": 401,
              "code": "unauthorized",
              "detail": "The API key is missing, revoked or expired."
            }
          }
        }
      },
      "Forbidden": {
        "description": "The key does not have the scope this endpoint needs.",
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            },
            "example": {
              "type": "about:blank",
              "title": "Forbidden",
              "status": 403,
              "code": "insufficient_scope",
              "detail": "This endpoint needs the routes:write scope."
            }
          }
        }
      },
      "NotFound": {
        "description": "No route with that id belongs to this company.",
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            },
            "example": {
              "type": "about:blank",
              "title": "Not found",
              "status": 404,
              "code": "not_found",
              "detail": "No route with id b4f1c1e2…"
            }
          }
        }
      },
      "ValidationError": {
        "description": "The body failed validation. `errors` names each field.",
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/ValidationProblem"
            },
            "example": {
              "type": "about:blank",
              "title": "Validation failed",
              "status": 400,
              "code": "validation_error",
              "detail": "The request body is invalid.",
              "errors": [
                {
                  "field": "stops",
                  "message": "At least two stops are required."
                },
                {
                  "field": "stops[1].latitude",
                  "message": "Latitude is required."
                }
              ]
            }
          }
        }
      },
      "RateLimited": {
        "description": "More than 600 requests in a minute from this key. Retry after the number of seconds in `Retry-After`.",
        "headers": {
          "Retry-After": {
            "schema": {
              "type": "integer"
            },
            "description": "Seconds to wait before retrying."
          },
          "X-RateLimit-Limit": {
            "schema": {
              "type": "integer"
            }
          },
          "X-RateLimit-Remaining": {
            "schema": {
              "type": "integer"
            }
          }
        },
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            },
            "example": {
              "type": "about:blank",
              "title": "Too many requests",
              "status": 429,
              "code": "rate_limited",
              "detail": "More than 600 requests in a minute. Retry after 12 seconds."
            }
          }
        }
      }
    },
    "examples": {
      "RouteDraft": {
        "value": {
          "id": "9d1e3d0c-1b7a-4e0a-8a55-7b7c2b0e1f6d",
          "externalId": "ord_88213",
          "reference": "Load 23787",
          "name": "Load 23787",
          "status": "draft",
          "environment": "live",
          "pending": null,
          "vehicle": {
            "id": "veh_3f9a",
            "unitNumber": "4471"
          },
          "driver": {
            "id": "drv_a17c",
            "name": "Dana Whitfield"
          },
          "scheduledStart": "2026-09-14T11:00:00Z",
          "stops": [
            {
              "sequence": 0,
              "name": "Dallas DC",
              "address": "2200 Irving Blvd, Dallas, TX 75207",
              "latitude": 32.7876,
              "longitude": -96.8302,
              "type": "waypoint",
              "plannedArrival": null,
              "actualArrival": null,
              "actualDeparture": null
            },
            {
              "sequence": 1,
              "name": "Memphis Yard",
              "address": "4100 Air Park St, Memphis, TN 38118",
              "latitude": 35.0512,
              "longitude": -89.9613,
              "type": "destination",
              "plannedArrival": "2026-09-14T19:00:00Z",
              "actualArrival": null,
              "actualDeparture": null
            }
          ],
          "distanceMiles": 452.3,
          "fuelPlan": null,
          "createdAt": "2026-09-13T14:42:18Z"
        }
      },
      "RoutePlanned": {
        "value": {
          "id": "9d1e3d0c-1b7a-4e0a-8a55-7b7c2b0e1f6d",
          "externalId": "ord_88213",
          "reference": "Load 23787",
          "name": "Load 23787",
          "status": "active",
          "environment": "live",
          "pending": null,
          "vehicle": {
            "id": "veh_3f9a",
            "unitNumber": "4471"
          },
          "driver": {
            "id": "drv_a17c",
            "name": "Dana Whitfield"
          },
          "scheduledStart": "2026-09-14T11:00:00Z",
          "stops": [
            {
              "sequence": 0,
              "name": "Dallas DC",
              "address": "2200 Irving Blvd, Dallas, TX 75207",
              "latitude": 32.7876,
              "longitude": -96.8302,
              "type": "waypoint",
              "plannedArrival": null,
              "actualArrival": "2026-09-14T11:04:12Z",
              "actualDeparture": "2026-09-14T11:31:40Z"
            },
            {
              "sequence": 1,
              "name": "Love's #412",
              "address": "I-30 Exit 94, Sulphur Springs, TX",
              "latitude": 33.1362,
              "longitude": -95.6011,
              "type": "fuel_stop",
              "plannedArrival": "2026-09-14T13:10:00Z",
              "actualArrival": null,
              "actualDeparture": null
            },
            {
              "sequence": 2,
              "name": "Memphis Yard",
              "address": "4100 Air Park St, Memphis, TN 38118",
              "latitude": 35.0512,
              "longitude": -89.9613,
              "type": "destination",
              "plannedArrival": "2026-09-14T19:00:00Z",
              "actualArrival": null,
              "actualDeparture": null
            }
          ],
          "distanceMiles": 452.3,
          "fuelPlan": {
            "status": "active",
            "generatedAt": "2026-09-14T11:06:40Z",
            "totalDistanceMiles": 452.3,
            "totalGallons": 96.5,
            "totalCost": 341.12,
            "averagePricePerGallon": 3.535,
            "savingsPerGallon": 0.212,
            "savingsTotal": 20.46,
            "stops": [
              {
                "sequence": 1,
                "name": "Love's #412",
                "address": "I-30 Exit 94, Sulphur Springs, TX",
                "latitude": 33.1362,
                "longitude": -95.6011,
                "milesFromOrigin": 83.4,
                "gallonsToFill": 96.5,
                "pricePerGallon": 3.535,
                "totalCost": 341.12,
                "arrivalFuelPercent": 22,
                "estimatedArrival": "2026-09-14T13:10:00Z"
              }
            ]
          },
          "createdAt": "2026-09-13T14:42:18Z"
        }
      }
    },
    "schemas": {
      "RouteInput": {
        "type": "object",
        "required": [
          "externalId",
          "vehicle",
          "driver",
          "scheduledStart",
          "stops"
        ],
        "properties": {
          "externalId": {
            "type": "string",
            "maxLength": 200,
            "description": "Your TMS's identifier for the load. Used for idempotency and to find the route again later."
          },
          "reference": {
            "type": "string",
            "maxLength": 200,
            "description": "The load or order number as dispatchers know it. Becomes the route name in Novil, for example `Load 23787`, and pairs re-pushed loads for superseding."
          },
          "vehicle": {
            "$ref": "#/components/schemas/VehicleRef"
          },
          "driver": {
            "$ref": "#/components/schemas/DriverRef"
          },
          "scheduledStart": {
            "type": "string",
            "format": "date-time",
            "description": "When the truck should leave the first stop, RFC 3339 with an offset. Routes further out than the company's import window are held as `pending_window`."
          },
          "stops": {
            "type": "array",
            "minItems": 2,
            "maxItems": 50,
            "items": {
              "$ref": "#/components/schemas/RouteStopInput"
            },
            "description": "At least two, in driving order. The last stop is the destination."
          }
        }
      },
      "VehicleRef": {
        "type": "object",
        "description": "The truck. Provide `unitNumber` or `vin`.",
        "properties": {
          "unitNumber": {
            "type": "string",
            "description": "The unit number exactly as it appears in Novil."
          },
          "vin": {
            "type": "string",
            "minLength": 17,
            "maxLength": 17,
            "description": "17-character VIN, when unit numbers differ between your systems."
          }
        }
      },
      "DriverRef": {
        "type": "object",
        "description": "The driver. Provide `name` or `externalId`.",
        "properties": {
          "name": {
            "type": "string",
            "description": "Full name as it appears in Novil."
          },
          "externalId": {
            "type": "string",
            "description": "Your driver id, when drivers are synced to Novil from a telematics provider."
          }
        }
      },
      "RouteStopInput": {
        "type": "object",
        "required": [
          "name",
          "latitude",
          "longitude"
        ],
        "properties": {
          "name": {
            "type": "string",
            "maxLength": 200,
            "description": "Shown to the driver and on the route timeline."
          },
          "address": {
            "type": "string",
            "maxLength": 400,
            "description": "Street address, for display only. This endpoint does not geocode."
          },
          "latitude": {
            "type": "number",
            "minimum": -90,
            "maximum": 90
          },
          "longitude": {
            "type": "number",
            "minimum": -180,
            "maximum": 180
          },
          "arrivalWindow": {
            "$ref": "#/components/schemas/ArrivalWindow"
          }
        }
      },
      "ArrivalWindow": {
        "type": "object",
        "description": "The appointment window at the stop, when there is one.",
        "properties": {
          "start": {
            "type": "string",
            "format": "date-time"
          },
          "end": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "RouteStatus": {
        "type": "string",
        "enum": [
          "pending",
          "pending_mapping",
          "pending_window",
          "draft",
          "planned",
          "active",
          "completed",
          "error",
          "canceled"
        ],
        "description": "`pending*` statuses mean the route is accepted but not yet imported: `pending_mapping` waits on a vehicle or driver mapping, `pending_window` waits for the scheduled start to enter the import window, `pending` has not been attempted yet. `draft` is imported and waiting for the truck; `planned` has a fuel plan ready; `active` has been sent to the driver; `completed`, `error` and `canceled` are terminal."
      },
      "Route": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Stable for the life of the route, assigned when the route is pushed."
          },
          "externalId": {
            "type": "string"
          },
          "reference": {
            "type": [
              "string",
              "null"
            ]
          },
          "name": {
            "type": "string",
            "description": "The route name shown in Novil."
          },
          "status": {
            "$ref": "#/components/schemas/RouteStatus"
          },
          "environment": {
            "type": "string",
            "enum": [
              "live",
              "sandbox"
            ]
          },
          "pending": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/RoutePending"
              },
              {
                "type": "null"
              }
            ],
            "description": "Why the route is held, when `status` is a `pending*` value."
          },
          "vehicle": {
            "$ref": "#/components/schemas/RouteVehicle"
          },
          "driver": {
            "$ref": "#/components/schemas/RouteDriver"
          },
          "scheduledStart": {
            "type": "string",
            "format": "date-time"
          },
          "stops": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/RouteStop"
            }
          },
          "distanceMiles": {
            "type": [
              "number",
              "null"
            ],
            "description": "Routed distance once Novil has computed it."
          },
          "fuelPlan": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/FuelPlan"
              },
              {
                "type": "null"
              }
            ],
            "description": "The route's fuel plan, once one exists: null while `draft`, the plan Novil selected once `planned`, the plan sent to the driver once `active`. The route IS the plan; there is no separate resource to fetch."
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "RoutePending": {
        "type": "object",
        "properties": {
          "reason": {
            "type": "string",
            "enum": [
              "vehicle_unmapped",
              "driver_unmapped",
              "outside_import_window",
              "not_attempted",
              "error"
            ]
          },
          "detail": {
            "type": "string"
          }
        }
      },
      "RouteVehicle": {
        "type": "object",
        "properties": {
          "id": {
            "type": [
              "string",
              "null"
            ],
            "description": "Novil vehicle id once matched."
          },
          "unitNumber": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "RouteDriver": {
        "type": "object",
        "properties": {
          "id": {
            "type": [
              "string",
              "null"
            ],
            "description": "Novil driver id once matched."
          },
          "name": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "RouteStop": {
        "type": "object",
        "properties": {
          "sequence": {
            "type": "integer"
          },
          "name": {
            "type": "string"
          },
          "address": {
            "type": [
              "string",
              "null"
            ]
          },
          "latitude": {
            "type": [
              "number",
              "null"
            ]
          },
          "longitude": {
            "type": [
              "number",
              "null"
            ]
          },
          "type": {
            "type": "string",
            "enum": [
              "waypoint",
              "destination",
              "fuel_stop"
            ],
            "description": "`fuel_stop` entries appear once a plan is active."
          },
          "plannedArrival": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "actualArrival": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Set from the truck's GPS when it reaches the stop."
          },
          "actualDeparture": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          }
        }
      },
      "FuelPlan": {
        "type": "object",
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "planned",
              "active",
              "completed"
            ]
          },
          "generatedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "totalDistanceMiles": {
            "type": "number"
          },
          "totalGallons": {
            "type": "number"
          },
          "totalCost": {
            "type": "number"
          },
          "averagePricePerGallon": {
            "type": "number"
          },
          "savingsPerGallon": {
            "type": [
              "number",
              "null"
            ],
            "description": "Average corridor price minus the plan's average price."
          },
          "savingsTotal": {
            "type": [
              "number",
              "null"
            ]
          },
          "stops": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/FuelPlanStop"
            }
          }
        },
        "description": "The fuel stops Novil planned for the route: where to stop, how many gallons to buy, and at what price. Once the route is `active`, the same stops also appear in the route's `stops` timeline as `fuel_stop` entries."
      },
      "FuelPlanStop": {
        "type": "object",
        "properties": {
          "sequence": {
            "type": "integer"
          },
          "name": {
            "type": "string"
          },
          "address": {
            "type": [
              "string",
              "null"
            ]
          },
          "latitude": {
            "type": "number"
          },
          "longitude": {
            "type": "number"
          },
          "milesFromOrigin": {
            "type": "number"
          },
          "gallonsToFill": {
            "type": "number"
          },
          "pricePerGallon": {
            "type": "number",
            "description": "Net price including tax."
          },
          "totalCost": {
            "type": "number"
          },
          "arrivalFuelPercent": {
            "type": "integer"
          },
          "estimatedArrival": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          }
        }
      },
      "RouteList": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Route"
            }
          },
          "total": {
            "type": "integer"
          },
          "limit": {
            "type": "integer"
          },
          "offset": {
            "type": "integer"
          }
        }
      },
      "Vehicle": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "unitNumber": {
            "type": [
              "string",
              "null"
            ]
          },
          "vin": {
            "type": [
              "string",
              "null"
            ]
          },
          "licensePlate": {
            "type": [
              "string",
              "null"
            ]
          },
          "make": {
            "type": [
              "string",
              "null"
            ]
          },
          "model": {
            "type": [
              "string",
              "null"
            ]
          },
          "year": {
            "type": [
              "string",
              "null"
            ]
          },
          "fuelTankCapacityGallons": {
            "type": [
              "number",
              "null"
            ]
          }
        }
      },
      "VehicleList": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Vehicle"
            }
          },
          "total": {
            "type": "integer"
          },
          "limit": {
            "type": "integer"
          },
          "offset": {
            "type": "integer"
          }
        }
      },
      "Driver": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": [
              "string",
              "null"
            ],
            "description": "Full name; what `driver.name` on a route is matched against."
          },
          "displayName": {
            "type": [
              "string",
              "null"
            ]
          },
          "externalId": {
            "type": [
              "string",
              "null"
            ],
            "description": "The telematics provider's driver id, when the driver was synced."
          }
        }
      },
      "DriverList": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Driver"
            }
          },
          "total": {
            "type": "integer"
          },
          "limit": {
            "type": "integer"
          },
          "offset": {
            "type": "integer"
          }
        }
      },
      "Me": {
        "type": "object",
        "properties": {
          "company": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string"
              },
              "name": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "timeZone": {
                "type": "string"
              }
            }
          },
          "key": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string"
              },
              "name": {
                "type": "string"
              },
              "prefix": {
                "type": "string"
              },
              "environment": {
                "type": "string",
                "enum": [
                  "live",
                  "sandbox"
                ]
              },
              "scopes": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "createdAt": {
                "type": "string",
                "format": "date-time"
              },
              "expiresAt": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "date-time"
              }
            }
          },
          "rateLimit": {
            "type": "object",
            "properties": {
              "limit": {
                "type": "integer"
              },
              "window": {
                "type": "string"
              }
            }
          }
        }
      },
      "Problem": {
        "type": "object",
        "description": "RFC 9457 problem details, with a stable machine-readable `code`.",
        "properties": {
          "type": {
            "type": "string"
          },
          "title": {
            "type": "string"
          },
          "status": {
            "type": "integer"
          },
          "code": {
            "type": "string",
            "enum": [
              "validation_error",
              "unauthorized",
              "insufficient_scope",
              "not_found",
              "route_active",
              "rate_limited",
              "internal_error"
            ]
          },
          "detail": {
            "type": "string"
          }
        }
      },
      "ValidationProblem": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Problem"
          },
          {
            "type": "object",
            "properties": {
              "errors": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "field": {
                      "type": "string"
                    },
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        ]
      }
    }
  }
}
