Developers
Every vehicle, position, and alert you can see in Dragon Fleet, you can also read programmatically — on every plan, with no enterprise tier and no extra approval step. Mint a scoped token in Settings → API tokens, pick exactly the scopes and vehicles it needs, and call the same REST API the web app itself uses.
Programmatic access is not gated by subscription tier — it is part of the base product, the same way your data always being exportable is.
Base URL & auth
Every endpoint below is served from this origin under /api/v1 — for example, the hosted deployment serves it at https://api.apps.drwifi.nz/api/v1. A self-hosted instance serves the same paths from its own origin.
Authenticate every request with a scoped API token as a bearer credential:
Authorization: Bearer dragonfleet_sk_...
Tokens are shown in full exactly once at creation time and stored hashed thereafter — there is no way to retrieve a lost token; revoke it and issue a new one.
Scopes
A token can only call an endpoint if it was issued with that endpoint's required scope. Vehicle-scoped endpoints (everything under /vehicles/{id}) are further limited to the specific vehicles the token was issued for — re-checked against the issuing user's current access on every request, not just at issuance. Organization-level reads (/geofences, /drivers) return every record in the token's organization regardless of its vehicle scope.
Vehicle metadata and device listings.
The single most recent position fix — the most sensitive read in the product.
Bucketed position history, speed alerts, and mileage reports.
The generic alert-event feed (currently: speed alerts; more alert types land here over time).
The organization's geofence definitions.
Reserved vocabulary only — no endpoint currently accepts it. Every geofence mutation route rejects scoped tokens outright (403); this scope exists so the read/write pair is symmetric for a future task that revisits token-scoped writes.
The organization's driver roster and per-vehicle driver assignments.
No write scope grants a mutation today — issuing, listing, and revoking tokens, and every create/update/delete endpoint in the API, are session-only in v1. A scoped token that calls a mutation route always gets 403.
Endpoints
Every endpoint below also works with a signed-in user's session — this list covers the subset a scoped API token can reach.
List vehicles visible to the caller.
Response: Vehicle[]
A single vehicle by id. 404 if it doesn't exist or isn't visible to this token.
Response: Vehicle
Devices attached to a vehicle.
Response: Device[]
The vehicle's single most recent position fix — "where is this vehicle right now".
Response: { position: Position | null }
Hourly-bucketed position trend data (up to ~1h stale — never the live-tracking path).
Query: from=<ISO 8601>&to=<ISO 8601> (both required, from < to)
Response: { history: HistoryBucket[] }
The full ordered (ts asc), server-decoded position track for a bounded window — the trip-replay path. Server-side even-sampling capped at 3000 points. Requires the advanced_reports plan (a 402 upgrade response otherwise — the one token endpoint that can 402).
Query: from=<ISO 8601>&to=<ISO 8601> (both required, from < to)
Response: { track: TrackPoint[] }
Speed-alert events for a vehicle, newest first, location decoded to lon/lat.
Query: from=<ISO 8601>&to=<ISO 8601> (both optional, both-or-neither)
Response: { alerts: VehicleAlert[] }
Total distance travelled over a window — a billing-ready mileage report.
Query: driver_assignment_id=<uuid> OR (from=<ISO 8601>&to=<ISO 8601>)
Response: { distance_km, positions_count, from, to, driver? }
The generic, decoded alert-event feed across every alert type the platform emits.
Query: from=&to=&type=<alert_type>&limit=<1-500, default 200> (all optional)
Response: { alert_events: AlertEvent[] }
The caller's organization's geofences, geometry decoded to GeoJSON-shaped numbers.
Response: Geofence[]
The caller's organization's driver roster.
Response: Driver[]
A vehicle's driver assignments, current and past, newest first, driver joined in.
Response: DriverAssignmentWithDriver[]
Errors
Errors are always a JSON object with an error string, never a raw upstream message:
{ "error": "Token is missing required scope: positions:read:live" }Malformed request — a bad query param, invalid body, or a bad vehicle/token id.
Missing, malformed, unknown, revoked, or expired token.
The organization's subscription entitlement blocks the action.
Authenticated, but this token's scopes don't cover this endpoint (or it's a mutation route — no write scopes exist for tokens in v1).
The resource doesn't exist, or isn't visible to this token — deliberately conflated so a token can never learn that a vehicle it can't see exists.
Valid and authorized, but conflicts with existing state.
Something failed on our end. Never contains upstream error detail.
Example: curl
curl https://api.apps.drwifi.nz/api/v1/vehicles/<vehicle-id>/position \ -H "Authorization: Bearer dragonfleet_sk_..."
Example: JavaScript
Plain fetch:
const res = await fetch(
"https://api.apps.drwifi.nz/api/v1/vehicles/<vehicle-id>/history?from=2026-07-01T00:00:00Z&to=2026-07-08T00:00:00Z",
{ headers: { Authorization: "Bearer dragonfleet_sk_..." } },
);
const { history } = await res.json();Or the typed client this app itself uses (@dragonfleet/api-client on npm within this monorepo — createApiClient accepts a static scoped token exactly the same way it accepts a session token):
import { createApiClient } from "@dragonfleet/api-client";
const client = createApiClient({
baseUrl: "https://api.apps.drwifi.nz",
getAuthToken: () => "dragonfleet_sk_...",
});
const vehicles = await client.vehicles.list();What's not here yet
There is no machine-readable OpenAPI/Swagger document yet — this hand-written page is the honest v1. The endpoint list above is kept accurate by cross-checking it against the actual route handlers, not generated from them; if that drifts, treat this page as the bug and the route handlers as the source of truth. A generated spec is a reasonable follow-up if real third-party integration demand shows up, not before.