← Back to home

Docs · API · Migration

Migrate in an afternoon.

Switching to DirtFleet from Fleetio / Samsara Maintain / Whip Around / a spreadsheet? The bulk-import endpoints handle the heavy lifting. Pick the recipe that matches your source and copy-paste the script.

The migration shape

Every fleet-management migration boils down to three chunks of data:

  1. 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.
  2. 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.
  3. 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/team in-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/me shows 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