GPS Export
GPS Export packages a period of raw GPS data for one or more devices into a
downloadable archive (GPX / CSV / KML) through an asynchronous job:
create the job, track its progress, then download the artifact. Job
management requires the gps:export scope (pro tier or above), and job
creation consumes the plan's monthly export_job quota (see
Rate Limits).
Creating a job
POST /v1/gps/export-jobs — requires the gps:export scope. from / to /
format are required, and the requested range can span at most 90 days
(UTC calendar days — to at most 90 days after from). A wider range is
rejected with 422 invalid_field carrying
maxRangeDays: 90 and the offending rangeDays, before any quota is charged —
fix the range; retrying does not help. Omitting psns targets every
cloud-connected device in the organization.
All timestamps in this API are UTC (RFC 3339, Z), and export ranges are
UTC calendar days: only the UTC date of from / to is used — the time
of day is ignored and the archive covers whole UTC days, so
2026-06-01T18:00:00Z and 2026-06-01T00:00:00Z request the same first day.
The timestamps inside the exported files (GPX <time>, CSV datetime) are
UTC as well.
curl -X POST "https://openapi.fleeta.io/v1/gps/export-jobs" \
-H "Authorization: Bearer flt_live_..." \
-H "Content-Type: application/json" \
-d '{
"psns": ["7XBPK0BE00000001", "7XBPK0BE00000003"],
"from": "2026-06-01T00:00:00Z",
"to": "2026-07-01T00:00:00Z",
"format": "csv",
"packaging": "perDevice"
}'
{
"data": {
"jobId": "gpsexp_a1b2c3d4e5f6",
"status": "queued",
"estimatedPoints": 480,
"createdAt": "2026-07-16T04:00:00Z"
}
}
format:gpx/csv/kmlpackaging:single(one merged file) orperDevice(one file per device). Omit it and the job follows the web console:perDevicewhen more than one device is targeted,singlefor a single device. Set it explicitly whenever you need a specific layout — an explicit value is always used as given.
packaging: "single" puts every device and every day into one file, and a CSV
holds one row per GPS point — there is no row cap. Spreadsheets stop at
1,048,576 rows (Excel, Google Sheets and LibreOffice alike), and Excel will
open a larger file with the surplus silently cut off, which reads as "my data
disappeared".
Rough size: rows ≈ devices × days × 300 to 1,000. A busy vehicle logs about
1,000 points a day and a fleet averages nearer 300, so:
| Devices | Days | Rows (≈300/day) | Rows (≈1,000/day) |
|---|---|---|---|
| 10 | 30 | 90,000 | 300,000 |
| 70 | 30 | 630,000 | 2,100,000 ⚠ |
| 70 | 90 | 1,890,000 ⚠ | 6,300,000 ⚠ |
Use perDevice (the default for multi-device jobs) or split the range into
shorter jobs when the estimate approaches the limit. GET /v1/gps/export-jobs/{jobId}
reports estimatedPoints, which is the same number of CSV rows.
- Cloud devices only — GPS export covers cloud-connected dashcams
(
category: cloudinGET /v1/devices). Naming a Wi-Fi-only dashcam (category: wifi) inpsnsreturns422 cloud_onlywith the offending PSNs inpsns; a PSN outside your company/group scope returns422 unknown_psn. Omitpsnsto export every cloud-connected device in the organization. - One job at a time per organization — creating a job while another is
still
queued/runningreturns a409. Two distinct codes can appear here, and they are checked in this order:concurrency_limit_exceeded— your tier's concurrent-job allowance is used up. The problem response carrieslimitandactive. Only enterprise keys have an allowance above 1; on tiers whose allowance is0, GPS Export is not offered at all and every create returns this code. This check runs before the quota is charged, so a rejected create costs you nothing.export_in_progress— the export backend already has a job running for this organization. The response carries the in-flight job'sactiveJobIdandactiveStatusso you can poll or cancel it first.
- The monthly job count is capped by the
export_jobquota — exceeding it returns403 quota_exceeded, which is not retryable — see Rate Limits.
Tracking progress
GET /v1/gps/export-jobs/{jobId} returns the job with a real-time
progress percentage. While the job is running, progress advances
from 0 up to 95 as chunks are processed (computed live from worker
progress); it reads 100 exactly when status is completed.
{
"data": {
"jobId": "gpsexp_a1b2c3d4e5f6",
"status": "running",
"progress": 62,
"estimatedPoints": 480,
"createdAt": "2026-07-16T04:00:00Z"
}
}
The job lifecycle is queued → running (progress 0–95) → completed (progress 100). A job can also end as failed, or canceled via
DELETE /v1/gps/export-jobs/{jobId} (canceling an already-completed job
returns 409 job_already_completed).
Downloading the result
Once status is completed, the same lookup includes a presigned
downloadUrl:
{
"data": {
"jobId": "gpsexp_a1b2c3d4e5f6",
"status": "completed",
"progress": 100,
"estimatedPoints": 480,
"createdAt": "2026-07-16T04:00:00Z",
"completedAt": "2026-07-16T04:00:30Z",
"downloadUrl": "https://…s3….amazonaws.com/gps-export/…/output.zip?X-Amz-Signature=…",
"downloadFileName": "gps-export_20260601_20260701_csv.zip",
"downloadUrlExpiresIn": 3600
}
}
downloadUrlis valid fordownloadUrlExpiresInseconds (3600 = 1 hour) from the moment of the response. It is regenerated on every lookup — if a URL expires, simply re-request the job to get a fresh one.- The URL is a plain presigned link — download it with any HTTP client, no
Authorizationheader needed (and none should be sent). downloadUrlis omitted when the job produced no data for the requested range — treat it as optional.
What is inside the archive
The download is a zip. Its entries depend on packaging: one
export.<ext> for single, or one file per device
(camera-<name>-<psn>.<ext>) for perDevice. Every timestamp inside is UTC.
CSV columns
One row per GPS point, in this order. Columns are only ever appended — the existing names and their order are a fixed contract, so a parser that reads by position keeps working.
| Column | Meaning | Unit / format |
|---|---|---|
psn | Dashcam serial number | — |
carName | Vehicle name at export time (empty when unnamed) | — |
datetime | Time of the point | RFC 3339 UTC (…Z) |
lat | Latitude | degrees (WGS 84) |
lng | Longitude | degrees (WGS 84) |
drive_no | Trip number the point belongs to | integer |
heading | Direction of travel | degrees, 0 = north |
speedKmh | Ground speed | km/h, one decimal — empty when the device reported no speed (0 means "stationary" and is a real value) |
GPX / KML
- GPX 1.1 — one
<trk>per trip,<trkpt>per point with<time>(UTC). Speed rides in the standard Garmin extension (<gpxtpx:TrackPointExtension><gpxtpx:speed>), whose unit is metres per second, not km/h. Points with no reported speed carry no<extensions>block at all. - KML 2.2 — one
<Placemark>per trip with a<LineString>. KML has no standard place for a per-point scalar, so KML carries no speed. Choose CSV or GPX when you need it.
Integration patterns
Two recommended ways to consume export jobs:
(a) Polling — when you need a progress UI
Poll the job at a modest interval (5 seconds or more) and render
progress; download when completed.
async function runExport(bv, body) {
const { data: job } = await bv.telemetry.createExportJob(body);
for (;;) {
await new Promise((r) => setTimeout(r, 5000)); // 5s+ — don't burn rate limit
const { data } = await bv.telemetry.exportJob(job.jobId);
render(data.progress); // 0–95 while running, 100 when done
if (data.status === 'completed') return data.downloadUrl; // presigned, 1h
if (data.status === 'failed' || data.status === 'canceled') {
throw new Error(`export ${data.status}`);
}
}
}
(b) export_completed webhook — recommended for server integrations
Subscribe to the export_completed webhook and download
when the push arrives — no polling loop, no wasted quota. The webhook
payload carries its own downloadUrl / downloadFileName /
downloadUrlExpiresIn (1 hour), so most receivers can download straight
from the delivery. If your queue delays processing past the TTL,
re-request GET /v1/gps/export-jobs/{jobId} for a fresh URL.
app.post('/hooks/fleeta', express.raw({ type: 'application/json' }), async (req, res) => {
const rawBody = req.body.toString('utf8');
if (!verifyWebhook(process.env.WEBHOOK_SECRET, rawBody, req.headers)) {
return res.status(401).end();
}
res.status(200).end(); // ack first, process async
const payload = JSON.parse(rawBody);
if (payload.event === 'export_completed' && payload.data.downloadUrl) {
await downloadTo(payload.data.downloadUrl, payload.data.downloadFileName);
}
});
See the Webhooks guide for the full
payload (jobId / format / packaging / cameraCount / rangeStart /
rangeEnd / sizeBytes / partial / download fields) and signature
verification — verifyWebhook above is the guide's any-match verifier, which
keeps working through a secret rotation.
Caveats / best practices
- Partial results:
partial: true(in the webhook payload) means some devices/chunks failed — the artifact covers only part of the requested range. Decide whether to re-run for the missing window. - Jobs and their artifacts expire (
expiresAtin list responses) — don't treat the job store as long-term storage; move artifacts to your own storage after download. - Listing jobs (
GET /v1/gps/export-jobs) uses cursor pagination, newest first — filter withstatus. - Range cap: split exports longer than 90 days into consecutive jobs (one
at a time per organization — see above). The cap counts UTC calendar days,
and the time of day in
from/tois ignored. - File size: there is no row cap on a merged file. Leave
packagingunset (multi-device jobs then split per device) or set it explicitly, and checkestimatedPointsagainst the 1,048,576-row spreadsheet limit before handing asingleCSV to a spreadsheet — see the caution above. - Everything is UTC: the
from/toyou send, thefrom/to/createdAt/completedAtechoed on the job, the webhook'srangeStart/rangeEnd, and every timestamp inside the exported files. Convert to local time on your side if you need it. - Export quotas are tier-based (
export_jobmonthly,export_concurrentparallel). Exceedingexport_concurrentreturns409 concurrency_limit_exceeded; independently of the quota, an organization runs one job at a time upstream (409 export_in_progress). A403 quota_exceededis not solved by retrying — see Rate Limits.
Related links
- Webhooks —
export_completedpayload and HMAC verification - Rate Limits —
export_job/export_concurrentquotas - Pagination — cursor iteration for the job list
- SDKs —
telemetry.createExportJob()/exportJob()/cancelExportJob()