{
  "openapi": "3.1.0",
  "info": {
    "title": "N-Ways Maps Platform — Fleet and logistics API",
    "version": "1.0.0",
    "summary": "Vehicles, drivers, deliveries with live ETAs, tracking links, webhooks, traffic feeds and offline packs.",
    "description": "The logistics suite of the N-Ways Maps Platform (partner products `fleet` and `traffic_feed`).\nEvery call needs a partner API key that includes the product; keys work only\nwhile N-Ways has partner access switched on for you.\n\n**Auth.** Send the key as `X-NWays-Api-Key: nw_…` (servers, rider apps) or `?key=nw_…`.\nFleet calls write your own data (vehicles, tasks, webhooks), so use a key restricted to your\nservers' IP addresses or your rider app (Android package + SHA-1, iOS bundle id). Browsers:\nonly the public tracking page.\n\n**Errors.** Key and quota errors and product errors share one envelope:\n`{\"error\": {\"code\": 404, \"status\": \"NOT_FOUND\", \"reason\": \"TASK_NOT_FOUND\", \"message\": \"…\", \"details\": […]}}`\n(`details`: Google's ErrorInfo with the facts in `metadata` and a Help link to the reason on\nhttps://nways.ng/developers/errors). Platform and key reasons: PLATFORM_DISABLED (503, Retry-After: the\nplatform is switched off, your key is fine), PLATFORM_PREVIEW_ONLY, API_KEY_MISSING, API_KEY_INVALID,\nAPI_KEY_REVOKED, ORGANISATION_CLOSED, ORGANISATION_SUSPENDED, API_ACCESS_OFF, API_TURNED_OFF, PROJECT_SUSPENDED, API_KEY_PAUSED,\nAPI_KEY_SERVICE_BLOCKED, API_KEY_*_BLOCKED, BILLING_DISABLED, RATE_LIMIT_EXCEEDED, QUOTA_EXCEEDED,\nTEMPORARILY_BLOCKED, BACKEND_ERROR. BILLING_DISABLED is a 402: the free monthly allowance is used up\nand there's no credit left (add credit in the console, Billing).\n\n**Features N-Ways turned off.** N-Ways can turn one feature off for an organisation (403, the message\nsays why and whom to contact; nothing else changes): DELIVERIES_OFF (new tasks), CONTRACTORS_OFF\n(sending tasks to N-Ways drivers, inviting or hiring them), SMART_PRICE_OFF (pay suggestions and Smart\nPrice: send `payMax` yourself), WEBHOOKS_OFF (no webhook sends; adding or testing an endpoint is\nrefused), ERP_OFF (the events export), TRACKERS_OFF (adding trackers, and their positions and alarms; the trackers\nstay registered). A suspended organisation's new tasks are refused with\nORGANISATION_SUSPENDED, like its keys; tasks under way carry on.\n\n**Rate limits.** Defaults per key and minute: fleet 6,000, traffic_feed 60.\nAnswers carry `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`.\n\n**Ids.** Vehicles, drivers and tasks are addressed by your own ids (1–64 of `A–Z a–z 0–9 _ . : -`),\nunique per project. What N-Ways makes (webhooks, alert rules and alerts, rider links and invites,\nproofs, webhook deliveries) has an opaque string id: a prefix, `_` and 10 letters or digits\n(`wh_Ub3yH9sLk2`, `rule_…`, `alr_…`, `lnk_…`, `rin_…`, `prf_…`, `dlv_…`). Treat ids as strings and\nnever parse them. The older numbered forms (`wh_12`, `rule_7`, `inv_3`…) are still accepted in paths\nuntil 1 April 2027. Times are RFC 3339 in UTC.\n\n**Data terms.** Positions you send are kept as trip history for `retentionDays` (30 by default,\n1–90), then deleted. ETA and traffic figures serve the trip only: they are cleared when a task\nends. Your positions feed N-Ways' public live traffic only if you opt in (`shareTraffic`, off by\ndefault); then they are used anonymously, like any phone's (a session per vehicle and day, never\nyour ids). The traffic feed is aggregated over many devices and never contains a trace.\n",
    "contact": {
      "name": "N-Ways",
      "email": "hello@nways.ng",
      "url": "https://nways.ng/developers"
    }
  },
  "servers": [
    {
      "url": "https://api.nways.ng/maps/v1/fleet",
      "description": "Partner API host"
    },
    {
      "url": "https://nways.ng/maps/v1/fleet",
      "description": "Same API through the website's host"
    }
  ],
  "security": [
    {
      "apiKeyHeader": []
    },
    {
      "apiKeyQuery": []
    }
  ],
  "tags": [
    {
      "name": "Settings"
    },
    {
      "name": "Drivers"
    },
    {
      "name": "Vehicles"
    },
    {
      "name": "Locations"
    },
    {
      "name": "Tasks"
    },
    {
      "name": "GPS trackers",
      "description": "The trackers fitted to your cars, bikes and trucks (Concox, Jimi, Micodus, Teltonika, Sinotrack, Coban, Queclink, Meitrack,\nRuptela, Suntech, Galileosky, iStartek, JT/T 808 and more, and phones running Traccar Client or OsmAnd). Add one by its IMEI\n(14 digits are fine: the check digit is worked out), and put it on a vehicle now or later (POST /trackers/assign; a vehicle\ncan have several, its position from the best current fix, the primary first); send it the setup SMS steps (`setup`), and its\npositions appear as that vehicle's, with ignition, power and battery. First claim wins: a tracker another organisation\nholds is refused (TRACKER_CLAIMED_ELSEWHERE: move it with /tracker-transfers). Devices report to `trk.nways.ng` on their\nprotocol's port (GT06 5023, Teltonika 5027, H02 5013, the catalogue lists each; 5000 works any of them out); ones that report\nunder a shorter id (TK103, watches, JT/T 808 by terminal or SIM number) are matched to their IMEI;\nphones to `https://nways.ng/v1/trackers/osmand?id=<tracker id>&lat=&lon=&timestamp=&speed=` (OsmAnd's protocol).\nAlarms (SOS, power cut, overspeed, low battery) are alerts of kind `tracker_alarm` and the webhook `asset.tracker_alarm`.\nTry it without hardware: `POST /trackers/{trackerId}/virtual` drives a virtual tracker speaking the device's own protocol,\nor run `go run ./cmd/trackersim` (the backend repository) against your own gateway.\n\n**Supported devices.** `GET https://nways.ng/v1/trackers/catalogue` (no key) lists every brand and model N-Ways supports\n(`status`: supported, or coming for one whose protocol isn't read yet), each model's connection methods (`sms`, `configurator`, `platform`, `app`) and its\nfamily's setup SMS, configurator steps and commands. The same list is at https://nways.ng/developers/gps-trackers.\n\n**Keep your tracking platform.** Already on Traccar, Wialon, Navixy, GPSWox, flespi or your own? Turn on forwarding\n(`POST /trackers/forwarding`) and point your platform at your organisation's address; units are matched to your trackers by\nIMEI or unit id, and units you haven't added are listed (`GET /trackers/forwarding`) for one-tap adding. The addresses\n(https://nways.ng, no API key: the token in the address is the key, so keep it secret and rotate it if it leaks):\n\n- Traccar (and GPSWox): `forward.url = https://nways.ng/v1/trackers/forward/<token>/traccar` with `forward.json = true`\n  (positions) and `event.forward.url` the same (events: ignitionOn/Off, alarms). Or the URL template form,\n  `…/<token>/osmand?id={uniqueId}&lat={latitude}&lon={longitude}&timestamp={fixTime}&speed={speed}&bearing={course}`.\n- flespi: an HTTP stream to `…/<token>/flespi` (its arrays of messages).\n- Wialon (and Navixy) retranslation, Wialon IPS: server `trk.nways.ng`, port 5039, each unit's IMEI as its unique id and\n  your token as its password.\n- Anything else: `POST https://nways.ng/v1/trackers/forward/<token>/webhook` (or `…/forward/webhook` with\n  `Authorization: Bearer <token>`), JSON: one position, an array, or `{\"positions\": [...]}`, at most 500 per request:\n  `unitId` (or `imei`; 4–40 letters, digits, - or _), `lat`, `lng` (or `lon`), `time` (ISO 8601 or epoch seconds or ms;\n  default now), `speedKmh`, `heading`, `altitude`, `accuracy` (m), `sats`, `ignition`, `extVoltage` (V), `batteryV`,\n  `batteryPct`, `odometerKm`, `alarms` (sos, power_cut, overspeed, low_battery, tow, crash, vibration, geofence, removal),\n  `valid` (false: kept in the log, never on the map), `name`. Answer: `{\"taken\": n, \"unknown\": [\"<unit ids not added>\"]}`;\n  401 INVALID_FORWARDING_TOKEN, 400 with what's wrong in which position, 413 above 2 MB or 500 positions.\n\n**Commands** (owners and admins): locate now, restart, reporting interval and heartbeat, over the device's open\nconnection to N-Ways' gateway where its protocol allows, else as the SMS to send it (with its SIM's number). Nothing changes\nwhat the vehicle does: there is no engine cut-off.\n\n**Moving a tracker here** from another organisation: `POST /tracker-transfers`, send the device its restart SMS, then\n`POST /tracker-transfers/{id}/start`; a fresh login within 3 minutes moves it (the other organisation is told).\n"
    },
    {
      "name": "Pickup places",
      "description": "Your stores and warehouses, to choose a delivery's pickup from (`pickupPlaceId`) instead of placing it each time."
    },
    {
      "name": "Customers",
      "description": "Who you deliver to and each one's drop-offs, kept from your deliveries, to send the next one in a few taps."
    },
    {
      "name": "Live"
    },
    {
      "name": "Zones"
    },
    {
      "name": "Rider links"
    },
    {
      "name": "Rider app",
      "description": "Invite riders to N-Ways Business, the companies' phone app (background position sharing, jobs, proof of delivery)."
    },
    {
      "name": "Tracking"
    },
    {
      "name": "Alerts",
      "description": "Rules you set up (leaves an area, enters a zone, not heard from, stopped too long) and the alerts they raise, with acknowledge and resolve."
    },
    {
      "name": "Events",
      "description": "Every event, as webhooks receive it, downloadable for a sheet, Excel or your own systems."
    },
    {
      "name": "Webhooks"
    },
    {
      "name": "Traffic feed"
    },
    {
      "name": "Offline packs"
    }
  ],
  "paths": {
    "/settings": {
      "get": {
        "tags": [
          "Settings"
        ],
        "operationId": "getSettings",
        "summary": "Get the project's fleet settings",
        "responses": {
          "200": {
            "description": "Settings",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Settings"
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      },
      "put": {
        "tags": [
          "Settings"
        ],
        "operationId": "updateSettings",
        "summary": "Change fleet settings (fields left out keep their value)",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SettingsUpdate"
              },
              "example": {
                "brandName": "Chowtime",
                "offlineAfterS": 300
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Settings",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Settings"
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/live": {
      "get": {
        "tags": [
          "Live"
        ],
        "operationId": "getLive",
        "summary": "Every vehicle now, with its rider, state, current delivery and zones",
        "description": "`state`: `moving`, `stopped` (stayed within 50 m for 2 minutes or more; `stoppedSince` says since when), `offline`\n(silent longer than `offlineAfterS`) or `never` (no position yet). `task` is the open delivery assigned last;\n`openTasks` counts them all. `counts` sums the fleet and its open deliveries (`unassigned` = created, no vehicle;\n`late` = open and past `dueAt`, or expected after it).\n",
        "responses": {
          "200": {
            "description": "The fleet now",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Live"
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/alerts": {
      "get": {
        "tags": [
          "Live"
        ],
        "operationId": "getAlerts",
        "summary": "What needs a look now, most urgent first",
        "description": "`offline`: a vehicle with an open delivery silent longer than `offlineAfterS`; `late`: an open delivery past\n`dueAt` or expected after it; `stopped`: a vehicle with an open delivery stopped longer than `stopAlertS`.\n",
        "responses": {
          "200": {
            "description": "Alerts",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "alerts",
                    "at"
                  ],
                  "properties": {
                    "alerts": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Alert"
                      }
                    },
                    "at": {
                      "type": "string",
                      "format": "date-time"
                    }
                  }
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/routes": {
      "get": {
        "tags": [
          "Live"
        ],
        "operationId": "getRoutes",
        "summary": "Where each vehicle on a delivery is going",
        "description": "For every vehicle with an open delivery: the road it has travelled since its first open (or today's finished)\ndelivery began (`done`, at most 6 hours, a point every 15 s at most), the road ahead from its last position\nthrough every stop it hasn't reached (`ahead`, from the routing engine; straight lines while `routed` is false),\nand its stops in order: today's finished ones first, then each open delivery's pickup and drop-off. Lines are\n[lng, lat] (GeoJSON order) and simplified. The road ahead is worked out again when the stops change and every few\nminutes, not on every position: trim it to the vehicle's live position on your map.\n",
        "parameters": [
          {
            "name": "vehicleId",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "One vehicle only."
          }
        ],
        "responses": {
          "200": {
            "description": "Routes",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "routes",
                    "at"
                  ],
                  "properties": {
                    "routes": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/VehicleRoute"
                      }
                    },
                    "at": {
                      "type": "string",
                      "format": "date-time"
                    }
                  }
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/drivers": {
      "get": {
        "tags": [
          "Drivers"
        ],
        "operationId": "listDrivers",
        "summary": "List drivers",
        "responses": {
          "200": {
            "description": "Drivers, by id",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "drivers"
                  ],
                  "properties": {
                    "drivers": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Driver"
                      }
                    }
                  }
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      },
      "post": {
        "tags": [
          "Drivers"
        ],
        "operationId": "createDriver",
        "summary": "Add a driver",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "id"
                ],
                "properties": {
                  "id": {
                    "$ref": "#/components/schemas/Id"
                  },
                  "name": {
                    "type": "string",
                    "maxLength": 100,
                    "description": "The older single name; ignored when firstName and lastName are sent."
                  },
                  "phone": {
                    "type": "string",
                    "maxLength": 32
                  },
                  "firstName": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 50,
                    "description": "Letters (any script, with accents), spaces, hyphens and apostrophes. Send with lastName; name is then 'firstName lastName'."
                  },
                  "lastName": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 50,
                    "description": "Required with firstName; the same rules."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Driver"
                }
              }
            }
          },
          "409": {
            "$ref": "#/components/responses/Error"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/drivers/{id}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/Id"
        }
      ],
      "get": {
        "tags": [
          "Drivers"
        ],
        "operationId": "getDriver",
        "summary": "Get a driver",
        "responses": {
          "200": {
            "description": "Driver",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Driver"
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      },
      "patch": {
        "tags": [
          "Drivers"
        ],
        "operationId": "updateDriver",
        "summary": "Change a driver",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "properties": {
                  "name": {
                    "type": "string",
                    "maxLength": 100,
                    "description": "The older single name; ignored when firstName and lastName are sent."
                  },
                  "phone": {
                    "type": "string",
                    "maxLength": 32
                  },
                  "firstName": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 50,
                    "description": "Letters (any script, with accents), spaces, hyphens and apostrophes. Send with lastName; name is then 'firstName lastName'."
                  },
                  "lastName": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 50,
                    "description": "Required with firstName; the same rules."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Driver",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Driver"
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      },
      "delete": {
        "tags": [
          "Drivers"
        ],
        "operationId": "deleteDriver",
        "summary": "Delete a driver (vehicles keep running without one)",
        "responses": {
          "204": {
            "description": "Deleted"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/vehicles": {
      "get": {
        "tags": [
          "Vehicles"
        ],
        "operationId": "listVehicles",
        "summary": "List vehicles with their last known positions",
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "online",
                "offline"
              ]
            },
            "description": "Only vehicles heard from within `offlineAfterS` (online) or not."
          }
        ],
        "responses": {
          "200": {
            "description": "Vehicles, by id",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "vehicles"
                  ],
                  "properties": {
                    "vehicles": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Vehicle"
                      }
                    }
                  }
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      },
      "post": {
        "tags": [
          "Vehicles"
        ],
        "operationId": "createVehicle",
        "summary": "Add a vehicle",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "id"
                ],
                "properties": {
                  "id": {
                    "$ref": "#/components/schemas/Id"
                  },
                  "name": {
                    "type": "string",
                    "maxLength": 100
                  },
                  "mode": {
                    "$ref": "#/components/schemas/VehicleMode"
                  },
                  "plate": {
                    "type": "string",
                    "maxLength": 20
                  },
                  "driverId": {
                    "type": "string",
                    "description": "A driver's id, or empty for none."
                  },
                  "group": {
                    "type": "string",
                    "maxLength": 60,
                    "description": "Your own grouping (\"Lekki bikes\"), for alert rules."
                  },
                  "colour": {
                    "$ref": "#/components/schemas/VehicleColour"
                  }
                }
              },
              "example": {
                "id": "bike-17",
                "name": "Bike 17",
                "mode": "motorcycle",
                "plate": "LAG 123 XY",
                "driverId": "d-4",
                "group": "Yaba"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Vehicle"
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/vehicles/{id}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/Id"
        }
      ],
      "get": {
        "tags": [
          "Vehicles"
        ],
        "operationId": "getVehicle",
        "summary": "Get a vehicle and its last known position",
        "responses": {
          "200": {
            "description": "Vehicle",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Vehicle"
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      },
      "patch": {
        "tags": [
          "Vehicles"
        ],
        "operationId": "updateVehicle",
        "summary": "Change a vehicle (driverId \"\" removes the driver)",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "properties": {
                  "name": {
                    "type": "string",
                    "maxLength": 100
                  },
                  "mode": {
                    "$ref": "#/components/schemas/VehicleMode"
                  },
                  "plate": {
                    "type": "string",
                    "maxLength": 20
                  },
                  "driverId": {
                    "type": "string"
                  },
                  "group": {
                    "type": "string",
                    "maxLength": 60
                  },
                  "colour": {
                    "$ref": "#/components/schemas/VehicleColour"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Vehicle",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Vehicle"
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      },
      "delete": {
        "tags": [
          "Vehicles"
        ],
        "operationId": "deleteVehicle",
        "summary": "Delete a vehicle and its trip history",
        "responses": {
          "204": {
            "description": "Deleted"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/vehicles/{id}/history": {
      "parameters": [
        {
          "$ref": "#/components/parameters/Id"
        }
      ],
      "get": {
        "tags": [
          "Vehicles"
        ],
        "operationId": "vehicleHistory",
        "summary": "A vehicle's trip history (oldest first)",
        "description": "Default the last 24 hours; at most 7 days per call and 10,000 positions. History older than `retentionDays` is deleted.",
        "parameters": [
          {
            "name": "from",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "to",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "taskId",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Only positions sent while this task was under way."
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 10000
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Positions",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "vehicleId",
                    "from",
                    "to",
                    "positions"
                  ],
                  "properties": {
                    "vehicleId": {
                      "type": "string"
                    },
                    "from": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "to": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "positions": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Position"
                      }
                    }
                  }
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/vehicles/{id}/events": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        }
      ],
      "get": {
        "tags": [
          "Vehicles"
        ],
        "operationId": "vehicleEvents",
        "summary": "A vehicle's timeline from its tracker (ignition on and off, power cut and back), newest first",
        "parameters": [
          {
            "name": "from",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date-time"
            },
            "description": "Default: 24 hours ago."
          },
          {
            "name": "to",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date-time"
            },
            "description": "At most 7 days after from."
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "maximum": 500,
              "default": 500
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Entries",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "vehicleId": {
                      "type": "string"
                    },
                    "events": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "at": {
                            "type": "string",
                            "format": "date-time"
                          },
                          "kind": {
                            "type": "string",
                            "enum": [
                              "ignition_on",
                              "ignition_off",
                              "power_cut",
                              "power_back"
                            ]
                          },
                          "lat": {
                            "type": "number"
                          },
                          "lng": {
                            "type": "number"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/vehicles/{id}/links": {
      "parameters": [
        {
          "$ref": "#/components/parameters/Id"
        }
      ],
      "get": {
        "tags": [
          "Rider links"
        ],
        "operationId": "listRiderLinks",
        "summary": "The vehicle's rider links (never their addresses), newest first",
        "responses": {
          "200": {
            "description": "Links",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "links"
                  ],
                  "properties": {
                    "links": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/RiderLink"
                      }
                    }
                  }
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      },
      "post": {
        "tags": [
          "Rider links"
        ],
        "operationId": "createRiderLink",
        "summary": "A link a rider opens on their phone to share its position for this vehicle (no app needed)",
        "description": "The answer's `url` (https://nways.ng/d/…) is shown once: send it to the rider. The page asks the rider to agree,\nthen shares the phone's position while it's open and sharing is on, shows their open deliveries and lets them\nmark one picked up or delivered. Positions count like `POST /locations` for this vehicle.\n",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "properties": {
                  "days": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 90,
                    "default": 30,
                    "description": "How long the link works."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The link with its `url`",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RiderLink"
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/drivers/{id}/invites": {
      "parameters": [
        {
          "$ref": "#/components/parameters/Id"
        }
      ],
      "get": {
        "tags": [
          "Rider app"
        ],
        "operationId": "listRiderInvites",
        "summary": "The rider's invitations to N-Ways Business (never their codes), newest first",
        "responses": {
          "200": {
            "description": "Invitations",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "invites"
                  ],
                  "properties": {
                    "invites": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/RiderInvite"
                      }
                    }
                  }
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      },
      "post": {
        "tags": [
          "Rider app"
        ],
        "operationId": "createRiderInvite",
        "summary": "Invite the rider to N-Ways Business",
        "description": "The answer's `code` (8 characters, shown as XXXX-XXXX) and `url` (https://nways.ng/r/…) are shown once: send them\nto the rider. On a phone with N-Ways Business the link opens the app; otherwise the rider installs it, signs in\nwith their N-Ways account and types the code. Joining binds the rider's account to this driver: the app then\nshares the phone's position in the background while the rider is on duty (for the vehicle whose `driverId` is\nthis driver), shows that vehicle's tasks and marks them picked up, delivered or failed with photo or signature\nproof. A new invitation replaces the rider's earlier ones; each works once.\n",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "properties": {
                  "days": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 30,
                    "default": 7,
                    "description": "How long the code works."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The invitation with its `code` and `url`",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RiderInvite"
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/invites/{inviteId}": {
      "parameters": [
        {
          "name": "inviteId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "pattern": "^inv_[0-9]+$"
          }
        }
      ],
      "delete": {
        "tags": [
          "Rider app"
        ],
        "operationId": "revokeRiderInvite",
        "summary": "Withdraw an invitation (its code stops working)",
        "responses": {
          "204": {
            "description": "Withdrawn"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/drivers/{id}/app": {
      "parameters": [
        {
          "$ref": "#/components/parameters/Id"
        }
      ],
      "delete": {
        "tags": [
          "Rider app"
        ],
        "operationId": "unlinkRiderApp",
        "summary": "Take the rider's N-Ways account off this driver",
        "description": "The app stops sharing the rider's position and showing their jobs at once. Invite them again to bring them back.",
        "responses": {
          "204": {
            "description": "Unlinked"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/tasks/{id}/proofs": {
      "parameters": [
        {
          "$ref": "#/components/parameters/Id"
        }
      ],
      "get": {
        "tags": [
          "Tasks"
        ],
        "operationId": "listTaskProofs",
        "summary": "The task's proof of delivery (photos, signatures) and the rider's note",
        "responses": {
          "200": {
            "description": "Proofs",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "taskId",
                    "proofs",
                    "endNote"
                  ],
                  "properties": {
                    "taskId": {
                      "$ref": "#/components/schemas/Id"
                    },
                    "proofs": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Proof"
                      }
                    },
                    "endNote": {
                      "type": "string",
                      "description": "What the rider wrote when finishing."
                    }
                  }
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/proofs/{proofId}": {
      "parameters": [
        {
          "name": "proofId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "pattern": "^prf_[0-9]+$"
          }
        }
      ],
      "get": {
        "tags": [
          "Tasks"
        ],
        "operationId": "getProof",
        "summary": "A proof's image (JPEG, PNG or WebP), kept as long as its task",
        "responses": {
          "200": {
            "description": "The image",
            "content": {
              "image/jpeg": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              },
              "image/png": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              },
              "image/webp": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/links/{linkId}": {
      "parameters": [
        {
          "name": "linkId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "pattern": "^lnk_[0-9]+$"
          }
        }
      ],
      "delete": {
        "tags": [
          "Rider links"
        ],
        "operationId": "revokeRiderLink",
        "summary": "Stop a rider link at once (its page then says it has ended)",
        "responses": {
          "204": {
            "description": "Revoked"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/locations": {
      "post": {
        "tags": [
          "Locations"
        ],
        "operationId": "postLocations",
        "summary": "Send positions (batched)",
        "description": "Rider apps send their fixes in batches, for one vehicle or many (1–1,000 per call). Fixes up to\n24 hours old are kept as history (an app back online sends what it buffered); the newest moves the\nvehicle's last known position. A fresh fix (10 minutes or newer) re-times the vehicle's open task\nwith our router and live traffic (at most every 10 s per task), and marks it `arriving` when the\ndrop-off is within its `arrivingS` or 250 m. Fixes that can't be used are listed in `rejected`\nby index; the rest are accepted. Send every 5–15 s while on a task.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "updates"
                ],
                "properties": {
                  "updates": {
                    "type": "array",
                    "minItems": 1,
                    "maxItems": 1000,
                    "items": {
                      "$ref": "#/components/schemas/LocationUpdate"
                    }
                  }
                }
              },
              "example": {
                "updates": [
                  {
                    "vehicleId": "bike-17",
                    "lat": 6.512,
                    "lng": 3.375,
                    "t": "2026-10-15T09:12:03Z",
                    "speedMps": 6.2,
                    "heading": 210,
                    "accuracyM": 8
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Accepted",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "accepted",
                    "rejected",
                    "tasks"
                  ],
                  "properties": {
                    "accepted": {
                      "type": "integer"
                    },
                    "rejected": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "required": [
                          "index",
                          "reason"
                        ],
                        "properties": {
                          "index": {
                            "type": "integer"
                          },
                          "reason": {
                            "type": "string",
                            "enum": [
                              "VEHICLE_NOT_FOUND",
                              "INVALID_POSITION",
                              "INVALID_TIME",
                              "TIME_OUT_OF_RANGE",
                              "INVALID_VALUE"
                            ]
                          }
                        }
                      }
                    },
                    "tasks": {
                      "type": "array",
                      "description": "The open tasks of the vehicles in the batch, re-timed.",
                      "items": {
                        "type": "object",
                        "required": [
                          "id",
                          "status",
                          "eta"
                        ],
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "status": {
                            "$ref": "#/components/schemas/TaskStatus"
                          },
                          "eta": {
                            "oneOf": [
                              {
                                "$ref": "#/components/schemas/ETA"
                              },
                              {
                                "type": "null"
                              }
                            ]
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/tasks": {
      "get": {
        "tags": [
          "Tasks"
        ],
        "operationId": "listTasks",
        "summary": "List tasks, newest first",
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "created",
                "assigned",
                "picked_up",
                "arriving",
                "completed",
                "failed",
                "cancelled",
                "active"
              ]
            },
            "description": "`active` = assigned, picked_up or arriving."
          },
          {
            "name": "vehicleId",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 500,
              "default": 100
            }
          },
          {
            "name": "pageToken",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "`nextPageToken` from the previous page."
          }
        ],
        "responses": {
          "200": {
            "description": "Tasks",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "tasks"
                  ],
                  "properties": {
                    "tasks": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Task"
                      }
                    },
                    "nextPageToken": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      },
      "post": {
        "tags": [
          "Tasks"
        ],
        "operationId": "createTask",
        "summary": "Create a delivery",
        "description": "A task has an optional pickup and a drop-off. Give `vehicleId` to assign it at once. The answer has the\nrecipient's `trackingUrl` (valid 72 hours, or until an hour after the task ends). Events:\n`task.created` (and `task.assigned`).\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "dropoff"
                ],
                "properties": {
                  "id": {
                    "$ref": "#/components/schemas/Id",
                    "description": "Your id for it; N-Ways makes one (job_…) when you leave it out."
                  },
                  "pickup": {
                    "$ref": "#/components/schemas/Stop"
                  },
                  "pickupPlaceId": {
                    "type": "string",
                    "example": "plc_4fQ9xLm2Za",
                    "description": "One of your pickup places as the pickup (its name, address and position are copied in), instead of `pickup`."
                  },
                  "dropoff": {
                    "$ref": "#/components/schemas/Stop"
                  },
                  "recipientName": {
                    "type": "string",
                    "maxLength": 100,
                    "description": "For your records; never shown on the tracking page."
                  },
                  "recipientPhone": {
                    "type": "string",
                    "maxLength": 32,
                    "pattern": "^[+0-9 ()-]*$",
                    "description": "So the rider can call the customer from N-Ways Business; never shown on the tracking page."
                  },
                  "notes": {
                    "type": "string",
                    "maxLength": 500
                  },
                  "lang": {
                    "type": "string",
                    "enum": [
                      "en",
                      "fr",
                      "pt"
                    ],
                    "default": "en",
                    "description": "The tracking page's language."
                  },
                  "arrivingS": {
                    "type": "integer",
                    "minimum": 30,
                    "maximum": 1800,
                    "default": 180,
                    "description": "Seconds before arrival at which the task becomes `arriving`."
                  },
                  "vehicleId": {
                    "type": "string"
                  },
                  "dueAt": {
                    "type": "string",
                    "format": "date-time",
                    "description": "The deadline: past it (or expected after it) the task is `late`."
                  },
                  "area": {
                    "$ref": "#/components/schemas/Area"
                  },
                  "recipientEmail": {
                    "type": "string",
                    "format": "email",
                    "description": "N-Ways emails the customer its news (on its way, arriving, delivered) with the tracking link."
                  },
                  "proofRequired": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "enum": [
                        "photo",
                        "pin",
                        "signature"
                      ]
                    },
                    "description": "What the driver must give to deliver; leave out for your settings' proofRequired."
                  },
                  "packageSize": {
                    "type": "string",
                    "enum": [
                      "small",
                      "medium",
                      "large"
                    ]
                  },
                  "itemDescription": {
                    "type": "string",
                    "maxLength": 200
                  },
                  "reference": {
                    "type": "string",
                    "maxLength": 64,
                    "description": "Your own reference (an invoice or order number)."
                  },
                  "scheduledAt": {
                    "type": "string",
                    "format": "date-time",
                    "description": "A pickup later (up to 60 days ahead): the driver is reminded before it."
                  }
                }
              },
              "example": {
                "id": "order-88412",
                "pickup": {
                  "lat": 6.5095,
                  "lng": 3.3711,
                  "name": "Mama Put",
                  "Yaba": null,
                  "address": "12 Herbert Macaulay Way"
                },
                "dropoff": {
                  "lat": 6.4969,
                  "lng": 3.3539,
                  "name": "Home",
                  "address": "4 Adeniran Ogunsanya St"
                },
                "lang": "en",
                "vehicleId": "bike-17"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Task"
                }
              }
            }
          },
          "403": {
            "description": "DELIVERIES_OFF (N-Ways turned new deliveries off for your organisation) or ORGANISATION_SUSPENDED; the message says why.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/tasks/{id}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/Id"
        }
      ],
      "get": {
        "tags": [
          "Tasks"
        ],
        "operationId": "getTask",
        "summary": "Get a task with its live ETA",
        "responses": {
          "200": {
            "description": "Task",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Task"
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      },
      "patch": {
        "tags": [
          "Tasks"
        ],
        "operationId": "updateTask",
        "summary": "Edit a delivery",
        "description": "Changes any of the fields below, checked as creating a task checks them; leave out what stays, send `null`\nto clear an optional field. Answers the task, with the changed fields in `editedFields` and the\n`X-NWays-Changed` header (nothing changed: no event).\n\n**When.** Everything can change until the delivery is picked up (`editable: all`): it's `created`, or\n`assigned` with no `pickedUpAt` (and, for an N-Ways driver, before they start it: their request may be\nwaiting or accepted). Then only `notes` and `dueAt` until it ends (`editable: limited`); other fields\nanswer 409 `TASK_UNDER_WAY`, an ended task 409 `TASK_ENDED`. Who has it changes with\n`POST /tasks/{id}/assign` or `/dispatch`, never here.\n\n**Afterwards.** The ETA is worked out again and the tracking link stays the same. Event\n`task.updated` (`data.task`, `data.changed`). A request still waiting for an N-Ways driver stays the\nsame request with the new details (and at least a minute left to answer); the driver is told when the\npickup, drop-off or times changed. A driver who accepted it (N-Ways or N-Ways Business) is told what\nchanged. A customer N-Ways already emailed gets an \"updated\" email when the drop-off or a time changed, at\nmost one every 10 minutes.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "minProperties": 1,
                "properties": {
                  "pickup": {
                    "oneOf": [
                      {
                        "$ref": "#/components/schemas/Stop"
                      },
                      {
                        "type": "null"
                      }
                    ],
                    "description": "null removes it (not for a delivery sent to an N-Ways driver)."
                  },
                  "pickupPlaceId": {
                    "type": "string",
                    "description": "One of your pickup places as the new pickup, instead of `pickup`."
                  },
                  "dropoff": {
                    "$ref": "#/components/schemas/Stop"
                  },
                  "recipientName": {
                    "type": "string",
                    "maxLength": 100
                  },
                  "recipientPhone": {
                    "type": "string",
                    "maxLength": 32,
                    "pattern": "^[+0-9 ()-]*$"
                  },
                  "recipientEmail": {
                    "type": "string",
                    "description": "An email address, or \"\" for none."
                  },
                  "notes": {
                    "type": "string",
                    "maxLength": 500
                  },
                  "itemDescription": {
                    "type": "string",
                    "maxLength": 200
                  },
                  "packageSize": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "enum": [
                      "small",
                      "medium",
                      "large",
                      null
                    ]
                  },
                  "reference": {
                    "type": "string",
                    "maxLength": 64
                  },
                  "dueAt": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "format": "date-time"
                  },
                  "scheduledAt": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "format": "date-time",
                    "description": "Now to 60 days ahead."
                  },
                  "proofRequired": {
                    "type": [
                      "array",
                      "null"
                    ],
                    "items": {
                      "type": "string",
                      "enum": [
                        "photo",
                        "pin",
                        "signature"
                      ]
                    },
                    "description": "null: your settings' proofRequired."
                  },
                  "area": {
                    "oneOf": [
                      {
                        "$ref": "#/components/schemas/Area"
                      },
                      {
                        "type": "null"
                      }
                    ]
                  },
                  "lang": {
                    "type": "string",
                    "enum": [
                      "en",
                      "fr",
                      "pt"
                    ]
                  },
                  "arrivingS": {
                    "type": "integer",
                    "minimum": 30,
                    "maximum": 1800
                  }
                }
              },
              "example": {
                "dropoff": {
                  "lat": 6.4549,
                  "lng": 3.4246,
                  "name": "Office",
                  "address": "2 Kingsway Rd",
                  "Ikoyi": null
                },
                "dueAt": "2026-10-17T15:00:00Z",
                "notes": "Call on arrival"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The task as it is now",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Task"
                }
              }
            }
          },
          "409": {
            "description": "TASK_UNDER_WAY (only notes and dueAt can change now) or TASK_ENDED.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/tasks/{id}/assign": {
      "parameters": [
        {
          "$ref": "#/components/parameters/Id"
        }
      ],
      "post": {
        "tags": [
          "Tasks"
        ],
        "operationId": "assignTask",
        "summary": "Assign (or reassign) a vehicle — created|assigned → assigned; event task.assigned",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "vehicleId"
                ],
                "properties": {
                  "vehicleId": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Task",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Task"
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/tasks/{id}/pickup": {
      "parameters": [
        {
          "$ref": "#/components/parameters/Id"
        }
      ],
      "post": {
        "tags": [
          "Tasks"
        ],
        "operationId": "pickupTask",
        "summary": "Mark picked up — assigned → picked_up",
        "responses": {
          "200": {
            "description": "Task",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Task"
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/tasks/{id}/complete": {
      "parameters": [
        {
          "$ref": "#/components/parameters/Id"
        }
      ],
      "post": {
        "tags": [
          "Tasks"
        ],
        "operationId": "completeTask",
        "summary": "Mark delivered — assigned|picked_up|arriving → completed; event task.completed",
        "responses": {
          "200": {
            "description": "Task",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Task"
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/tasks/{id}/fail": {
      "parameters": [
        {
          "$ref": "#/components/parameters/Id"
        }
      ],
      "post": {
        "tags": [
          "Tasks"
        ],
        "operationId": "failTask",
        "summary": "Mark failed — any open status → failed; event task.failed",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "properties": {
                  "reason": {
                    "type": "string",
                    "maxLength": 200
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Task",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Task"
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/tasks/{id}/cancel": {
      "parameters": [
        {
          "$ref": "#/components/parameters/Id"
        }
      ],
      "post": {
        "tags": [
          "Tasks"
        ],
        "operationId": "cancelTask",
        "summary": "Cancel — any open status → cancelled",
        "responses": {
          "200": {
            "description": "Task",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Task"
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/tasks/{id}/share": {
      "parameters": [
        {
          "$ref": "#/components/parameters/Id"
        }
      ],
      "post": {
        "tags": [
          "Tracking"
        ],
        "operationId": "shareTask",
        "summary": "The recipient's tracking link (a new one if it expired; none once the task ended)",
        "responses": {
          "200": {
            "description": "Link",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "url",
                    "expiresAt"
                  ],
                  "properties": {
                    "url": {
                      "type": "string",
                      "format": "uri",
                      "example": "https://nways.ng/t/3fT9kQ2xVbN7LmP0aZc4RwYe"
                    },
                    "expiresAt": {
                      "type": "string",
                      "format": "date-time"
                    }
                  }
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/tasks/{id}/pin-email": {
      "parameters": [
        {
          "$ref": "#/components/parameters/Id"
        }
      ],
      "post": {
        "tags": [
          "Tasks"
        ],
        "operationId": "emailTaskPin",
        "summary": "Send the customer's delivery PIN to their email",
        "description": "For a delivery that asks for a PIN, until it ends: keeps the email on the delivery and sends the PIN to it (the fix when `pinDelivery.needsEmail` is true). You never see the PIN.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "email"
                ],
                "additionalProperties": false,
                "properties": {
                  "email": {
                    "type": "string",
                    "format": "email"
                  }
                }
              },
              "example": {
                "email": "ngozi@example.com"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The delivery",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Task"
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/whatsapp/number": {
      "get": {
        "tags": [
          "Tasks"
        ],
        "operationId": "whatsappNumber",
        "summary": "Whether a delivery PIN can go to a number on WhatsApp",
        "description": "WhatsApp can't say ahead whether a number is on WhatsApp: `known` is a number your customer got a PIN on before, `unreachable` one WhatsApp couldn't reach lately (a delivery asking for a PIN then needs `recipientEmail`).",
        "parameters": [
          {
            "name": "phone",
            "in": "query",
            "schema": {
              "type": "string",
              "maxLength": 32
            },
            "description": "As typed: +234…, or the local way for your country."
          }
        ],
        "responses": {
          "200": {
            "description": "The number's reach",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "enabled",
                    "phone",
                    "valid",
                    "known",
                    "unreachable"
                  ],
                  "properties": {
                    "enabled": {
                      "type": "boolean",
                      "description": "PINs go on WhatsApp. When false, they go by email and push."
                    },
                    "phone": {
                      "type": "string",
                      "description": "The number in E.164 (\"\" when it isn't one).",
                      "example": "+2348031234567"
                    },
                    "valid": {
                      "type": "boolean"
                    },
                    "known": {
                      "type": "boolean"
                    },
                    "unreachable": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/trackers": {
      "get": {
        "tags": [
          "GPS trackers"
        ],
        "operationId": "listTrackers",
        "summary": "List your trackers, with counts by state",
        "parameters": [
          {
            "name": "state",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "awaiting_first_fix",
                "live",
                "silent"
              ]
            }
          },
          {
            "name": "model",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "A catalogue model id."
          },
          {
            "name": "q",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Finds a name, IMEI, vehicle or SIM number."
          }
        ],
        "responses": {
          "200": {
            "description": "Trackers",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "trackers",
                    "counts"
                  ],
                  "properties": {
                    "trackers": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Tracker"
                      }
                    },
                    "counts": {
                      "type": "object",
                      "additionalProperties": {
                        "type": "integer"
                      }
                    },
                    "models": {
                      "type": "object",
                      "additionalProperties": {
                        "type": "integer"
                      }
                    }
                  }
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      },
      "post": {
        "tags": [
          "GPS trackers"
        ],
        "operationId": "createTracker",
        "summary": "Add a tracker by its IMEI (first claim wins)",
        "description": "Errors: INVALID_IMEI (not 15 digits, or the Luhn check digit is wrong), INVALID_DEVICE_ID (a phone's id), TRACKER_EXISTS (already yours), TRACKER_CLAIMED_ELSEWHERE (another organisation holds it), VEHICLE_NOT_FOUND.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TrackerInput"
              },
              "example": {
                "imei": "356307042441013",
                "vehicleId": "truck-12",
                "model": "concox-gt06n",
                "network": "mtn",
                "simNumber": "+2348030000000"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Added",
            "with its setup steps": null,
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Tracker"
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/trackers/catalog": {
      "get": {
        "tags": [
          "GPS trackers"
        ],
        "operationId": "trackerCatalog",
        "summary": "The device catalogue, your country's SIM networks and their APNs, and where devices report",
        "description": "models: every model (with status supported | coming, its methods and family), then forwarded and not-sure. brands, families, protocols, counts: as GET https://nways.ng/v1/trackers/catalogue. networks: your country's; unverified: an APN from a public listing not yet checked on a real SIM.",
        "responses": {
          "200": {
            "description": "Catalogue",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "models": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/TrackerModel"
                      }
                    },
                    "brands": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    },
                    "families": {
                      "type": "object",
                      "additionalProperties": {
                        "type": "object"
                      }
                    },
                    "protocols": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "name": {
                            "type": "string"
                          },
                          "status": {
                            "type": "string",
                            "enum": [
                              "supported",
                              "coming"
                            ]
                          },
                          "port": {
                            "type": "integer"
                          }
                        }
                      }
                    },
                    "networks": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "name": {
                            "type": "string"
                          },
                          "apn": {
                            "type": "string"
                          },
                          "user": {
                            "type": "string"
                          },
                          "pass": {
                            "type": "string"
                          },
                          "unverified": {
                            "type": "boolean"
                          }
                        }
                      }
                    },
                    "country": {
                      "type": "string"
                    },
                    "host": {
                      "type": "string",
                      "example": "trk.nways.ng"
                    },
                    "ports": {
                      "type": "object",
                      "additionalProperties": {
                        "type": "integer"
                      }
                    },
                    "osmandUrl": {
                      "type": "string"
                    },
                    "platforms": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "commandKinds": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    }
                  }
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/trackers/import": {
      "post": {
        "tags": [
          "GPS trackers"
        ],
        "operationId": "importTrackers",
        "summary": "Add many trackers from a CSV (check it first with dryRun)",
        "description": "Columns by header, any order, commas or semicolons: imei, vehicle (one of yours by id, name or plate, or a new one by that name), model (a catalogue id or \"Brand Model\"; empty: not sure), sim, network, and optionally apn, name, plate, type. At most 1,000 rows. Each row says what's wrong (issues: imei_missing, imei_invalid, imei_check_digit, imei_duplicate, imei_yours, imei_elsewhere, model_unknown, network_unknown, vehicle_missing, mode_unknown); with dryRun false the good rows are added and the rest left out.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "csv"
                ],
                "properties": {
                  "csv": {
                    "type": "string"
                  },
                  "dryRun": {
                    "type": "boolean"
                  }
                }
              },
              "example": {
                "csv": "imei,vehicle,model,sim,network\n356307042441013,Hiace 2,Concox GT06N,08030000000,MTN\n",
                "dryRun": true
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Each row",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "rows": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "row": {
                            "type": "integer"
                          },
                          "imei": {
                            "type": "string"
                          },
                          "model": {
                            "type": "string"
                          },
                          "network": {
                            "type": "string"
                          },
                          "simNumber": {
                            "type": "string"
                          },
                          "vehicle": {
                            "type": "string"
                          },
                          "vehicleId": {
                            "type": "string"
                          },
                          "newVehicle": {
                            "type": "boolean"
                          },
                          "ok": {
                            "type": "boolean"
                          },
                          "trackerId": {
                            "type": "string"
                          },
                          "issues": {
                            "type": "array",
                            "items": {
                              "type": "object",
                              "properties": {
                                "field": {
                                  "type": "string"
                                },
                                "code": {
                                  "type": "string"
                                },
                                "message": {
                                  "type": "string"
                                }
                              }
                            }
                          }
                        }
                      }
                    },
                    "total": {
                      "type": "integer"
                    },
                    "ok": {
                      "type": "integer"
                    },
                    "bad": {
                      "type": "integer"
                    },
                    "added": {
                      "type": "integer"
                    },
                    "dryRun": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/trackers/setup.csv": {
      "get": {
        "tags": [
          "GPS trackers"
        ],
        "operationId": "trackerSetupSheet",
        "summary": "The installers' sheet, every tracker's setup SMS in order (CSV)",
        "parameters": [
          {
            "name": "ids",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Tracker ids, comma-separated. Default: the trackers not on the map yet."
          },
          {
            "name": "lang",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "en",
                "fr",
                "pt"
              ],
              "default": "en"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "One line per SMS: IMEI, tracker, vehicle, model, SIM number, step, what to do, SMS, answer.",
            "content": {
              "text/csv": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/trackers/forwarding": {
      "get": {
        "tags": [
          "GPS trackers"
        ],
        "operationId": "getTrackerForwarding",
        "summary": "Forwarding from your tracking platform, its addresses, and the units it sends",
        "responses": {
          "200": {
            "description": "Forwarding",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TrackerForwarding"
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      },
      "post": {
        "tags": [
          "GPS trackers"
        ],
        "operationId": "rotateTrackerForwarding",
        "summary": "Turn forwarding on, or give it a new token (the old one stops at once)",
        "responses": {
          "200": {
            "description": "Forwarding",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TrackerForwarding"
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      },
      "delete": {
        "tags": [
          "GPS trackers"
        ],
        "operationId": "deleteTrackerForwarding",
        "summary": "Turn forwarding off (the units stay listed)",
        "responses": {
          "200": {
            "description": "Forwarding",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TrackerForwarding"
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/trackers/forwarding/units/{unitId}/add": {
      "parameters": [
        {
          "name": "unitId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        }
      ],
      "post": {
        "tags": [
          "GPS trackers"
        ],
        "operationId": "addForwardedUnit",
        "summary": "Add a forwarded unit as one of your trackers (a new vehicle named after it, unless you give one)",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TrackerInput"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Added",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Tracker"
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/trackers/forwarding/units/{unitId}": {
      "parameters": [
        {
          "name": "unitId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        }
      ],
      "delete": {
        "tags": [
          "GPS trackers"
        ],
        "operationId": "forgetForwardedUnit",
        "summary": "Forget a forwarded unit (it comes back if your platform sends it again)",
        "responses": {
          "200": {
            "description": "Forgotten"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/tracker-transfers": {
      "get": {
        "tags": [
          "GPS trackers"
        ],
        "operationId": "listTrackerTransfers",
        "summary": "Your requests to move trackers here, and trackers that moved away",
        "responses": {
          "200": {
            "description": "Transfers",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "transfers": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/TrackerTransfer"
                      }
                    },
                    "windowS": {
                      "type": "integer",
                      "example": 180
                    }
                  }
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      },
      "post": {
        "tags": [
          "GPS trackers"
        ],
        "operationId": "requestTrackerTransfer",
        "summary": "Ask for a tracker another organisation holds (owners and admins)",
        "description": "Errors: TRACKER_NOT_CLAIMED (nobody holds it: add it), TRACKER_EXISTS (yours already), TRANSFER_NOT_PROVABLE (a phone or forwarded unit: its organisation removes it).",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "imei"
                ],
                "properties": {
                  "imei": {
                    "type": "string"
                  },
                  "vehicleId": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Asked",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TrackerTransfer"
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/tracker-transfers/{transferId}/start": {
      "parameters": [
        {
          "name": "transferId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "example": "trf_4fQ9xLm2Za"
          }
        }
      ],
      "post": {
        "tags": [
          "GPS trackers"
        ],
        "operationId": "startTrackerTransfer",
        "summary": "You're making the device log in afresh now (its restart SMS); a fresh login within 3 minutes moves it",
        "responses": {
          "200": {
            "description": "Waiting",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TrackerTransfer"
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/tracker-transfers/{transferId}": {
      "parameters": [
        {
          "name": "transferId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        }
      ],
      "delete": {
        "tags": [
          "GPS trackers"
        ],
        "operationId": "cancelTrackerTransfer",
        "summary": "Withdraw a request",
        "responses": {
          "200": {
            "description": "Withdrawn"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/trackers/{trackerId}": {
      "parameters": [
        {
          "name": "trackerId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "example": "trk_4fQ9xLm2Za"
          },
          "description": "Its id, or its IMEI."
        }
      ],
      "get": {
        "tags": [
          "GPS trackers"
        ],
        "operationId": "getTracker",
        "summary": "Get a tracker, its setup steps and what's wrong",
        "responses": {
          "200": {
            "description": "Tracker",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Tracker"
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      },
      "patch": {
        "tags": [
          "GPS trackers"
        ],
        "operationId": "updateTracker",
        "summary": "Edit a tracker (its vehicle, model, SIM)",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TrackerInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Tracker",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Tracker"
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      },
      "delete": {
        "tags": [
          "GPS trackers"
        ],
        "operationId": "deleteTracker",
        "summary": "Remove a tracker (it can then be added by anyone)",
        "responses": {
          "200": {
            "description": "Removed"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/trackers/{trackerId}/log": {
      "parameters": [
        {
          "name": "trackerId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        }
      ],
      "get": {
        "tags": [
          "GPS trackers"
        ],
        "operationId": "trackerLog",
        "summary": "The device log (connections, raw frames, decoded fields, answers sent, errors), oldest first",
        "description": "The last 500 entries and 7 days are kept. Without `after`, the last `limit` entries.",
        "parameters": [
          {
            "name": "after",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "An entry id: only later entries."
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "maximum": 500,
              "default": 200
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Entries",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "entries": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/TrackerLogEntry"
                      }
                    }
                  }
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/trackers/{trackerId}/stream": {
      "parameters": [
        {
          "name": "trackerId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        }
      ],
      "get": {
        "tags": [
          "GPS trackers"
        ],
        "operationId": "trackerStream",
        "summary": "The device log and state as they change (server-sent events)",
        "description": "`event: state` (the Tracker) and `event: log` (a TrackerLogEntry), starting with the state and the last 50 entries (or those after `?after=`). A comment every 15 s keeps it open.",
        "responses": {
          "200": {
            "description": "An event stream",
            "content": {
              "text/event-stream": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/trackers/{trackerId}/virtual": {
      "parameters": [
        {
          "name": "trackerId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        }
      ],
      "post": {
        "tags": [
          "GPS trackers"
        ],
        "operationId": "startVirtualTracker",
        "summary": "Drive a virtual tracker for this one (its own protocol, through N-Ways' gateway)",
        "description": "A short road in your country, a position every 3 s, answers checked byte for byte. Faults to try: noAckWait, badCrc, invalidFix, clockSkewH (8: a tracker on China time), backlog. Errors: VIRTUAL_RUNNING, TRACKER_NO_VEHICLE, TRACKER_CONNECTED (the real one is connected), VIRTUAL_TRACKER_UNAVAILABLE.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "properties": {
                  "minutes": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 10,
                    "default": 3
                  },
                  "sos": {
                    "type": "boolean"
                  },
                  "powerCut": {
                    "type": "boolean"
                  },
                  "noAckWait": {
                    "type": "boolean"
                  },
                  "badCrc": {
                    "type": "boolean"
                  },
                  "invalidFix": {
                    "type": "boolean"
                  },
                  "clockSkewH": {
                    "type": "number"
                  },
                  "backlog": {
                    "type": "integer",
                    "maximum": 50
                  },
                  "lang": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Started",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/VirtualTracker"
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      },
      "delete": {
        "tags": [
          "GPS trackers"
        ],
        "operationId": "stopVirtualTracker",
        "summary": "Stop its virtual tracker",
        "responses": {
          "200": {
            "description": "Stopped"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/trackers/{trackerId}/commands": {
      "parameters": [
        {
          "name": "trackerId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        }
      ],
      "get": {
        "tags": [
          "GPS trackers"
        ],
        "operationId": "listTrackerCommands",
        "summary": "Commands sent to a tracker (the last 30), and which it takes",
        "responses": {
          "200": {
            "description": "Commands",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "commands": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/TrackerCommand"
                      }
                    },
                    "kinds": {
                      "type": "object",
                      "additionalProperties": {
                        "type": "string",
                        "enum": [
                          "gprs",
                          "sms"
                        ]
                      },
                      "description": "Each command it takes: over its connection (gprs) or by SMS only."
                    },
                    "connected": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      },
      "post": {
        "tags": [
          "GPS trackers"
        ],
        "operationId": "sendTrackerCommand",
        "summary": "Send a command (owners and admins); its answer comes in the device log",
        "description": "Connected and its protocol takes it over the connection: sent, then answered. Otherwise: offline or sms, with the SMS to send it (sms, smsTo). Errors: COMMAND_NOT_SUPPORTED, COMMANDS_NOT_SUPPORTED (a phone or forwarded unit).",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "kind"
                ],
                "properties": {
                  "kind": {
                    "type": "string",
                    "enum": [
                      "locate",
                      "restart",
                      "interval",
                      "heartbeat"
                    ]
                  },
                  "seconds": {
                    "type": "integer",
                    "description": "interval: 5–3600; heartbeat: 60–3600 (whole minutes)."
                  }
                }
              },
              "example": {
                "kind": "interval",
                "seconds": 30
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Sent (or the SMS to send)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TrackerCommand"
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/places": {
      "get": {
        "tags": [
          "Pickup places"
        ],
        "operationId": "listPlaces",
        "summary": "List your pickup places (the default first)",
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Finds a name, address or door code."
          }
        ],
        "responses": {
          "200": {
            "description": "Places",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "places"
                  ],
                  "properties": {
                    "places": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Place"
                      }
                    }
                  }
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      },
      "post": {
        "tags": [
          "Pickup places"
        ],
        "operationId": "createPlace",
        "summary": "Add a pickup place (up to 200)",
        "description": "Your first place is the default unless you say otherwise; making one the default unsets the others.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PlaceInput"
              },
              "example": {
                "name": "Yaba warehouse",
                "doorCode": "LA14A18BE13",
                "address": "12 Herbert Macaulay Way",
                "lat": 6.5095,
                "lng": 3.3711,
                "phone": "+234 803 000 0000",
                "hours": "Mon–Sat 8:00–18:00"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Place"
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/places/{placeId}": {
      "parameters": [
        {
          "name": "placeId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "example": "plc_4fQ9xLm2Za"
          }
        }
      ],
      "get": {
        "tags": [
          "Pickup places"
        ],
        "operationId": "getPlace",
        "summary": "Get a pickup place",
        "responses": {
          "200": {
            "description": "Place",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Place"
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      },
      "patch": {
        "tags": [
          "Pickup places"
        ],
        "operationId": "updatePlace",
        "summary": "Edit a pickup place",
        "description": "Deliveries already made from it keep the pickup they were given.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PlaceInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Place",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Place"
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      },
      "delete": {
        "tags": [
          "Pickup places"
        ],
        "operationId": "deletePlace",
        "summary": "Delete a pickup place",
        "responses": {
          "204": {
            "description": "Deleted"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/customers": {
      "get": {
        "tags": [
          "Customers"
        ],
        "operationId": "listCustomers",
        "summary": "List your customers, most recent delivery first",
        "description": "A customer is everyone your deliveries went to with the same phone (however it was written), else the same\nemail, else the same name. Kept from your deliveries as they're made and edited, a year after their last\ndelivery.\n",
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Finds a name, an email, or any part of a phone number."
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 25
            }
          },
          {
            "name": "pageToken",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "`nextPageToken` (or `prevPageToken`) from another page."
          }
        ],
        "responses": {
          "200": {
            "description": "Customers",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "customers",
                    "offset"
                  ],
                  "properties": {
                    "customers": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Customer"
                      }
                    },
                    "nextPageToken": {
                      "type": "string"
                    },
                    "prevPageToken": {
                      "type": "string"
                    },
                    "offset": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/customers/{customerId}": {
      "parameters": [
        {
          "name": "customerId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "example": "cus_4fQ9xLm2Za"
          }
        }
      ],
      "get": {
        "tags": [
          "Customers"
        ],
        "operationId": "getCustomer",
        "summary": "Get a customer",
        "responses": {
          "200": {
            "description": "Customer",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Customer"
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      },
      "delete": {
        "tags": [
          "Customers"
        ],
        "operationId": "deleteCustomer",
        "summary": "Forget a customer and their addresses",
        "description": "For a customer who asks to be forgotten. Their deliveries stay, with your fleet's retention; a new delivery to them starts a new customer.",
        "responses": {
          "204": {
            "description": "Forgotten"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/customers/{customerId}/addresses": {
      "parameters": [
        {
          "name": "customerId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        }
      ],
      "get": {
        "tags": [
          "Customers"
        ],
        "operationId": "listCustomerAddresses",
        "summary": "List a customer's addresses, last used first",
        "description": "Each distinct drop-off (to about 11 m) with its latest name, address and door code, how often it was used and when last.",
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Finds a name, address or door code."
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 25
            }
          },
          {
            "name": "pageToken",
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Addresses",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "addresses",
                    "total",
                    "offset"
                  ],
                  "properties": {
                    "addresses": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Address"
                      }
                    },
                    "total": {
                      "type": "integer",
                      "description": "All the customer's addresses (not only those found)."
                    },
                    "nextPageToken": {
                      "type": "string"
                    },
                    "prevPageToken": {
                      "type": "string"
                    },
                    "offset": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/zones": {
      "get": {
        "tags": [
          "Zones"
        ],
        "operationId": "listZones",
        "summary": "The project's zones (circles), each with how many vehicles are in it now",
        "responses": {
          "200": {
            "description": "Zones",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "zones"
                  ],
                  "properties": {
                    "zones": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Zone"
                      }
                    }
                  }
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      },
      "post": {
        "tags": [
          "Zones"
        ],
        "operationId": "createZone",
        "summary": "Add a zone (at most 200 a project)",
        "description": "Vehicles entering and leaving it send `zone.entered` and `zone.exited`, from each vehicle's newest position.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "lat",
                  "lng",
                  "radiusM"
                ],
                "properties": {
                  "id": {
                    "$ref": "#/components/schemas/Id"
                  },
                  "name": {
                    "type": "string",
                    "maxLength": 100
                  },
                  "lat": {
                    "type": "number"
                  },
                  "lng": {
                    "type": "number"
                  },
                  "radiusM": {
                    "type": "integer",
                    "minimum": 50,
                    "maximum": 50000
                  }
                }
              },
              "example": {
                "id": "depot-yaba",
                "name": "Yaba depot",
                "lat": 6.5095,
                "lng": 3.3711,
                "radiusM": 300
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Zone"
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/zones/{id}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/Id"
        }
      ],
      "get": {
        "tags": [
          "Zones"
        ],
        "operationId": "getZone",
        "summary": "Get a zone",
        "responses": {
          "200": {
            "description": "Zone",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Zone"
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      },
      "patch": {
        "tags": [
          "Zones"
        ],
        "operationId": "updateZone",
        "summary": "Rename, move or resize a zone (lat and lng together)",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "properties": {
                  "name": {
                    "type": "string",
                    "maxLength": 100
                  },
                  "lat": {
                    "type": "number"
                  },
                  "lng": {
                    "type": "number"
                  },
                  "radiusM": {
                    "type": "integer",
                    "minimum": 50,
                    "maximum": 50000
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Zone",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Zone"
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      },
      "delete": {
        "tags": [
          "Zones"
        ],
        "operationId": "deleteZone",
        "summary": "Delete a zone",
        "responses": {
          "204": {
            "description": "Deleted"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/tasks/{id}/area": {
      "parameters": [
        {
          "$ref": "#/components/parameters/Id"
        }
      ],
      "post": {
        "tags": [
          "Tasks"
        ],
        "operationId": "setTaskArea",
        "summary": "Set or clear a delivery's own area",
        "description": "Leaving it raises `asset.geofence_exit` (through your geofence_exit rules with area `dispatch`, or a default one).",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "area"
                ],
                "properties": {
                  "area": {
                    "oneOf": [
                      {
                        "$ref": "#/components/schemas/Area"
                      },
                      {
                        "type": "null"
                      }
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The task",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Task"
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/alert-rules": {
      "get": {
        "tags": [
          "Alerts"
        ],
        "operationId": "listAlertRules",
        "summary": "List alert rules",
        "responses": {
          "200": {
            "description": "Rules",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "rules"
                  ],
                  "properties": {
                    "rules": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/AlertRule"
                      }
                    }
                  }
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      },
      "post": {
        "tags": [
          "Alerts"
        ],
        "operationId": "createAlertRule",
        "summary": "Add an alert rule (up to 100)",
        "description": "Evaluated on our servers whether or not anyone has the console open: geofences on each position you send,\nsilent and idle every 30 seconds. One open alert per rule, asset and subject; one that cleared isn't opened\nagain for 5 minutes; reminders every `realertS` until acknowledged.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AlertRule"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AlertRule"
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/alert-rules/{ruleId}": {
      "parameters": [
        {
          "name": "ruleId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "example": "rule_4fQ9xLm2Za"
          }
        }
      ],
      "get": {
        "tags": [
          "Alerts"
        ],
        "operationId": "getAlertRule",
        "summary": "Get a rule",
        "responses": {
          "200": {
            "description": "The rule",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AlertRule"
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      },
      "patch": {
        "tags": [
          "Alerts"
        ],
        "operationId": "updateAlertRule",
        "summary": "Change a rule (the kind stays); active false pauses it and resolves its open alerts",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AlertRule"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The rule",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AlertRule"
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      },
      "delete": {
        "tags": [
          "Alerts"
        ],
        "operationId": "deleteAlertRule",
        "summary": "Delete a rule (its open alerts are resolved)",
        "responses": {
          "204": {
            "description": "Deleted"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/alert-events": {
      "get": {
        "tags": [
          "Alerts"
        ],
        "operationId": "listAlerts",
        "summary": "Alerts raised, newest first",
        "parameters": [
          {
            "name": "state",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "open",
                "acknowledged",
                "resolved",
                "active"
              ]
            },
            "description": "`active` = open or acknowledged."
          },
          {
            "name": "vehicleId",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "kind",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "geofence_exit",
                "geofence_enter",
                "silent",
                "idle",
                "crash",
                "tracker_alarm"
              ]
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 500,
              "default": 100
            }
          },
          {
            "name": "pageToken",
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Alerts",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "alerts"
                  ],
                  "properties": {
                    "alerts": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/AlertEvent"
                      }
                    },
                    "nextPageToken": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/alert-events/{alertId}": {
      "parameters": [
        {
          "name": "alertId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "example": "alr_8KwP2nVd5T"
          }
        }
      ],
      "get": {
        "tags": [
          "Alerts"
        ],
        "operationId": "getAlert",
        "summary": "Get an alert",
        "responses": {
          "200": {
            "description": "The alert",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AlertEvent"
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/alert-events/{alertId}/acknowledge": {
      "parameters": [
        {
          "name": "alertId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        }
      ],
      "post": {
        "tags": [
          "Alerts"
        ],
        "operationId": "acknowledgeAlert",
        "summary": "Acknowledge an open alert (no more reminders; it stays until it clears or is resolved)",
        "responses": {
          "200": {
            "description": "The alert",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AlertEvent"
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/alert-events/{alertId}/resolve": {
      "parameters": [
        {
          "name": "alertId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        }
      ],
      "post": {
        "tags": [
          "Alerts"
        ],
        "operationId": "resolveAlert",
        "summary": "Resolve an alert (sends asset.alert_resolved)",
        "responses": {
          "200": {
            "description": "The alert",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AlertEvent"
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/events/export": {
      "get": {
        "tags": [
          "Events"
        ],
        "operationId": "exportEvents",
        "summary": "Download every event (CSV for Excel or Google Sheets, or JSON)",
        "description": "The same payloads webhooks get, kept as long as your history (retentionDays). At most 100,000 per download.",
        "parameters": [
          {
            "name": "format",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "csv",
                "json"
              ],
              "default": "csv"
            }
          },
          {
            "name": "from",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "RFC 3339, or YYYY-MM-DD (Lagos day)."
          },
          {
            "name": "to",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "type",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "A prefix: asset. or task."
          }
        ],
        "responses": {
          "200": {
            "description": "The events",
            "content": {
              "text/csv": {
                "schema": {
                  "type": "string"
                }
              },
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Event"
                  }
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/webhooks": {
      "get": {
        "tags": [
          "Webhooks"
        ],
        "operationId": "listWebhooks",
        "summary": "List webhook endpoints",
        "responses": {
          "200": {
            "description": "Endpoints and the events there are",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "webhooks",
                    "events"
                  ],
                  "properties": {
                    "webhooks": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Webhook"
                      }
                    },
                    "events": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/EventType"
                      }
                    }
                  }
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      },
      "post": {
        "tags": [
          "Webhooks"
        ],
        "operationId": "createWebhook",
        "summary": "Add an endpoint (up to 10); the answer has its signing secret, shown once",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "url"
                ],
                "properties": {
                  "url": {
                    "type": "string",
                    "format": "uri",
                    "description": "https, a public address; redirects are not followed."
                  },
                  "events": {
                    "type": "array",
                    "items": {
                      "$ref": "#/components/schemas/EventType"
                    },
                    "description": "Empty = every event."
                  },
                  "headers": {
                    "type": "object",
                    "additionalProperties": {
                      "type": "string",
                      "maxLength": 1000
                    },
                    "maxProperties": 10,
                    "description": "Sent with every delivery (an auth token your CRM asks for). Not X-NWays-*, Host, Content-Type, User-Agent."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Webhook"
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/webhooks/{id}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/WebhookId"
        }
      ],
      "patch": {
        "tags": [
          "Webhooks"
        ],
        "operationId": "updateWebhook",
        "summary": "Change an endpoint's url, events or active",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "properties": {
                  "url": {
                    "type": "string",
                    "format": "uri"
                  },
                  "events": {
                    "type": "array",
                    "items": {
                      "$ref": "#/components/schemas/EventType"
                    }
                  },
                  "active": {
                    "type": "boolean",
                    "description": "Off: pending deliveries fail and no new ones are queued."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Endpoint",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Webhook"
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      },
      "delete": {
        "tags": [
          "Webhooks"
        ],
        "operationId": "deleteWebhook",
        "summary": "Delete an endpoint and its delivery log",
        "responses": {
          "204": {
            "description": "Deleted"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/webhooks/{id}/rotate-secret": {
      "parameters": [
        {
          "$ref": "#/components/parameters/WebhookId"
        }
      ],
      "post": {
        "tags": [
          "Webhooks"
        ],
        "operationId": "rotateWebhookSecret",
        "summary": "A new signing secret (shown once); the old one stops at once",
        "responses": {
          "200": {
            "description": "Endpoint with `secret`",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Webhook"
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/webhooks/{id}/test": {
      "parameters": [
        {
          "$ref": "#/components/parameters/WebhookId"
        }
      ],
      "post": {
        "tags": [
          "Webhooks"
        ],
        "operationId": "testWebhook",
        "summary": "Queue a `ping` event to the endpoint",
        "responses": {
          "202": {
            "description": "Queued",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "deliveryId"
                  ],
                  "properties": {
                    "deliveryId": {
                      "type": "string",
                      "example": "dlv_Qa71ZcR0mX"
                    }
                  }
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/webhooks/{id}/deliveries": {
      "parameters": [
        {
          "$ref": "#/components/parameters/WebhookId"
        }
      ],
      "get": {
        "tags": [
          "Webhooks"
        ],
        "operationId": "listDeliveries",
        "summary": "The endpoint's delivery log, newest first (kept 30 days)",
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "pending",
                "delivered",
                "failed"
              ]
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 50
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Deliveries",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "deliveries"
                  ],
                  "properties": {
                    "deliveries": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Delivery"
                      }
                    }
                  }
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/webhooks/{id}/deliveries/{deliveryId}/retry": {
      "parameters": [
        {
          "$ref": "#/components/parameters/WebhookId"
        },
        {
          "name": "deliveryId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "pattern": "^dlv_[0-9]+$"
          }
        }
      ],
      "post": {
        "tags": [
          "Webhooks"
        ],
        "operationId": "retryDelivery",
        "summary": "Send a delivery again now (failed, or delivered and wanted again); its attempts are kept",
        "responses": {
          "202": {
            "description": "Queued",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "id",
                    "status"
                  ],
                  "properties": {
                    "id": {
                      "type": "string"
                    },
                    "status": {
                      "const": "pending"
                    }
                  }
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/traffic-feed": {
      "get": {
        "tags": [
          "Traffic feed"
        ],
        "operationId": "trafficFeed",
        "summary": "Road speeds in a box (product traffic_feed; enterprise)",
        "description": "Aggregated speeds per road and direction from N-Ways' self-learning traffic, only where enough\ndistinct devices contributed: live readings (last 30 minutes) need 2, typical speeds (by day type\nand 15-minute slot) need 3. Never a trace, a device count or a session. Geometry is the OSM way\n(GeoJSON `[lng, lat]`); `direction` is along (`forward`) or against (`backward`) its drawing.\n`level`: 0 free, 1 slow, 2 heavy, 3 standstill, against the road's free-flow speed.\n",
        "parameters": [
          {
            "name": "bbox",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "example": "3.30,6.42,3.45,6.55"
            },
            "description": "minLng,minLat,maxLng,maxLat; at most 2° across."
          },
          {
            "name": "kind",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "live",
                "typical"
              ],
              "default": "live"
            }
          },
          {
            "name": "at",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date-time"
            },
            "description": "kind=typical: the time of week (local day type and 15-minute slot); default now."
          },
          {
            "name": "mode",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "car",
                "motorcycle"
              ],
              "default": "car"
            }
          },
          {
            "name": "format",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "geojson",
                "ndjson"
              ],
              "default": "geojson"
            },
            "description": "ndjson: a FeedInfo line, then one Feature per line (application/x-ndjson)."
          }
        ],
        "responses": {
          "200": {
            "description": "Feed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TrafficFeed"
                }
              },
              "application/x-ndjson": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/offline/regions": {
      "get": {
        "tags": [
          "Offline packs"
        ],
        "operationId": "listRegions",
        "summary": "Regions offline packs are made for",
        "responses": {
          "200": {
            "description": "Regions",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "regions",
                    "maxzoom",
                    "custom"
                  ],
                  "properties": {
                    "regions": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Region"
                      }
                    },
                    "maxzoom": {
                      "type": "integer",
                      "example": 15
                    },
                    "custom": {
                      "type": "object",
                      "properties": {
                        "maxDegrees": {
                          "type": "number",
                          "example": 0.6
                        },
                        "step": {
                          "type": "number",
                          "example": 0.05
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/offline/packs/{region}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/Region"
        },
        {
          "$ref": "#/components/parameters/PackBBox"
        },
        {
          "$ref": "#/components/parameters/MaxZoom"
        }
      ],
      "get": {
        "tags": [
          "Offline packs"
        ],
        "operationId": "getPack",
        "summary": "A region's pack manifest (202 while it's built; ask again after Retry-After)",
        "description": "A pack is the region's map (`map.pmtiles`, an extract of the archive the Maps SDK draws; open it with\nMapLibre Native's pmtiles:// support) and its routing graph tiles (`routing.tar`, Valhalla level 0–2\ntiles with the engine's own paths) for on-device routing. Built once per map and graph version;\n`version` changes when either does.\n",
        "responses": {
          "200": {
            "description": "Ready",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PackManifest"
                }
              }
            }
          },
          "202": {
            "description": "Being built",
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PackManifest"
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/offline/packs/{region}/{file}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/Region"
        },
        {
          "name": "file",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "enum": [
              "map.pmtiles",
              "routing.tar"
            ]
          }
        },
        {
          "$ref": "#/components/parameters/PackBBox"
        },
        {
          "$ref": "#/components/parameters/MaxZoom"
        }
      ],
      "get": {
        "tags": [
          "Offline packs"
        ],
        "operationId": "downloadPackFile",
        "summary": "Download a pack file (Range requests supported; ETag = its hash)",
        "responses": {
          "200": {
            "description": "The file",
            "content": {
              "application/vnd.pmtiles": {},
              "application/x-tar": {}
            }
          },
          "206": {
            "description": "Part of the file"
          },
          "409": {
            "$ref": "#/components/responses/Error"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    }
  },
  "webhooks": {
    "fleetEvent": {
      "post": {
        "summary": "An event, POSTed to your endpoint",
        "description": "Headers: `X-NWays-Event` (the type), `X-NWays-Delivery` (dlv_…, the same on retries),\n`X-NWays-Signature: t=<unix seconds>,v1=<hex>` where v1 = HMAC-SHA256(secret, \"<t>.<raw body>\").\nCheck it with your endpoint's secret and refuse timestamps more than 5 minutes off. Answer any 2xx\nwithin 10 s; anything else is retried after 30 s, 2 min, 10 min, 30 min, 1 h, 3 h, 6 h and 12 h,\nthen marked failed. Events may arrive out of order or more than once: use the event `id` and\nthe task's status. Events: task.created, task.assigned, task.arriving, task.completed, task.failed\n(data.task, a Task), vehicle.offline (data.vehicle, a Vehicle; once per silence longer than\n`offlineAfterS`), and zone.entered / zone.exited (data.zone {id, name}, data.vehicle {id, name},\ndata.at, data.position {lat, lng}).\n",
        "parameters": [
          {
            "name": "X-NWays-Signature",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-NWays-Event",
            "in": "header",
            "required": true,
            "schema": {
              "$ref": "#/components/schemas/EventType"
            }
          },
          {
            "name": "X-NWays-Delivery",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Event"
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Received"
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "apiKeyHeader": {
        "type": "apiKey",
        "in": "header",
        "name": "X-NWays-Api-Key"
      },
      "apiKeyQuery": {
        "type": "apiKey",
        "in": "query",
        "name": "key"
      }
    },
    "parameters": {
      "Id": {
        "name": "id",
        "in": "path",
        "required": true,
        "schema": {
          "$ref": "#/components/schemas/Id"
        }
      },
      "WebhookId": {
        "name": "id",
        "in": "path",
        "required": true,
        "schema": {
          "type": "string",
          "pattern": "^wh_[0-9]+$"
        }
      },
      "Region": {
        "name": "region",
        "in": "path",
        "required": true,
        "schema": {
          "type": "string",
          "example": "lagos"
        },
        "description": "A region id, or `custom` with `bbox`."
      },
      "PackBBox": {
        "name": "bbox",
        "in": "query",
        "schema": {
          "type": "string"
        },
        "description": "region=custom: minLng,minLat,maxLng,maxLat, snapped outwards to 0.05°, at most 0.6° across."
      },
      "MaxZoom": {
        "name": "maxzoom",
        "in": "query",
        "schema": {
          "type": "integer",
          "minimum": 8,
          "maximum": 15,
          "default": 15
        }
      }
    },
    "responses": {
      "Error": {
        "description": "Error",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      }
    },
    "schemas": {
      "Id": {
        "type": "string",
        "pattern": "^[A-Za-z0-9_.:\\-]{1,64}$"
      },
      "Error": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "object",
            "required": [
              "code",
              "status",
              "reason",
              "message",
              "details"
            ],
            "properties": {
              "code": {
                "type": "integer",
                "example": 404
              },
              "status": {
                "type": "string",
                "example": "NOT_FOUND"
              },
              "reason": {
                "type": "string",
                "example": "TASK_NOT_FOUND",
                "description": "Platform, key and quota reasons (PLATFORM_*, API_KEY_*, ORGANISATION_*, PROJECT_SUSPENDED, BILLING_DISABLED,\nRATE_LIMIT_EXCEEDED, QUOTA_EXCEEDED, TEMPORARILY_BLOCKED, BACKEND_ERROR) or fleet reasons: INVALID_ARGUMENT, REQUEST_TOO_LARGE, NOT_FOUND,\nDRIVER_NOT_FOUND, VEHICLE_NOT_FOUND, TASK_NOT_FOUND, WEBHOOK_NOT_FOUND, REGION_NOT_FOUND, FILE_NOT_FOUND,\nDRIVER_EXISTS, VEHICLE_EXISTS, TASK_EXISTS, TASK_STATUS_CONFLICT, TASK_ENDED, TOO_MANY_WEBHOOKS,\nZONE_NOT_FOUND, ZONE_EXISTS, TOO_MANY_ZONES, LINK_NOT_FOUND, DELIVERY_NOT_FOUND,\nPACK_BUILDING, PACK_BUILD_FAILED, PACKS_UNAVAILABLE; features N-Ways turned off for the\norganisation: DELIVERIES_OFF, CONTRACTORS_OFF, SMART_PRICE_OFF, WEBHOOKS_OFF, ERP_OFF.\n"
              },
              "message": {
                "type": "string"
              },
              "details": {
                "type": "array",
                "description": "Google's ErrorInfo (reason, domain nways.ng, metadata with `docs` and the facts) and Help (a link to the reason on https://nways.ng/developers/errors).",
                "items": {
                  "type": "object",
                  "required": [
                    "@type"
                  ],
                  "properties": {
                    "@type": {
                      "type": "string"
                    }
                  },
                  "additionalProperties": true
                }
              }
            }
          }
        }
      },
      "Settings": {
        "type": "object",
        "required": [
          "shareTraffic",
          "retentionDays",
          "brandName",
          "offlineAfterS",
          "stopAlertS"
        ],
        "properties": {
          "shareTraffic": {
            "type": "boolean",
            "default": false,
            "description": "Let your vehicles' positions also feed N-Ways' public live traffic (anonymously). Off unless you turn it on."
          },
          "retentionDays": {
            "type": "integer",
            "minimum": 1,
            "maximum": 90,
            "default": 30,
            "description": "How long trip history and ended tasks are kept."
          },
          "brandName": {
            "type": "string",
            "maxLength": 60,
            "description": "The name tracking pages show (\"Your delivery from …\")."
          },
          "offlineAfterS": {
            "type": "integer",
            "minimum": 60,
            "maximum": 86400,
            "default": 600,
            "description": "Silence after which a vehicle is offline (vehicle.offline)."
          },
          "stopAlertS": {
            "type": "integer",
            "minimum": 60,
            "maximum": 7200,
            "default": 600,
            "description": "A vehicle on a delivery stopped this long is a `stopped` alert."
          }
        }
      },
      "SettingsUpdate": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "shareTraffic": {
            "type": "boolean"
          },
          "retentionDays": {
            "type": "integer",
            "minimum": 1,
            "maximum": 90
          },
          "brandName": {
            "type": "string",
            "maxLength": 60
          },
          "offlineAfterS": {
            "type": "integer",
            "minimum": 60,
            "maximum": 86400
          },
          "stopAlertS": {
            "type": "integer",
            "minimum": 60,
            "maximum": 7200
          }
        }
      },
      "Driver": {
        "type": "object",
        "required": [
          "id",
          "name",
          "firstName",
          "lastName",
          "phone",
          "createdAt",
          "updatedAt",
          "app"
        ],
        "properties": {
          "id": {
            "$ref": "#/components/schemas/Id"
          },
          "name": {
            "type": "string",
            "description": "firstName and lastName together."
          },
          "firstName": {
            "type": "string"
          },
          "lastName": {
            "type": "string",
            "description": "Empty for a driver created with only a one-word name."
          },
          "phone": {
            "type": "string"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          },
          "app": {
            "type": "object",
            "description": "The rider in N-Ways Business.",
            "required": [
              "status",
              "joinedAt",
              "onDuty",
              "consentedAt"
            ],
            "properties": {
              "status": {
                "type": "string",
                "enum": [
                  "none",
                  "invited",
                  "joined"
                ]
              },
              "joinedAt": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "date-time"
              },
              "email": {
                "type": "string",
                "description": "The rider's N-Ways account (joined only)."
              },
              "name": {
                "type": "string"
              },
              "onDuty": {
                "type": "boolean",
                "description": "Sharing their position now."
              },
              "consentedAt": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "date-time",
                "description": "When they agreed to share their position (null: not now)."
              },
              "inviteExpiresAt": {
                "type": "string",
                "format": "date-time",
                "description": "Invited: until when the code works."
              }
            }
          }
        }
      },
      "RiderInvite": {
        "type": "object",
        "required": [
          "id",
          "driverId",
          "createdBy",
          "createdAt",
          "expiresAt",
          "usedAt",
          "revokedAt",
          "active"
        ],
        "properties": {
          "id": {
            "type": "string",
            "pattern": "^inv_[0-9]+$"
          },
          "driverId": {
            "$ref": "#/components/schemas/Id"
          },
          "code": {
            "type": "string",
            "pattern": "^[0-9A-HJKMNP-TV-Z]{4}-[0-9A-HJKMNP-TV-Z]{4}$",
            "description": "Only when made."
          },
          "url": {
            "type": "string",
            "format": "uri",
            "description": "Only when made: https://nways.ng/r/<code>."
          },
          "createdBy": {
            "type": "string"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "expiresAt": {
            "type": "string",
            "format": "date-time"
          },
          "usedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "revokedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "active": {
            "type": "boolean"
          }
        }
      },
      "Proof": {
        "type": "object",
        "required": [
          "id",
          "kind",
          "contentType",
          "bytes",
          "lat",
          "lng",
          "by",
          "createdAt"
        ],
        "properties": {
          "id": {
            "type": "string",
            "pattern": "^prf_[0-9]+$"
          },
          "kind": {
            "type": "string",
            "enum": [
              "photo",
              "signature"
            ]
          },
          "contentType": {
            "type": "string",
            "enum": [
              "image/jpeg",
              "image/png",
              "image/webp"
            ]
          },
          "bytes": {
            "type": "integer"
          },
          "lat": {
            "type": [
              "number",
              "null"
            ]
          },
          "lng": {
            "type": [
              "number",
              "null"
            ]
          },
          "by": {
            "type": "string",
            "description": "The rider's name."
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "VehicleMode": {
        "type": "string",
        "enum": [
          "motorcycle",
          "okada",
          "motorbike",
          "keke",
          "car",
          "van",
          "truck",
          "bicycle",
          "walk",
          "boat",
          "asset"
        ],
        "default": "motorcycle",
        "description": "How ETAs are routed. motorcycle = a dispatch bike (motorcycle roads and timing; the commercial okada bans,\nwhich are for passenger okadas, don't apply); okada = a passenger okada fleet (kept out of okada ban areas);\nmotorbike = a private motorbike; keke = a tricycle (two-wheeler roads, no okada bans); car, van, truck = car routing;\nbicycle; walk = a courier on foot; boat = on the water (ETA in a straight line at its speed); asset = anything\nelse you track (a trailer, a generator): never routed.\n"
      },
      "Position": {
        "type": "object",
        "required": [
          "lat",
          "lng",
          "at"
        ],
        "properties": {
          "lat": {
            "type": "number"
          },
          "lng": {
            "type": "number"
          },
          "heading": {
            "type": "number",
            "minimum": 0,
            "maximum": 360
          },
          "speedMps": {
            "type": "number"
          },
          "accuracyM": {
            "type": "number"
          },
          "at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "VehicleColour": {
        "type": "string",
        "enum": [
          "blue",
          "orange",
          "teal",
          "purple",
          "pink",
          "green",
          "brown",
          "navy",
          "magenta",
          "olive",
          "cyan",
          "maroon",
          "sky",
          "lime",
          "plum",
          "sand"
        ],
        "description": "The vehicle's colour on the console's and N-Ways Business's maps. A new vehicle takes the one its project uses least."
      },
      "VehicleRoute": {
        "type": "object",
        "required": [
          "vehicleId",
          "colour",
          "done",
          "ahead",
          "routed",
          "stops",
          "updatedAt"
        ],
        "properties": {
          "vehicleId": {
            "type": "string"
          },
          "colour": {
            "$ref": "#/components/schemas/VehicleColour"
          },
          "done": {
            "type": "array",
            "items": {
              "type": "array",
              "items": {
                "type": "number"
              },
              "minItems": 2,
              "maxItems": 2
            }
          },
          "ahead": {
            "type": "array",
            "items": {
              "type": "array",
              "items": {
                "type": "number"
              },
              "minItems": 2,
              "maxItems": 2
            }
          },
          "routed": {
            "type": "boolean",
            "description": "ahead follows the roads (false: straight lines between the stops for now)."
          },
          "stops": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "n",
                "kind",
                "taskId",
                "taskRef",
                "label",
                "lat",
                "lng",
                "status",
                "etaAt",
                "dueAt",
                "done",
                "final"
              ],
              "properties": {
                "n": {
                  "type": "integer",
                  "description": "The stop's number, from 1."
                },
                "kind": {
                  "type": "string",
                  "enum": [
                    "pickup",
                    "dropoff"
                  ]
                },
                "taskId": {
                  "type": "string"
                },
                "taskRef": {
                  "type": "string",
                  "description": "The delivery's number (#1042)."
                },
                "label": {
                  "type": "string",
                  "description": "Who or where it is for: the recipient, else the drop-off's name."
                },
                "name": {
                  "type": "string",
                  "description": "The stop's name or address."
                },
                "lat": {
                  "type": "number"
                },
                "lng": {
                  "type": "number"
                },
                "status": {
                  "$ref": "#/components/schemas/TaskStatus"
                },
                "etaAt": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "format": "date-time"
                },
                "dueAt": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "format": "date-time"
                },
                "done": {
                  "type": "boolean",
                  "description": "Reached (picked up, or delivered)."
                },
                "final": {
                  "type": "boolean",
                  "description": "The last stop not yet reached."
                }
              }
            }
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "Vehicle": {
        "type": "object",
        "required": [
          "id",
          "name",
          "mode",
          "plate",
          "driverId",
          "lastLocation",
          "online",
          "createdAt",
          "updatedAt"
        ],
        "properties": {
          "id": {
            "$ref": "#/components/schemas/Id"
          },
          "name": {
            "type": "string"
          },
          "mode": {
            "$ref": "#/components/schemas/VehicleMode"
          },
          "plate": {
            "type": "string"
          },
          "driverId": {
            "type": [
              "string",
              "null"
            ]
          },
          "group": {
            "type": "string"
          },
          "batteryPct": {
            "type": "number",
            "description": "The tracker's or phone's battery at its last report, when it sends one."
          },
          "colour": {
            "$ref": "#/components/schemas/VehicleColour"
          },
          "lastLocation": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/Position"
              },
              {
                "type": "null"
              }
            ]
          },
          "online": {
            "type": "boolean",
            "description": "Heard from within offlineAfterS."
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "LocationUpdate": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "vehicleId",
          "lat",
          "lng"
        ],
        "properties": {
          "vehicleId": {
            "type": "string"
          },
          "lat": {
            "type": "number",
            "minimum": -90,
            "maximum": 90
          },
          "lng": {
            "type": "number",
            "minimum": -180,
            "maximum": 180
          },
          "t": {
            "type": "string",
            "format": "date-time",
            "description": "When the fix was taken (default now); up to 24 h old, 2 min ahead."
          },
          "speedMps": {
            "type": "number",
            "minimum": 0,
            "maximum": 100
          },
          "heading": {
            "type": "number",
            "minimum": 0,
            "maximum": 360
          },
          "accuracyM": {
            "type": "number",
            "minimum": 0,
            "maximum": 10000
          },
          "batteryPct": {
            "type": "number",
            "minimum": 0,
            "maximum": 100,
            "description": "Battery level, shown in the console."
          },
          "ignition": {
            "type": "boolean",
            "description": "A tracker's ignition (ACC)."
          },
          "extVoltageV": {
            "type": "number",
            "minimum": 0,
            "maximum": 100,
            "description": "A tracker's external power, volts (0: cut)."
          },
          "odometerM": {
            "type": "number",
            "minimum": 0,
            "description": "A tracker's odometer, metres."
          },
          "sats": {
            "type": "integer",
            "minimum": 0,
            "maximum": 99,
            "description": "Satellites in view."
          },
          "source": {
            "type": "string",
            "enum": [
              "app",
              "tracker"
            ],
            "description": "Where the fix comes from (default app). A tracker's positions never feed live traffic."
          }
        }
      },
      "Tracker": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "example": "trk_4fQ9xLm2Za"
          },
          "imei": {
            "type": "string",
            "example": "356307042441013"
          },
          "name": {
            "type": "string"
          },
          "vehicleId": {
            "type": [
              "string",
              "null"
            ]
          },
          "vehicleName": {
            "type": "string"
          },
          "protocol": {
            "type": "string",
            "example": "gt06",
            "description": "A protocol N-Ways' gateway reads (gt06, teltonika, h02, gps103, tk103, gl200, meitrack, ruptela, jt808, suntech, galileo, startek, topin, xexun, gosafe, tzone, wialon, watch, osmand), forward for a unit forwarded from your tracking platform, or \"\" until its model or first connection says."
          },
          "model": {
            "type": "string"
          },
          "simNumber": {
            "type": "string"
          },
          "network": {
            "type": "string"
          },
          "apn": {
            "type": "string"
          },
          "state": {
            "type": "string",
            "enum": [
              "awaiting_first_fix",
              "live",
              "silent"
            ]
          },
          "connected": {
            "type": "boolean"
          },
          "heard": {
            "type": "boolean",
            "description": "N-Ways' gateway has heard this device (even before it was added)."
          },
          "lastSeenAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "lastFixAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Its last GPS fix."
          },
          "onMapAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Its last fix that moved its vehicle on the map."
          },
          "position": {
            "oneOf": [
              {
                "type": "object",
                "properties": {
                  "lat": {
                    "type": "number"
                  },
                  "lng": {
                    "type": "number"
                  },
                  "at": {
                    "type": "string",
                    "format": "date-time"
                  }
                }
              },
              {
                "type": "null"
              }
            ]
          },
          "batteryPct": {
            "type": [
              "number",
              "null"
            ]
          },
          "batteryV": {
            "type": [
              "number",
              "null"
            ]
          },
          "extVoltageV": {
            "type": [
              "number",
              "null"
            ]
          },
          "ignition": {
            "type": [
              "boolean",
              "null"
            ]
          },
          "sats": {
            "type": [
              "integer",
              "null"
            ]
          },
          "odometerM": {
            "type": [
              "number",
              "null"
            ]
          },
          "claimedAt": {
            "type": "string",
            "format": "date-time"
          },
          "setup": {
            "type": "object",
            "description": "The single tracker's: the SMS steps in order (key, sms, reply, optional), or a phone app's settings (value).",
            "properties": {
              "protocol": {
                "type": "string"
              },
              "host": {
                "type": "string"
              },
              "port": {
                "type": "integer"
              },
              "url": {
                "type": "string"
              },
              "smsTo": {
                "type": "string"
              },
              "assumed": {
                "type": "boolean"
              },
              "steps": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "n": {
                      "type": "integer"
                    },
                    "key": {
                      "type": "string"
                    },
                    "sms": {
                      "type": "string"
                    },
                    "value": {
                      "type": "string"
                    },
                    "reply": {
                      "type": "string"
                    },
                    "optional": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "diagnosis": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "no_vehicle",
                "never_heard",
                "wrong_protocol",
                "clock_skew",
                "refused_positions",
                "no_gps_fix",
                "bad_crc",
                "unknown_frames",
                "drops"
              ]
            },
            "description": "What the gateway saw that needs fixing, most useful first."
          },
          "virtual": {
            "$ref": "#/components/schemas/VirtualTracker"
          }
        }
      },
      "TrackerModel": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "example": "concox-gt06n"
          },
          "brand": {
            "type": "string",
            "example": "Concox"
          },
          "brandId": {
            "type": "string"
          },
          "name": {
            "type": "string",
            "example": "GT06N"
          },
          "protocol": {
            "type": "string",
            "example": "gt06"
          },
          "family": {
            "type": "string",
            "description": "Its command set (setup SMS, configurator, commands)."
          },
          "kind": {
            "type": "string",
            "enum": [
              "vehicle",
              "asset",
              "personal",
              "obd",
              "phone"
            ]
          },
          "password": {
            "type": "string",
            "description": "The SMS password it ships with."
          },
          "popular": {
            "type": "boolean"
          },
          "status": {
            "type": "string",
            "enum": [
              "supported",
              "coming"
            ]
          },
          "methods": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "sms",
                "configurator",
                "platform",
                "app"
              ]
            }
          }
        }
      },
      "TrackerForwarding": {
        "type": "object",
        "properties": {
          "enabled": {
            "type": "boolean"
          },
          "token": {
            "type": [
              "string",
              "null"
            ],
            "example": "fwd_8KwP2nVd5T…",
            "description": "The key in your addresses: keep it secret."
          },
          "urls": {
            "type": [
              "object",
              "null"
            ],
            "properties": {
              "traccar": {
                "type": "string"
              },
              "osmand": {
                "type": "string"
              },
              "flespi": {
                "type": "string"
              },
              "webhook": {
                "type": "string"
              },
              "bearer": {
                "type": "string"
              }
            }
          },
          "wialon": {
            "type": "object",
            "properties": {
              "host": {
                "type": "string"
              },
              "port": {
                "type": "integer"
              },
              "password": {
                "type": "string"
              },
              "status": {
                "type": "string",
                "enum": [
                  "supported",
                  "coming"
                ]
              }
            }
          },
          "units": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "unitId": {
                  "type": "string"
                },
                "source": {
                  "type": "string",
                  "enum": [
                    "traccar",
                    "osmand",
                    "flespi",
                    "webhook",
                    "wialon"
                  ]
                },
                "name": {
                  "type": "string"
                },
                "firstSeen": {
                  "type": "string",
                  "format": "date-time"
                },
                "lastSeen": {
                  "type": "string",
                  "format": "date-time"
                },
                "lastFixAt": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "format": "date-time"
                },
                "position": {
                  "type": [
                    "object",
                    "null"
                  ]
                },
                "received": {
                  "type": "integer"
                },
                "trackerId": {
                  "type": "string",
                  "description": "Set once it's one of your trackers."
                },
                "trackerName": {
                  "type": "string"
                },
                "elsewhere": {
                  "type": "boolean",
                  "description": "Another organisation holds this IMEI."
                },
                "lastPayload": {
                  "type": "object",
                  "description": "What the platform sent last (units not added yet)."
                }
              }
            }
          },
          "lastAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "lastBySource": {
            "type": "object",
            "additionalProperties": {
              "type": "string",
              "format": "date-time"
            }
          },
          "unknown": {
            "type": "integer",
            "description": "Units not added yet."
          },
          "units24h": {
            "type": "integer"
          }
        }
      },
      "TrackerCommand": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "example": "cmd_4fQ9xLm2Za"
          },
          "kind": {
            "type": "string",
            "enum": [
              "locate",
              "restart",
              "interval",
              "heartbeat"
            ]
          },
          "seconds": {
            "type": "integer"
          },
          "state": {
            "type": "string",
            "enum": [
              "sent",
              "answered",
              "offline",
              "failed",
              "sms"
            ],
            "description": "offline and sms: send the SMS instead."
          },
          "text": {
            "type": "string",
            "description": "The command over the connection."
          },
          "sms": {
            "type": "string",
            "description": "The same as an SMS."
          },
          "smsTo": {
            "type": "string"
          },
          "reply": {
            "type": "string",
            "description": "What the device answered."
          },
          "error": {
            "type": "string"
          },
          "createdBy": {
            "type": "string"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "sentAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "answeredAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          }
        }
      },
      "TrackerTransfer": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "example": "trf_4fQ9xLm2Za"
          },
          "imei": {
            "type": "string"
          },
          "direction": {
            "type": "string",
            "enum": [
              "in",
              "out"
            ]
          },
          "state": {
            "type": "string",
            "enum": [
              "pending",
              "waiting",
              "moved",
              "expired",
              "cancelled"
            ]
          },
          "vehicleId": {
            "type": [
              "string",
              "null"
            ]
          },
          "restartSms": {
            "type": "string",
            "description": "What to send the device so it logs in afresh."
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "startedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "expiresAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "movedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "trackerId": {
            "type": "string"
          }
        }
      },
      "TrackerInput": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "imei": {
            "type": "string",
            "description": "15 digits (spaces and dashes are dropped); a phone's device id for model osmand-phone, a platform's unit id for model forwarded."
          },
          "name": {
            "type": "string"
          },
          "vehicleId": {
            "type": "string",
            "description": "One of your vehicles (\"\" to unlink)."
          },
          "vehicle": {
            "type": "object",
            "description": "A new vehicle for it, instead.",
            "properties": {
              "name": {
                "type": "string"
              },
              "mode": {
                "type": "string"
              },
              "plate": {
                "type": "string"
              }
            }
          },
          "model": {
            "type": "string",
            "description": "A catalogue model id, or not-sure (learned from its first connection)."
          },
          "simNumber": {
            "type": "string"
          },
          "network": {
            "type": "string",
            "description": "A catalogue network id, or other."
          },
          "apn": {
            "type": "string",
            "description": "Default: the network's."
          }
        }
      },
      "TrackerLogEntry": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "A cursor: later entries have larger ones."
          },
          "at": {
            "type": "string",
            "format": "date-time"
          },
          "dir": {
            "type": "string",
            "enum": [
              "in",
              "out",
              "note"
            ]
          },
          "kind": {
            "type": "string",
            "description": "login, position, heartbeat, alarm, status, handshake, unknown, ack, connect, disconnect, noise, refused"
          },
          "hex": {
            "type": "string"
          },
          "ascii": {
            "type": "string"
          },
          "decoded": {
            "type": "object"
          },
          "error": {
            "type": "string"
          },
          "remote": {
            "type": "string"
          }
        }
      },
      "VirtualTracker": {
        "type": "object",
        "properties": {
          "running": {
            "type": "boolean"
          },
          "protocol": {
            "type": "string"
          },
          "startedAt": {
            "type": "string",
            "format": "date-time"
          },
          "until": {
            "type": "string",
            "format": "date-time"
          },
          "sent": {
            "type": "integer"
          },
          "answered": {
            "type": "integer"
          },
          "problems": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "error": {
            "type": "string"
          }
        }
      },
      "Stop": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "lat",
          "lng"
        ],
        "properties": {
          "lat": {
            "type": "number"
          },
          "lng": {
            "type": "number"
          },
          "name": {
            "type": "string",
            "maxLength": 120
          },
          "address": {
            "type": "string",
            "maxLength": 300
          }
        }
      },
      "TaskStatus": {
        "type": "string",
        "enum": [
          "created",
          "assigned",
          "picked_up",
          "arriving",
          "completed",
          "failed",
          "cancelled"
        ]
      },
      "ETA": {
        "type": "object",
        "required": [
          "at",
          "durationS",
          "distanceM",
          "trafficDelayS",
          "updatedAt"
        ],
        "description": "From the vehicle's last fresh position, through the pickup when not yet picked up, with live traffic and incidents.",
        "properties": {
          "at": {
            "type": "string",
            "format": "date-time",
            "description": "Arrival at the drop-off."
          },
          "durationS": {
            "type": "number"
          },
          "distanceM": {
            "type": "number"
          },
          "trafficDelayS": {
            "type": "number",
            "description": "Part of durationS due to traffic and reported incidents."
          },
          "pickupAt": {
            "type": "string",
            "format": "date-time",
            "description": "Arrival at the pickup (until picked up)."
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "Task": {
        "type": "object",
        "required": [
          "id",
          "ref",
          "status",
          "vehicleId",
          "pickup",
          "dropoff",
          "recipientName",
          "notes",
          "lang",
          "arrivingS",
          "eta",
          "dueAt",
          "late",
          "trackingUrl",
          "trackingExpiresAt",
          "createdAt",
          "assignedAt",
          "pickedUpAt",
          "arrivingAt",
          "endedAt"
        ],
        "properties": {
          "id": {
            "$ref": "#/components/schemas/Id"
          },
          "ref": {
            "type": "string",
            "description": "The delivery's short number in the project, given by N-Ways (\"1042\", shown #1042): for people, in alerts and lists."
          },
          "status": {
            "$ref": "#/components/schemas/TaskStatus"
          },
          "vehicleId": {
            "type": [
              "string",
              "null"
            ]
          },
          "pickup": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/Stop"
              },
              {
                "type": "null"
              }
            ]
          },
          "dropoff": {
            "$ref": "#/components/schemas/Stop"
          },
          "recipientName": {
            "type": "string"
          },
          "recipientPhone": {
            "type": "string"
          },
          "notes": {
            "type": "string"
          },
          "lang": {
            "type": "string",
            "enum": [
              "en",
              "fr",
              "pt"
            ]
          },
          "arrivingS": {
            "type": "integer"
          },
          "eta": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/ETA"
              },
              {
                "type": "null"
              }
            ],
            "description": "Null until a fresh position is in, and once the task ends."
          },
          "dueAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "late": {
            "type": "boolean",
            "description": "Open and past dueAt, or expected after it."
          },
          "trackingUrl": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri",
            "description": "Null once expired."
          },
          "trackingExpiresAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "failureReason": {
            "type": "string"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "assignedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "pickedUpAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "arrivingAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "endedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "area": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/Area"
              },
              {
                "type": "null"
              }
            ],
            "description": "The delivery's own area: leaving it raises asset.geofence_exit."
          },
          "proofs": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Proof"
            },
            "description": "Only in GET /tasks/{id}: the rider's photos and signatures (N-Ways Business)."
          },
          "endNote": {
            "type": "string",
            "description": "Only in GET /tasks/{id}: what the rider wrote when finishing."
          },
          "recipientEmail": {
            "type": "string"
          },
          "proofRequired": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "photo",
                "pin",
                "signature"
              ]
            },
            "description": "The delivery's own, else your settings'."
          },
          "proofRequiredOverride": {
            "type": [
              "array",
              "null"
            ],
            "items": {
              "type": "string"
            }
          },
          "packageSize": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "small",
              "medium",
              "large",
              null
            ]
          },
          "itemDescription": {
            "type": "string"
          },
          "reference": {
            "type": "string"
          },
          "scheduledAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "connectionId": {
            "type": [
              "string",
              "null"
            ],
            "description": "The N-Ways driver's connection that accepted it."
          },
          "acceptedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "startedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "editable": {
            "type": "string",
            "enum": [
              "all",
              "limited",
              "none"
            ],
            "description": "What PATCH /tasks/{id} may change now: everything, only notes and dueAt (picked up or started), or nothing (ended)."
          },
          "editedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "The last edit."
          },
          "editedFields": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "What the last edit changed."
          },
          "pinDelivery": {
            "$ref": "#/components/schemas/PinDelivery"
          }
        }
      },
      "PinDelivery": {
        "type": "object",
        "description": "How the delivery PIN is reaching the customer, when the delivery asks for one and PINs go on WhatsApp. Never the PIN itself.",
        "required": [
          "channel",
          "state",
          "whatsappErrorCode",
          "needsEmail",
          "updatedAt"
        ],
        "properties": {
          "channel": {
            "type": "string",
            "enum": [
              "whatsapp",
              "email",
              "none"
            ]
          },
          "state": {
            "type": "string",
            "enum": [
              "sending",
              "sent",
              "delivered",
              "read",
              "failed"
            ]
          },
          "whatsappErrorCode": {
            "type": [
              "integer",
              "null"
            ],
            "description": "WhatsApp's failure (131026: not on WhatsApp), kept after the PIN went by email."
          },
          "needsEmail": {
            "type": "boolean",
            "description": "The customer hasn't got the PIN: send it with POST /tasks/{id}/pin-email."
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "Place": {
        "type": "object",
        "required": [
          "id",
          "name",
          "doorCode",
          "address",
          "lat",
          "lng",
          "phone",
          "hours",
          "default",
          "createdAt",
          "updatedAt"
        ],
        "properties": {
          "id": {
            "type": "string",
            "example": "plc_4fQ9xLm2Za"
          },
          "name": {
            "type": "string",
            "example": "Yaba warehouse"
          },
          "doorCode": {
            "type": "string",
            "description": "The NIPOST or GhanaPost door code it was found by (\"\" for none)."
          },
          "address": {
            "type": "string"
          },
          "lat": {
            "type": "number"
          },
          "lng": {
            "type": "number"
          },
          "phone": {
            "type": "string"
          },
          "hours": {
            "type": "string",
            "example": "Mon–Sat 8:00–18:00"
          },
          "default": {
            "type": "boolean",
            "description": "New deliveries in the console start from it."
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "Customer": {
        "type": "object",
        "required": [
          "id",
          "name",
          "phone",
          "email",
          "deliveries",
          "addresses",
          "lastDeliveryAt",
          "lastAddress"
        ],
        "properties": {
          "id": {
            "type": "string",
            "example": "cus_4fQ9xLm2Za"
          },
          "name": {
            "type": "string",
            "description": "As the latest delivery named them."
          },
          "phone": {
            "type": "string"
          },
          "email": {
            "type": "string"
          },
          "deliveries": {
            "type": "integer"
          },
          "addresses": {
            "type": "integer"
          },
          "lastDeliveryAt": {
            "type": "string",
            "format": "date-time"
          },
          "lastAddress": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/Address"
              },
              {
                "type": "null"
              }
            ],
            "description": "Where the latest delivery went."
          }
        }
      },
      "Address": {
        "type": "object",
        "required": [
          "id",
          "name",
          "address",
          "doorCode",
          "lat",
          "lng",
          "uses",
          "lastUsedAt"
        ],
        "properties": {
          "id": {
            "type": "string",
            "example": "adr_8KwP2nVd5T"
          },
          "name": {
            "type": "string"
          },
          "address": {
            "type": "string"
          },
          "doorCode": {
            "type": "string",
            "description": "The NIPOST or GhanaPost code the address starts with (\"\" for none)."
          },
          "lat": {
            "type": "number"
          },
          "lng": {
            "type": "number"
          },
          "uses": {
            "type": "integer",
            "description": "Deliveries that went there."
          },
          "lastUsedAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "PlaceInput": {
        "type": "object",
        "additionalProperties": false,
        "description": "name, lat and lng are required to add one.",
        "properties": {
          "name": {
            "type": "string",
            "maxLength": 100
          },
          "doorCode": {
            "type": "string",
            "maxLength": 32
          },
          "address": {
            "type": "string",
            "maxLength": 300
          },
          "lat": {
            "type": "number"
          },
          "lng": {
            "type": "number"
          },
          "phone": {
            "type": "string",
            "maxLength": 32
          },
          "hours": {
            "type": "string",
            "maxLength": 200
          },
          "default": {
            "type": "boolean"
          }
        }
      },
      "Area": {
        "type": "object",
        "required": [
          "type"
        ],
        "description": "A circle (lat, lng, radiusM 50–100000) or a polygon (coordinates: 3–500 [lng, lat] points).",
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "circle",
              "polygon"
            ]
          },
          "lat": {
            "type": "number"
          },
          "lng": {
            "type": "number"
          },
          "radiusM": {
            "type": "number",
            "minimum": 50,
            "maximum": 100000
          },
          "coordinates": {
            "type": "array",
            "items": {
              "type": "array",
              "items": {
                "type": "number"
              },
              "minItems": 2,
              "maxItems": 2
            }
          }
        },
        "example": {
          "type": "circle",
          "lat": 6.48,
          "lng": 3.43,
          "radiusM": 2500
        }
      },
      "Recipients": {
        "type": "object",
        "properties": {
          "roles": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "owner",
                "admin",
                "developer",
                "dispatcher",
                "billing",
                "viewer"
              ]
            }
          },
          "userIds": {
            "type": "array",
            "items": {
              "type": "integer"
            },
            "description": "Team members (console user ids)."
          },
          "addresses": {
            "type": "array",
            "items": {
              "type": "string",
              "format": "email"
            },
            "maxItems": 20,
            "description": "Email only: any address."
          }
        }
      },
      "AlertRule": {
        "type": "object",
        "required": [
          "kind"
        ],
        "properties": {
          "id": {
            "type": "string",
            "example": "rule_4fQ9xLm2Za",
            "readOnly": true
          },
          "name": {
            "type": "string",
            "maxLength": 100
          },
          "kind": {
            "type": "string",
            "enum": [
              "geofence_exit",
              "geofence_enter",
              "silent",
              "idle",
              "crash",
              "tracker_alarm"
            ],
            "description": "tracker_alarm: a GPS tracker's alarms (pick which with alarms); without a rule, SOS, power cut, tow and crash go to owners, admins and dispatchers at once and the others once. crash: a driver's phone detected a possible crash and help was sent (N-Ways drivers only during your started deliveries; N-Ways Business riders on duty or on a job). A crash rule only picks assets, channels and recipients; without one, owners, admins and dispatchers get every channel. Quiet hours never hold a crash back."
          },
          "active": {
            "type": "boolean",
            "default": true
          },
          "scope": {
            "type": "object",
            "description": "Which assets; empty lists = the whole fleet.",
            "properties": {
              "vehicleIds": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "groups": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "modes": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/VehicleMode"
                }
              }
            }
          },
          "area": {
            "type": "string",
            "enum": [
              "dispatch",
              "zones"
            ],
            "description": "geofence_exit: each delivery's own area, or zones."
          },
          "zoneIds": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "geofence_*: which zones (empty = all); idle: zones where stopping is fine (a depot)."
          },
          "silentS": {
            "type": "integer",
            "minimum": 120,
            "maximum": 604800,
            "default": 1800
          },
          "idleS": {
            "type": "integer",
            "minimum": 120,
            "maximum": 86400,
            "default": 1200
          },
          "idleRadiusM": {
            "type": "integer",
            "minimum": 20,
            "maximum": 5000,
            "default": 100
          },
          "channels": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "webhook",
                "email",
                "push"
              ]
            },
            "default": [
              "email",
              "push",
              "webhook"
            ]
          },
          "email": {
            "$ref": "#/components/schemas/Recipients"
          },
          "push": {
            "$ref": "#/components/schemas/Recipients"
          },
          "quietHours": {
            "type": [
              "object",
              "null"
            ],
            "properties": {
              "from": {
                "type": "string",
                "example": "22:00"
              },
              "to": {
                "type": "string",
                "example": "06:00"
              }
            },
            "description": "Lagos time: emails, pushes and webhooks wait until they end."
          },
          "alarms": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "sos",
                "power_cut",
                "overspeed",
                "low_battery",
                "tow",
                "crash",
                "transferred"
              ]
            },
            "description": "tracker_alarm: which alarms (empty = all). transferred: the tracker moved to another organisation."
          },
          "realertS": {
            "type": "integer",
            "description": "0 = tell once; else remind every realertS (300–86400) until acknowledged, at most 10 times.",
            "default": 3600
          },
          "createdBy": {
            "type": "string",
            "readOnly": true
          },
          "createdAt": {
            "type": "string",
            "format": "date-time",
            "readOnly": true
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time",
            "readOnly": true
          }
        },
        "example": {
          "name": "Boats leave their route area",
          "kind": "geofence_exit",
          "area": "dispatch",
          "scope": {
            "modes": [
              "boat"
            ]
          },
          "channels": [
            "webhook",
            "email",
            "push"
          ],
          "email": {
            "roles": [
              "owner",
              "dispatcher"
            ],
            "addresses": [
              "ops@example.ng"
            ]
          },
          "push": {
            "roles": [
              "dispatcher"
            ]
          },
          "quietHours": {
            "from": "22:00",
            "to": "06:00"
          },
          "realertS": 900
        }
      },
      "AlertEvent": {
        "type": "object",
        "required": [
          "id",
          "ruleId",
          "kind",
          "vehicleId",
          "state",
          "detail",
          "startedAt"
        ],
        "properties": {
          "id": {
            "type": "string",
            "example": "alr_8KwP2nVd5T"
          },
          "ruleId": {
            "type": "string",
            "description": "A rule id, or default:dispatch for a delivery area with no rule of its own, default:crash for a crash with no crash rule."
          },
          "ruleName": {
            "type": "string"
          },
          "kind": {
            "type": "string",
            "enum": [
              "geofence_exit",
              "geofence_enter",
              "silent",
              "idle",
              "crash",
              "tracker_alarm"
            ]
          },
          "vehicleId": {
            "type": "string"
          },
          "vehicleName": {
            "type": "string"
          },
          "subject": {
            "type": "string",
            "description": "task:<id> or zone:<id> for geofences; crash:<crs_id> for a crash."
          },
          "state": {
            "type": "string",
            "enum": [
              "open",
              "acknowledged",
              "resolved"
            ]
          },
          "detail": {
            "type": "object",
            "description": "position, at, taskId, area, zoneId, zoneName, lastSeenAt, since…; a crash: crashId, at, position, accuracyM, speedBeforeKmh, road, driverName, driverPhone (when you have it), taskId, taskRef, taskLabel, state (help | resolved), resolvedHow (ok | help_here)."
          },
          "startedAt": {
            "type": "string",
            "format": "date-time"
          },
          "notifiedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "notifications": {
            "type": "integer"
          },
          "acknowledgedBy": {
            "type": "string"
          },
          "acknowledgedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "resolvedBy": {
            "type": "string"
          },
          "resolvedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "autoResolved": {
            "type": "boolean",
            "description": "Cleared by itself (back inside, heard from again, moving)."
          }
        }
      },
      "EventType": {
        "type": "string",
        "enum": [
          "task.created",
          "task.assigned",
          "task.arriving",
          "task.completed",
          "task.failed",
          "vehicle.offline",
          "zone.entered",
          "zone.exited",
          "asset.geofence_exit",
          "asset.geofence_enter",
          "asset.silent",
          "asset.idle",
          "asset.alert_resolved",
          "connection.approved",
          "connection.declined",
          "connection.revoked",
          "connection.ended",
          "driver.availability_changed",
          "dispatch.offered",
          "dispatch.accepted",
          "dispatch.declined",
          "dispatch.expired",
          "dispatch.withdrawn",
          "task.started",
          "delivery.rated",
          "fleet.crash.detected",
          "fleet.crash.resolved",
          "task.updated"
        ]
      },
      "Event": {
        "type": "object",
        "required": [
          "id",
          "type",
          "version",
          "createdAt",
          "data"
        ],
        "properties": {
          "id": {
            "type": "string",
            "example": "evt_9bQ2xLm0aZc4RwYeT7Kp"
          },
          "type": {
            "type": "string",
            "description": "An EventType, or `ping` from the test call."
          },
          "version": {
            "type": "string",
            "example": "2026-10-16",
            "description": "The payload shape's version: fields are only added within a version."
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "data": {
            "type": "object",
            "properties": {
              "task": {
                "$ref": "#/components/schemas/Task"
              },
              "vehicle": {
                "description": "A Vehicle; {id, name} in zone events."
              },
              "zone": {
                "type": "object",
                "properties": {
                  "id": {
                    "type": "string"
                  },
                  "name": {
                    "type": "string"
                  }
                }
              },
              "at": {
                "type": "string",
                "format": "date-time"
              },
              "position": {
                "type": "object",
                "properties": {
                  "lat": {
                    "type": "number"
                  },
                  "lng": {
                    "type": "number"
                  }
                }
              },
              "alert": {
                "$ref": "#/components/schemas/AlertEvent"
              },
              "changed": {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "description": "task.updated: the fields the edit changed."
              }
            }
          }
        }
      },
      "Webhook": {
        "type": "object",
        "required": [
          "id",
          "url",
          "events",
          "active",
          "createdAt",
          "updatedAt"
        ],
        "properties": {
          "id": {
            "type": "string",
            "example": "wh_Ub3yH9sLk2"
          },
          "url": {
            "type": "string",
            "format": "uri"
          },
          "events": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/EventType"
            }
          },
          "active": {
            "type": "boolean"
          },
          "secret": {
            "type": "string",
            "example": "whsec_…",
            "description": "Only when made or rotated."
          },
          "headers": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Names of the custom headers sent with each delivery (values are never shown)."
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "Delivery": {
        "type": "object",
        "required": [
          "id",
          "event",
          "status",
          "attempts",
          "lastStatus",
          "lastError",
          "nextAttemptAt",
          "createdAt",
          "deliveredAt",
          "payload"
        ],
        "properties": {
          "id": {
            "type": "string",
            "example": "dlv_Qa71ZcR0mX"
          },
          "event": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "delivered",
              "failed"
            ]
          },
          "attempts": {
            "type": "integer"
          },
          "lastStatus": {
            "type": "integer",
            "description": "Your endpoint's last HTTP status (0 = no answer)."
          },
          "lastError": {
            "type": "string"
          },
          "nextAttemptAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "deliveredAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "payload": {
            "$ref": "#/components/schemas/Event"
          }
        }
      },
      "Zone": {
        "type": "object",
        "required": [
          "id",
          "name",
          "lat",
          "lng",
          "radiusM",
          "vehicles",
          "createdAt",
          "updatedAt"
        ],
        "properties": {
          "id": {
            "$ref": "#/components/schemas/Id"
          },
          "name": {
            "type": "string"
          },
          "lat": {
            "type": "number"
          },
          "lng": {
            "type": "number"
          },
          "radiusM": {
            "type": "integer"
          },
          "vehicles": {
            "type": "integer",
            "description": "Vehicles inside now."
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "RiderLink": {
        "type": "object",
        "required": [
          "id",
          "vehicleId",
          "createdBy",
          "createdAt",
          "expiresAt",
          "revokedAt",
          "consentedAt",
          "lastUsedAt",
          "active"
        ],
        "properties": {
          "id": {
            "type": "string",
            "example": "lnk_c8RdT5wQ1e"
          },
          "vehicleId": {
            "type": "string"
          },
          "url": {
            "type": "string",
            "format": "uri",
            "description": "Only when made: https://nways.ng/d/<token>."
          },
          "createdBy": {
            "type": "string"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "expiresAt": {
            "type": "string",
            "format": "date-time"
          },
          "revokedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "consentedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When the rider agreed to share."
          },
          "lastUsedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "active": {
            "type": "boolean"
          }
        }
      },
      "LiveVehicle": {
        "type": "object",
        "required": [
          "id",
          "name",
          "mode",
          "plate",
          "driver",
          "state",
          "lastLocation",
          "stoppedSince",
          "task",
          "openTasks",
          "zones"
        ],
        "properties": {
          "id": {
            "$ref": "#/components/schemas/Id"
          },
          "name": {
            "type": "string"
          },
          "mode": {
            "$ref": "#/components/schemas/VehicleMode"
          },
          "plate": {
            "type": "string"
          },
          "group": {
            "type": "string"
          },
          "colour": {
            "$ref": "#/components/schemas/VehicleColour"
          },
          "batteryPct": {
            "type": "number",
            "description": "The battery at its last report, when it sends one."
          },
          "driver": {
            "oneOf": [
              {
                "type": "object",
                "required": [
                  "id",
                  "name",
                  "phone"
                ],
                "properties": {
                  "id": {
                    "type": "string"
                  },
                  "name": {
                    "type": "string"
                  },
                  "phone": {
                    "type": "string"
                  }
                }
              },
              {
                "type": "null"
              }
            ]
          },
          "state": {
            "type": "string",
            "enum": [
              "moving",
              "stopped",
              "offline",
              "never"
            ]
          },
          "lastLocation": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/Position"
              },
              {
                "type": "null"
              }
            ]
          },
          "stoppedSince": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "task": {
            "oneOf": [
              {
                "type": "object",
                "required": [
                  "id",
                  "ref",
                  "status",
                  "pickup",
                  "dropoff",
                  "recipientName",
                  "eta",
                  "dueAt",
                  "late",
                  "trackingUrl"
                ],
                "properties": {
                  "id": {
                    "type": "string"
                  },
                  "ref": {
                    "type": "string",
                    "description": "The delivery's number (#1042)."
                  },
                  "status": {
                    "$ref": "#/components/schemas/TaskStatus"
                  },
                  "pickup": {
                    "oneOf": [
                      {
                        "$ref": "#/components/schemas/Stop"
                      },
                      {
                        "type": "null"
                      }
                    ]
                  },
                  "dropoff": {
                    "$ref": "#/components/schemas/Stop"
                  },
                  "recipientName": {
                    "type": "string"
                  },
                  "eta": {
                    "oneOf": [
                      {
                        "$ref": "#/components/schemas/ETA"
                      },
                      {
                        "type": "null"
                      }
                    ]
                  },
                  "dueAt": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "format": "date-time"
                  },
                  "late": {
                    "type": "boolean"
                  },
                  "trackingUrl": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "format": "uri"
                  }
                }
              },
              {
                "type": "null"
              }
            ]
          },
          "openTasks": {
            "type": "integer"
          },
          "zones": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string"
                },
                "name": {
                  "type": "string"
                }
              }
            }
          }
        }
      },
      "Live": {
        "type": "object",
        "required": [
          "vehicles",
          "counts",
          "settings",
          "at"
        ],
        "properties": {
          "vehicles": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/LiveVehicle"
            }
          },
          "counts": {
            "type": "object",
            "required": [
              "total",
              "moving",
              "stopped",
              "offline",
              "never",
              "activeTasks",
              "unassigned",
              "late"
            ],
            "properties": {
              "total": {
                "type": "integer"
              },
              "moving": {
                "type": "integer"
              },
              "stopped": {
                "type": "integer"
              },
              "offline": {
                "type": "integer"
              },
              "never": {
                "type": "integer"
              },
              "activeTasks": {
                "type": "integer"
              },
              "unassigned": {
                "type": "integer"
              },
              "late": {
                "type": "integer"
              }
            }
          },
          "settings": {
            "$ref": "#/components/schemas/Settings"
          },
          "at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "Alert": {
        "type": "object",
        "required": [
          "kind",
          "vehicleId",
          "vehicleName",
          "taskId",
          "since",
          "detail"
        ],
        "properties": {
          "kind": {
            "type": "string",
            "enum": [
              "crash",
              "offline",
              "late",
              "stopped"
            ],
            "description": "crash first (open until resolved; detail as the crash alert's, with alertId)."
          },
          "vehicleId": {
            "type": [
              "string",
              "null"
            ]
          },
          "vehicleName": {
            "type": "string"
          },
          "taskId": {
            "type": [
              "string",
              "null"
            ]
          },
          "taskRef": {
            "type": "string",
            "description": "The delivery's number (#1042), for the alert's words."
          },
          "taskLabel": {
            "type": "string",
            "description": "Who or where the delivery is for."
          },
          "vehicleColour": {
            "$ref": "#/components/schemas/VehicleColour"
          },
          "since": {
            "type": "string",
            "format": "date-time"
          },
          "detail": {
            "type": "object",
            "description": "offline {lastSeenAt, offlineAfterS}; late {dueAt, status, etaAt?}; stopped {stoppedS, stopAlertS, position}."
          }
        }
      },
      "TrafficFeed": {
        "type": "object",
        "required": [
          "type",
          "properties",
          "features"
        ],
        "properties": {
          "type": {
            "const": "FeatureCollection"
          },
          "properties": {
            "type": "object",
            "properties": {
              "kind": {
                "type": "string",
                "enum": [
                  "live",
                  "typical"
                ]
              },
              "mode": {
                "type": "string",
                "enum": [
                  "car",
                  "motorcycle"
                ]
              },
              "bbox": {
                "type": "array",
                "items": {
                  "type": "number"
                },
                "minItems": 4,
                "maxItems": 4
              },
              "at": {
                "type": "string",
                "format": "date-time"
              },
              "generatedAt": {
                "type": "string",
                "format": "date-time"
              },
              "minDevices": {
                "type": "integer",
                "description": "The k-anonymity threshold applied (live 2, typical 3)."
              },
              "attribution": {
                "type": "string"
              }
            }
          },
          "features": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "type",
                "geometry",
                "properties"
              ],
              "properties": {
                "type": {
                  "const": "Feature"
                },
                "geometry": {
                  "type": "object",
                  "description": "GeoJSON LineString"
                },
                "properties": {
                  "type": "object",
                  "required": [
                    "wayId",
                    "direction",
                    "speedKmh",
                    "freeFlowKmh",
                    "level",
                    "source"
                  ],
                  "properties": {
                    "wayId": {
                      "type": "integer",
                      "description": "OpenStreetMap way id."
                    },
                    "direction": {
                      "type": "string",
                      "enum": [
                        "forward",
                        "backward"
                      ]
                    },
                    "speedKmh": {
                      "type": "number"
                    },
                    "freeFlowKmh": {
                      "type": "number"
                    },
                    "level": {
                      "type": "integer",
                      "minimum": 0,
                      "maximum": 3
                    },
                    "source": {
                      "type": "string",
                      "enum": [
                        "live",
                        "typical"
                      ]
                    },
                    "updatedAt": {
                      "type": "string",
                      "format": "date-time",
                      "description": "Live only, to the minute."
                    }
                  }
                }
              }
            }
          }
        }
      },
      "Region": {
        "type": "object",
        "required": [
          "id",
          "name",
          "country",
          "bbox"
        ],
        "properties": {
          "id": {
            "type": "string",
            "example": "lagos"
          },
          "name": {
            "type": "string"
          },
          "country": {
            "type": "string",
            "example": "NG"
          },
          "bbox": {
            "type": "array",
            "items": {
              "type": "number"
            },
            "minItems": 4,
            "maxItems": 4
          }
        }
      },
      "PackManifest": {
        "type": "object",
        "required": [
          "region",
          "maxzoom",
          "version",
          "status",
          "files"
        ],
        "properties": {
          "region": {
            "$ref": "#/components/schemas/Region"
          },
          "maxzoom": {
            "type": "integer"
          },
          "version": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "building",
              "ready"
            ]
          },
          "builtAt": {
            "type": "string",
            "format": "date-time"
          },
          "files": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "name",
                "bytes",
                "sha256",
                "url"
              ],
              "properties": {
                "name": {
                  "type": "string",
                  "enum": [
                    "map.pmtiles",
                    "routing.tar"
                  ]
                },
                "bytes": {
                  "type": "integer"
                },
                "sha256": {
                  "type": "string"
                },
                "url": {
                  "type": "string",
                  "description": "Path to download (add your key)."
                }
              }
            }
          },
          "notes": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        }
      }
    }
  }
}
