Spoke Public API

The Spoke Public API (formerly Circuit for Teams) is an OpenAPI 3.1 HTTP/JSON API for managing delivery plans, stops, drivers, depots, routes, members and operations, optimizing and distributing routes, searching stops, and receiving webhook events. Authentication is an API key sent via HTTP Basic (key as username) or Bearer token.

OpenAPI Specification

circuit-v1-openapi-original.json Raw ↑
{
  "openapi": "3.1.0",
  "info": {
    "title": "Spoke API",
    "description": "This is the documentation of the Spoke Public API HTTP endpoints. The Spoke\nPublic API is a way for you to interact with Spoke products programmatically.\n\n# Introduction\n\nThe API has a set of HTTP methods to operate on Spoke resources for a specific\nteam.\n\nCurrently, Spoke offers no programming SDKs for interacting with the Public\nAPI, but you can implement your own interface by calling the methods documented\non this page.\n\n# v1 Migration Guide\n\nIf migrating from version 0.2b to v1, please note the following changes.\n\n### General\n\n1. Only `api.spoke.com` should be used for the `v1` API.\n2. Webhooks requests will now only contain the `spoke-signature` header.\n\n### Stops\n\nRemovals:\n\n| Operation  | 0.2b Property                   | v1 Property                      | Notes                                                                        |\n| ---------- | ------------------------------- | -------------------------------- | ---------------------------------------------------------------------------- |\n| WRITE      | `stop.driver`                   | `stop.allowedDrivers`            | Drivers should now be provided as an array of `DriverIds`.                   |\n| READ       | `stop.driverIdentifier`         | `stop.allowedDrivers`            | Drivers will now be returned as an array of `DriverIds`.                     |\n| READ       | `stop.allowedDriverIdentifiers` | `stop.allowedDrivers`            | Drivers will now be returned as an array of `DriverIds`.                     |\n| READ       | `stop.etaNullReason`            | `stop.eta.status`                | `eta` now has a status property with the same information.                   |\n| READ       | `stop.timeAtStopInfoNullReason` | `stop.timeAtStopInfo.status`     | `timeAtStopInfo` now has a status property with the same information.        |\n| READ/WRITE | `stop.circuitClientId`          | `stop.clientId`                  | When creating or reading stops, `circuitClientId` will now be `clientId`     |\n| READ       | `stop.deliveryInfo.arrivedAt`   | `timeAtStopInfo.value.arrivedAt` | This property is only available when `timeAtStopInfo.status === 'available'` |\n\nChanges:\n\n1. READ: `stop.eta` is now an object containing both the eta value, and additional metadata about the value.\n2. READ: `stop.timeAtStopInfo` structure has changed so that the original properties are now under `timeAtStopInfo.value.*`,\n   and an additional field `timeAtStopInfo.status` has been added to indicate why the info is not set.\n\n### Plans\n\nChanges:\n\n1. Previously we would allow `Drivers` to be added even if their `Depot` did not match the plan's `Depot`.\n   This is no longer allowed. Non-matching `Drivers` must now have their `Depot` changed to match the target `Plan`,\n   or vice versa.\n2. `Driver` objects are no longer returned as part of `Plan` endpoint responses. Instead, `DriverIds` are returned and\n   the associated drivers must be fetched separately (if needed).\n\n# Using the API\n\nThis section describes how the Spoke API should be used, if you wish to see\nexample implementations take a look at the [API Usage\nExamples](/docs/api-examples) page.\n\n## Address\n\nThe base API address for this version of the API to be used before every\nendpoint listed here is: `https://api.spoke.com/public/v1`\n\nNotice the `https://` prefix, Spoke API will **not** accept plain HTTP\nrequests.\n\n## Authentication\n\nTo use Spoke Public API endpoints you will first need to generate an API key\nfor authenticating with our servers.\n\nTo do this you need to go to your Spoke Dispatch settings page > Integrations\n\\> API, and generate a new key there.\n\nOnce you have the API key you can use it in the `Authorization` header\neither using the Basic scheme, or as a Bearer token.\n\n### Basic Auth\n\n[Basic Authentication](https://en.wikipedia.org/wiki/Basic_access_authentication) is the primary authentication scheme used\nby Spoke's API.\n\nBasic authentication typically uses a base64 encoded `username:password`\npair, but since we only require an API key we instead structure the payload as `[yourApiKey]:[empty]`. This\nvalue is then base64 encoded as usual.\n\nExample with `curl`:\n\n```bash\ncurl \"https://api.spoke.com/public/v1/plans\" -u yourApiKey:\n```\n\nIf you did everything correctly you should see a list of your team's plans in response\nto this request.\n\nThe `-u` flag adds a header to the request in the following format:\n\n```http\nAuthorization: Basic eW91ckFwaUtleToK\n```\n\nNote that curl automatically base64 encoded the data. Other clients will do the same e.g. if using Postman you would\nconfigure the request's Authorization option instead of adding a header directly.\n\n### Bearer Token\n\nAlternatively you can pass the API key as a Bearer token without any special encoding.\ni.e. `Authorization: Bearer [yourApiKey]`.\n\nExample with `curl`:\n\n```bash\ncurl \"https://api.spoke.com/public/v1/plans\" -H \"Authorization: Bearer yourApiKey\"\n```\n\n## On resource types\n\nEvery response from the Spoke Public API will be as JSON Objects, and every\nrequest that has a body also needs to be in that type, and the header\n`Content-type: application/json` needs to present, so the server knows that the\nbody you are sending is valid JSON. Spoke will reject requests without that\nheader, so be sure to use it.\n\n## On resource IDs\n\nEvery resource in the Spoke Public API has a unique ID. This ID is generated\nby Spoke and is unique for every resource per collection.\n\nEvery resource representation returned by the API will show the ID in the\nfollowing format:\n\n```json\n{\n  \"id\": \"collectionName/resourceId\"\n}\n```\n\nAnd if the resource is on a sub-collection, it will be in the following format:\n\n```json\n{\n  \"id\": \"collectionName/resourceId/subcollectionName/subResourceId\"\n}\n```\n\nFor example, a stop with ID `stop1` under a plan with ID `plan1` will have the\nfollowing representation on its serialized ID field:\n\n```json\n{\n  \"id\": \"plans/plan1/stops/stop1\"\n}\n```\n\nThis means that if you wish to directly retrieve this stop by using a GET\nendpoint you can simply use this ID as follows:\n\n```bash\ncurl \"https://api.spoke.com/public/v1/`serializedId`\" -u yourApiKey:\n```\n\nFor the example above this would be:\n\n```bash\ncurl \"https://api.spoke.com/public/v1/plans/plan1/stops/stop1\" -u yourApiKey:\n```\n\n## On List endpoints\n\nWhen using list endpoints, you have the ability to combine various query options\nto locate the desired results. Some endpoints even offer specific filter options\nto aid in this search.\n\nAll the list endpoints employ pagination. This means that a single request might\nonly return a part of the entire set of resources you're aiming to retrieve. To\nmove through the subsequent pages, the Spoke API provides a `nextPageToken`\nfield in the response of every list endpoint query.\n\nIt's crucial to understand that when a `nextPageToken` is returned, it indicates\nthat more data is available. For every subsequent request, this token should be\nadded as the `pageToken` query parameter. And, importantly, **all the original\nquery parameters** used in the first request must also be included.\n\nHere's a step-by-step example to elucidate this:\n\n1. Suppose you initiate a request to list your plans:\n\n```bash\ncurl \"https://api.spoke.com/public/v1/plans?filter.startsGte=2023-05-01\" -u yourApiKey:\n```\n\n2. The Spoke API might return a response like:\n\n```json\n{\n  // other attributes\n  \"nextPageToken\": \"I53Jr5Eu2qK9omh0iA8q\"\n}\n```\n\n3. To retrieve the next page of data, use the returned `nextPageToken` as the\n   `pageToken` query parameter, and ensure all initial query parameters remain\n   the same:\n\n```bash\ncurl \"https://api.spoke.com/public/v1/plans?filter.startsGte=2023-05-01&pageToken=I53Jr5Eu2qK9omh0iA8q\" -u yourApiKey:\n```\n\n4. Repeat step 3 for all subsequent pages, updating the `pageToken` parameter\n   with the latest `nextPageToken` value returned until `nextPageToken` is\n   `null` in the response.\n\nSpoke also accepts limiting the maximum number of results per page by using\nthe `maxPageSize` query parameter, but notice that each individual endpoint has\na maximum value you can set this to.\n\n## On Update endpoints (HTTP PATCH verb)\n\nUpdate methods differ from the other methods in the API in the sense that\nmissing values in the JSON representation will **not** act upon the\nrepresentation of the resource.\n\nExplaining this by example: Suppose you have a Plan with the ID `plan1` in your\nplans' collection, and you wanted to merely update its title to `My API Plan`\nwithout changing other information, such as assigned drivers or the start date.\nTo do this you would issue the following request:\n\n```bash\ncurl -X PATCH http://localhost:5005/public/v1/plans/plan1 -H 'Content-type: application/json' -d '{\"title\": \"My API Plan\"}' -u yourApiKey:\n```\n\nNotice how we don't pass any other information in the JSON, only the title. This\nensures that the `PATCH` request will only operate on the provided parameters\nand keep the other parameters as-is.\n\nIt is important to also notice that when updating an array the whole array will\nbe replaced, Spoke API does not support partial updates on arrays.\n\n## On Rate-Limiting\n\nAll the endpoints in the Spoke Public API are rate-limited, which means we\nwill reject requests that come in too fast.\n\nTo know if your request was rate-limited, check if the response has the HTTP\nstatus code 429.\n\nEach endpoint has a different rate limit, and Spoke can change this rate\nlimit at any moment.\n\nThe rate limits are as follows:\n\n- **Rate Limits for Write Endpoints**: All write endpoints have a limit of 5\n  requests per second, which includes Creation (POST), Update (PATCH), and\n  Deletion (DELETE) operations for all models.\n  - An exception to this rule is the Driver Creation endpoint, which is limited to\n    1 request per second. Thus, we recommend using the Batch Import Drivers\n    when adding multiple drivers.\n- **Rate Limits for Read Endpoints**: All read endpoints, including list\n  endpoints, are limited to 10 requests per second.\n- **Rate Limits for Batch Import**:\n  - The _Batch Import_ endpoints for Stops and Unassigned Stops models are\n    limited to 100 requests per _10 minutes_ (with a maximum rate of 30 per _minute_).\n    However, these endpoints can handle the import of up to 1,000 stops per minute.\n    The Creation, Update, and Deletion endpoints for these models still maintain\n    a rate limit of 5 requests per second.\n  - The _Batch Import_ endpoint for Drivers is limited to 2 requests per\n    _minute_. This allows for the import of up to 100 drivers per minute.\n- **Rate Limits for Optimization Endpoints**: The Plan _optimization_ and\n  _re-optimization_ endpoints are limited to 100 requests per _10 minutes_\n  (with a maximum rate of 30 per _minute_), due to the long running nature of these operations.\n\nSpoke API will occasionally support bursts of requests, but they cannot be\nsustained and will be rejected if they last too long.\n\nIf Spoke rejects your request because it exceeds the rate limit, you must\nwait before retrying it. We suggest you use an [exponential\nbackoff](https://en.wikipedia.org/wiki/Exponential_backoff) approach for this.\n\nWe also recommend, besides the exponential backoff algorithm, that you add a\nrandom delay to each attempt to prevent a [thundering herd\nproblem](https://en.wikipedia.org/wiki/Thundering_herd_problem).\n\nIf the client keeps retrying rate-limited requests while being rejected with a\n429 at a high rate, Spoke will keep rejecting the requests until you turn\ndown the request rate.\n\nSpoke will also limit requests if it keeps receiving them at a high frequency\nat or close to the requests limit for an extended period, so while we support\nan occasional burst of requests, if this is sustained for a long period, we\nwill rate-limit the client making them.\n\nWhile we feel these rate-limits will work for the vast majority of use cases we\nunderstand every team is different. So please reach out to us and describe your\nuse case if these limits are not enough for you, and we will evaluate\nincreasing them for your team.\n\n## Stop Search API\n\nSince V1 of the API, a new [stop search endpoint](/api/v1#tag/Stops/operation/searchStops) is available. This endpoint allows searching across\nall stop documents (including unassigned stops), within your permitted data lifecycle.\n\nThe endpoint features full-text keyword search as well as a filtering DSL for including or excluding documents\nfrom the search.\n\n### Keywords\n\nThe most basic use of search is to simply provide keywords with the `keyword` parameter.\nThis will perform a fuzzy full-text search across all applicable text fields in the stop\ndocuments and return the results sorted by relevance (by default).\n\nFor example:\n\n`?keyword=london`\n\n### Filtering DSL\n\nFiltering is performed with a simple, SQL-inspired language. It allows for precise queries\nusing comparison operators and boolean logic.\n\nFor example (non-url-encoded for clarity):\n\n`?filter=address.placeId != \"ChIJj61dQgK6j4AR4GeTYWZsKWw\" and address.countryCode = \"DE\"`\n\n#### Syntax Overview\n\nA filter expression consists of a field path, a comparison operator, and a value:\n\n`[field] [operator] [value]`\n\nMultiple expressions can be combined using boolean operators:\n\n`[condition1] and ([condition2] or [condition3])`\n\n> **Note on encoding: These expressions should be url-encoded before being passed to the API**:\n\n---\n\n#### Supported Data Types\n\nThe DSL uses strictly typed fields and values. The following table describes the available data types and how to represent them in a filter string.\n\n| Type        | Description                                             | Examples                                 |\n| :---------- | :------------------------------------------------------ | :--------------------------------------- |\n| **String**  | Text values enclosed in **double quotes**.              | `\"active\"`, `\"John Doe\"`, `\"2026-01-01\"` |\n| **Integer** | Whole numbers, positive or negative.                    | `123`, `-10`, `0`                        |\n| **Float**   | Decimal numbers, positive or negative.                  | `123.45`, `-33.8688`                     |\n| **Boolean** | Logical values `true` or `false`. Case-insensitive.     | `true`, `FALSE`, `True`                  |\n| **Null**    | Represents an empty or missing value. Case-insensitive. | `null`, `NULL`                           |\n\n---\n\n#### Comparison Operators\n\nThe following operators are available for comparing fields against values.\n\n| Operator | Name             | Description                                                 | Supported Types    |\n| :------- | :--------------- | :---------------------------------------------------------- | :----------------- |\n| `=`      | Equals           | Field value exactly matches the provided value.             | All types          |\n| `!=`     | Not Equals       | Field value does not match the provided value.              | All types          |\n| `~=`     | Phrase Match     | Field value contains the provided string (substring match). | String             |\n| `>`      | Greater Than     | Field value is strictly greater than the provided value.    | String, Int, Float |\n| `>=`     | Greater or Equal | Field value is greater than or equal to the provided value. | String, Int, Float |\n| `<`      | Less Than        | Field value is strictly less than the provided value.       | String, Int, Float |\n| `<=`     | Less or Equal    | Field value is less than or equal to the provided value.    | String, Int, Float |\n\n> **Note on String Comparisons**: The operators `>`, `>=`, `<`, and `<=` can only be used with ISO-8601 date strings (e.g., `createdAt > \"2026-01-01\"`).\n\n---\n\n#### Boolean Operators\n\nBoolean operators allow you to combine multiple comparison filters into complex queries.\n\n| Operator | Description                                    | Precedence |\n| :------- | :--------------------------------------------- | :--------- |\n| `and`    | Returns true if both conditions are met.       | 2 (Higher) |\n| `or`     | Returns true if at least one condition is met. | 1 (Lower)  |\n\n_Operators are case-insensitive (`AND`, `and`, `And` are all valid)._\n\n#### Grouping and Precedence\n\nBy default, `and` has higher precedence than `or`. You can use **parentheses `()`** to group conditions and override this behaviour.\n\n- `a = 1 or b = 2 and c = 3` is evaluated as `a = 1 or (b = 2 and c = 3)`.\n- `(a = 1 or b = 2) and c = 3` forces the `or` condition to be evaluated first.\n\n---\n\n#### Fields and Paths\n\nFields represent the attribute of the resource you are filtering.\n\n- **Nested Fields**: Use dot notation to access nested attributes (e.g., `address.countryCode`).\n\n#### Custom Properties\n\nTeams can create custom properties for stops. These can be used in filters via the\ncustom property's ID (not the name) as a nested field.\n\nFor example:\n\n```\ncustom_property.0234-5678-0abc-def3 = \"value\"\n```\n\nTo know the ID of a custom property, use the [custom properties list endpoint](https://developer.dispatch.spoke.com/api/v1#tag/Team/operation/listCustomStopProperties).\n\n---\n\n#### Data Freshness\n\nNote that data is ingested into the search index more slowly than the realtime API. This means a\nstop may not be immediately available in the search index after it is created. If you need to fetch\na stop immediately, you should always use the stop restful resources e.g. [fetch stop](/api/v1#tag/Stops/operation/getStop)\n\n#### Examples\n\nThe following are examples of valid, parsable filter strings (field names are examples and may not valid for stops):\n\n- **Simple Equality**: `address.countryCode = \"DE\"` - return all stops where the countryCode is exactly \"DE\".\n- **Numeric Comparison**: `address.latitude >= 100` - return all stops where the latitude is greater than or equal to 100.\n- **Substring Search**: `address ~= \"London\"` - return all stops where the address contains \"London\".\n- **Null Check**: `arrivalTime = null` - return all stops where the arrivalTime field is not set.\n- **Boolean Check**: `deliveryInfo.attempted = true` - return all stops where deliveryInfo.attempted is set to true.\n- **Combined Logic**: `address ~= \"London\" and address.countryCode = \"GB\"` - return all stops where the address contains \"London\" and the country code is \"GB\".\n- **Multiple Options**: `address ~= \"London\" or address ~= \"Bristol\" or address ~= \"Manchester\"` - return all stops where the address contains either London, Bristol or Manchester.\n- **Complex Grouping**: `(address ~= \"London\" or address ~= \"Manchester\") and (createdAt >= \"2026-05-01T00:00:00Z\" and createdAt < \"2026-05-02T00:00:00Z\")` - return all stops where the address contains London or Manchester that were created on 2026-05-01.\n- **Date Range**: `createdat >= \"2026-05-01T00:00:00Z\" and createdAt < \"2026-06-01T00:00:00Z\"` - return all stops created in May 2026.\n\nFor code samples see [Searching Stops](/docs/v1/api-examples/search-stops)\n\n## On the models\n\nAfter this section you will find all the Spoke Public API endpoints available.\n\nEvery representation of resources that these endpoints create and return are\ndocumented in the [Models](/docs/category/models) page of the docs.\n",
    "version": "v1"
  },
  "components": {
    "securitySchemes": {
      "BasicAuth": {
        "type": "http",
        "scheme": "basic",
        "description": "Use the API key as the username and leave the password empty."
      }
    },
    "schemas": {
      "customStopPropertySchema": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "The custom stop property id"
          },
          "name": {
            "type": "string",
            "description": "The name of the custom stop property"
          },
          "visibleToDrivers": {
            "type": "boolean",
            "description": "Whether this custom stop property is visible to drivers."
          },
          "visibleToRecipients": {
            "type": "boolean",
            "description": "Whether this property is visible to recipients."
          }
        },
        "required": [
          "id",
          "name",
          "visibleToDrivers",
          "visibleToRecipients"
        ],
        "additionalProperties": false,
        "description": "The definition of a custom stop property."
      },
      "dateSchema": {
        "type": "object",
        "properties": {
          "day": {
            "type": "integer",
            "minimum": 1,
            "maximum": 31,
            "description": "The day of the date."
          },
          "month": {
            "type": "integer",
            "minimum": 1,
            "maximum": 12,
            "description": "The month of the date."
          },
          "year": {
            "type": "integer",
            "minimum": 2000,
            "maximum": 2100,
            "description": "The year of the date."
          }
        },
        "required": [
          "day",
          "month",
          "year"
        ],
        "additionalProperties": false,
        "description": "A date."
      },
      "depotIdSchema": {
        "type": "string",
        "pattern": "^depots\\/[a-zA-Z0-9---_]{1,50}$"
      },
      "depotSchema": {
        "type": "object",
        "properties": {
          "id": {
            "allOf": [
              {
                "$ref": "#/components/schemas/depotIdSchema"
              }
            ],
            "description": "The depot id, in the format `depots/<id>`"
          },
          "name": {
            "type": "string",
            "description": "The name of the depot."
          },
          "routeOverrides": {
            "anyOf": [
              {
                "type": "object",
                "properties": {
                  "startTime": {
                    "type": "object",
                    "properties": {
                      "hour": {
                        "type": "integer",
                        "minimum": -9007199254740991,
                        "maximum": 9007199254740991,
                        "description": "Hour of the day"
                      },
                      "minute": {
                        "type": "integer",
                        "minimum": -9007199254740991,
                        "maximum": 9007199254740991,
                        "description": "Minute of the hour"
                      }
                    },
                    "required": [
                      "hour",
                      "minute"
                    ],
                    "additionalProperties": false,
                    "description": "Default route start time."
                  },
                  "startAddress": {
                    "type": "object",
                    "properties": {
                      "address": {
                        "anyOf": [
                          {
                            "type": "string"
                          },
                          {
                            "type": "null"
                          }
                        ],
                        "description": "The full address."
                      },
                      "addressLineOne": {
                        "anyOf": [
                          {
                            "type": "string"
                          },
                          {
                            "type": "null"
                          }
                        ],
                        "description": "The first line of the address."
                      },
                      "addressLineTwo": {
                        "anyOf": [
                          {
                            "type": "string"
                          },
                          {
                            "type": "null"
                          }
                        ],
                        "description": "The second line of the address."
                      },
                      "latitude": {
                        "anyOf": [
                          {
                            "type": "number"
                          },
                          {
                            "type": "null"
                          }
                        ],
                        "description": "Latitude coordinate of the depot in decimal degrees."
                      },
                      "longitude": {
                        "anyOf": [
                          {
                            "type": "number"
                          },
                          {
                            "type": "null"
                          }
                        ],
                        "description": "Longitude coordinate of the depot in decimal degrees."
                      },
                      "placeId": {
                        "anyOf": [
                          {
                            "type": "string"
                          },
                          {
                            "type": "null"
                          }
                        ],
                        "description": "A Google PlaceID identifying the location of the depot."
                      },
                      "countryCode": {
                        "anyOf": [
                          {
                            "type": "string"
                          },
                          {
                            "type": "null"
                          }
                        ],
                        "description": "The country code of the address."
                      }
                    },
                    "required": [
                      "address",
                      "addressLineOne",
                      "addressLineTwo",
                      "latitude",
                      "longitude",
                      "placeId",
                      "countryCode"
                    ],
                    "description": "The default start address for routes originating from this depot."
                  },
                  "defaultTimeAtStop": {
                    "anyOf": [
                      {
                        "type": "number"
                      },
                      {
                        "type": "null"
                      }
                    ],
                    "description": "Estimated time at stop in seconds."
                  },
                  "endAddress": {
                    "anyOf": [
                      {
                        "type": "object",
                        "properties": {
                          "address": {
                            "anyOf": [
                              {
                                "type": "string"
                              },
                              {
                                "type": "null"
                              }
                            ],
                            "description": "The full address."
                          },
                          "addressLineOne": {
                            "anyOf": [
                              {
                                "type": "string"
                              },
                              {
                                "type": "null"
                              }
                            ],
                            "description": "The first line of the address."
                          },
                          "addressLineTwo": {
                            "anyOf": [
                              {
                                "type": "string"
                              },
                              {
                                "type": "null"
                              }
                            ],
                            "description": "The second line of the address."
                          },
                          "latitude": {
                            "anyOf": [
                              {
                                "type": "number"
                              },
                              {
                                "type": "null"
                              }
                            ],
                            "description": "Latitude coordinate of the depot in decimal degrees."
                          },
                          "longitude": {
                            "anyOf": [
                              {
                                "type": "number"
                              },
                              {
                                "type": "null"
                              }
                            ],
                            "description": "Longitude coordinate of the depot in decimal degrees."
                          },
                          "placeId": {
                            "anyOf": [
                              {
                                "type": "string"
                              },
                              {
                                "type": "null"
                              }
                            ],
                            "description": "A Google PlaceID identifying the location of the depot."
                          },
                          "countryCode": {
                            "anyOf": [
                              {
                                "type": "string"
                              },
                              {
                                "type": "null"
                              }
                            ],
                            "description": "The country code of the address."
                          }
                        },
                        "required": [
                          "address",
                          "addressLineOne",
                          "addressLineTwo",
                          "latitude",
                          "longitude",
                          "placeId",
                          "countryCode"
                        ],
                        "description": "The default end address for routes originating from this depot (if empty, assume a round-trip)."
                      },
                      {
                        "type": "null"
                      }
                    ]
                  },
                  "endTime": {
                    "anyOf": [
                      {
                        "type": "object",
                        "properties": {
                          "hour": {
                            "type": "integer",
                            "minimum": -9007199254740991,
                            "maximum": 9007199254740991,
                            "description": "Hour of the day"
                          },
                          "minute": {
                            "type": "integer",
                            "minimum": -9007199254740991,
                            "maximum": 9007199254740991,
                            "description": "Minute of the hour"
                          }
                        },
                        "required": [
                          "hour",
                          "minute"
                        ],
                        "additionalProperties": false,
                        "description": "Default route end time."
                      },
                      {
                        "type": "null"
                      }
                    ]
                  },
                  "maxStops": {
                    "anyOf": [
                      {
                        "type": "number"
                      },
                      {
                        "type": "null"
                      }
                    ],
                    "description": "Max stops assigned to each driver"
                  },
                  "roadSide": {
                    "anyOf": [
                      {
                        "type": "string",
                        "enum": [
                          "any",
                          "left_only",
            

# --- truncated at 32 KB (1532 KB total) ---
# Full source: https://raw.githubusercontent.com/api-evangelist/circuit/refs/heads/main/openapi/circuit-v1-openapi-original.json