The migration shape
Every fleet-management migration boils down to three chunks of data:
- Assets — the fleet roster (trucks, equipment, trailers). Goal: every row in your old system becomes an Asset in DirtFleet, with the same nickname / VIN / serial / customer asset number so people's mental model carries over.
- Historical hours — meter readings on each asset over the last 3-12 months. Goal: the PM schedule has enough baseline to start auto-flagging at the right thresholds.
- Open work orders — what's in progress right now. Goal: nothing falls through the cracks during the cutover.
DirtFleet's bulk-import endpoints handle all three. Assets cap at 50 per request, hours at 100, work orders one at a time. A 100-asset fleet with 12 months of weekly hours logs migrates in ~250 API calls — under 5 minutes end-to-end at the default 60 req/min cap.
Recipe 1
Fleetio → DirtFleet
Fleetio's data export ships as a ZIP of CSVs: vehicles.csv, meter-entries.csv, work-orders.csv. We map the columns and POST them in chunks.
#!/usr/bin/env node
// node 20+ — node fleetio-to-dirtfleet.mjs ./fleetio-export
import { readFile } from "node:fs/promises";
import { join } from "node:path";
const KEY = process.env.DIRTFLEET_API_KEY;
if (!KEY) throw new Error("Set DIRTFLEET_API_KEY");
const [, , dir] = process.argv;
if (!dir) throw new Error("Usage: node fleetio-to-dirtfleet.mjs <export-dir>");
// Tiny CSV parser — Fleetio exports don't use embedded quotes.
function parseCsv(text) {
const [header, ...lines] = text.trim().split(/\r?\n/);
const cols = header.split(",").map((c) => c.trim());
return lines.map((line) => {
const cells = line.split(",").map((c) => c.trim());
return Object.fromEntries(cols.map((c, i) => [c, cells[i] ?? ""]));
});
}
async function post(path, body) {
const r = await fetch(`https://dirtfleet.app${path}`, {
method: "POST",
headers: { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" },
body: JSON.stringify(body),
});
return r.json();
}
// 1. Vehicles → Assets
const vehicles = parseCsv(await readFile(join(dir, "vehicles.csv"), "utf8"));
const fleetioToDfAsset = new Map(); // fleetio_id → df_asset_id
for (let i = 0; i < vehicles.length; i += 50) {
const batch = vehicles.slice(i, i + 50).map((v) => ({
nickname: v.name,
assetClass: v.asset_type === "trailer" ? "trailer"
: v.asset_type === "off-road" ? "off-road"
: "on-road",
serial: v.serial || null,
vin: v.vin || null,
customerAssetNumber: v.asset_number || null,
}));
const res = await post("/api/v1/assets/batch", batch);
for (const r of res.results) {
if (r.ok) fleetioToDfAsset.set(vehicles[i + r.index].id, r.assetId);
}
console.log(`Assets ${i + batch.length}/${vehicles.length}`);
}
// 2. Meter entries → Hours
const meters = parseCsv(await readFile(join(dir, "meter-entries.csv"), "utf8"));
let imported = 0;
for (let i = 0; i < meters.length; i += 100) {
const batch = meters.slice(i, i + 100).map((m) => {
const dfAssetId = fleetioToDfAsset.get(m.vehicle_id);
if (!dfAssetId) return null;
return {
assetId: dfAssetId,
hoursReading: parseFloat(m.value) || 0,
note: `migrated from Fleetio entry ${m.id}`,
clientMutationId: `fleetio-meter-${m.id}`,
};
}).filter(Boolean);
if (!batch.length) continue;
const res = await post("/api/v1/hours/batch", batch);
imported += res.summary.succeeded;
}
console.log(`Hours imported: ${imported}/${meters.length}`);
// 3. Open work orders — single POSTs (no batch endpoint for WOs)
const wos = parseCsv(await readFile(join(dir, "work-orders.csv"), "utf8"));
for (const wo of wos) {
if (wo.status !== "open") continue;
const dfAssetId = fleetioToDfAsset.get(wo.vehicle_id);
if (!dfAssetId) continue;
await post("/api/v1/work-orders", {
title: wo.description || "Untitled (Fleetio import)",
assetId: dfAssetId,
priority: wo.priority === "high" ? "HIGH" : "NORMAL",
status: "OPEN",
});
}
console.log("Done.");Recipe 2
Generic CSV (spreadsheet escape)
If you've been running on a hand-maintained Google Sheet or Excel file, the migration is even simpler — just the asset roster plus optional historic hours. Save as CSV and run a tighter version of recipe 1.
#!/usr/bin/env node
// node 20+ — point at a CSV with columns: nickname,assetClass,serial,vin
// Run as: node spreadsheet-to-dirtfleet.mjs ./fleet.csv
import { readFile } from "node:fs/promises";
const KEY = process.env.DIRTFLEET_API_KEY;
const [, , file] = process.argv;
const text = await readFile(file, "utf8");
const [header, ...lines] = text.trim().split(/\r?\n/);
const cols = header.split(",").map((c) => c.trim());
const rows = lines.map((line) => {
const cells = line.split(",").map((c) => c.trim());
return Object.fromEntries(cols.map((c, i) => [c, cells[i] ?? ""]));
});
for (let i = 0; i < rows.length; i += 50) {
const batch = rows.slice(i, i + 50).map((r) => ({
nickname: r.nickname,
assetClass: r.assetClass || "off-road",
serial: r.serial || null,
vin: r.vin || null,
}));
const res = await fetch("https://dirtfleet.app/api/v1/assets/batch", {
method: "POST",
headers: { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" },
body: JSON.stringify(batch),
});
const body = await res.json();
console.log(`Chunk ${i / 50 + 1}: ${body.summary.succeeded} ok, ${body.summary.failed} failed`);
}Recipe 3
Whip Around → DirtFleet
Whip Around is checklist-first; the asset list there is usually thin (nickname + asset number). Their export ships one CSV per fleet. Migrate the assets, manually re-create your inspection checklists in DirtFleet (they don't translate 1:1 — Whip Around treats checklists as a separate domain than maintenance).
#!/usr/bin/env node
// Whip Around assets export → DirtFleet
// Run as: node whip-around-to-dirtfleet.mjs ./assets.csv
const KEY = process.env.DIRTFLEET_API_KEY;
const text = await fetch("file://" + process.argv[2]).then((r) => r.text())
.catch(async () => (await import("node:fs/promises")).readFile(process.argv[2], "utf8"));
const [header, ...lines] = text.trim().split(/\r?\n/);
const cols = header.split(",").map((c) => c.trim());
const rows = lines.map((line) => {
const cells = line.split(",").map((c) => c.trim());
return Object.fromEntries(cols.map((c, i) => [c, cells[i] ?? ""]));
});
// Whip Around columns we care about: name, asset_number, vin, type
for (let i = 0; i < rows.length; i += 50) {
const batch = rows.slice(i, i + 50).map((r) => ({
nickname: r.name,
customerAssetNumber: r.asset_number || null,
vin: r.vin || null,
// Whip Around's "type" doesn't map cleanly; default to on-road
// for trucks/vans, off-road for everything else.
assetClass: /truck|van|car/i.test(r.type ?? "") ? "on-road" : "off-road",
}));
await fetch("https://dirtfleet.app/api/v1/assets/batch", {
method: "POST",
headers: { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" },
body: JSON.stringify(batch),
});
}
console.log("Asset import done.");
console.log("Next: re-create your inspection checklists in DirtFleet.");
console.log("See: /checklists (in-app) — Whip Around's checklist model");
console.log("doesn't map 1:1, so this part is a one-time hand-port.");Then what
After the import
- Set PM intervals. Open each asset in DirtFleet, set the
serviceIntervalHrs(engine hours between PMs). The AUTO_PM flag system picks it up on the next hours log. - Invite your team.
/settings/teamin-app — invite drivers, mechanics, fleet managers. Unlimited users. - Hook up your integrations. Zapier, Slack, QuickBooks Online, Samsara.
- Verify your import.
curl https://dirtfleet.app/api/v1/meshows the org + key;/api/v1/assets+ cursor pagination walks the imported fleet.
Stuck? Email support@dirtfleet.app with your export file — a real person reads it. Most migrations we've helped with land in 60-90 minutes including the hand-port pieces.
← Back to the API reference · → More recipes · → SDK codegen