Recipe 1
Import 200 assets from a CSV
The most common one-shot migration: you have a spreadsheet of assets from Fleetio / Samsara / a custom system, and you want to seed them in DirtFleet. The bulk-import endpoint handles up to 50 per call, so we chunk.
#!/usr/bin/env node
// node 20+ — set DIRTFLEET_API_KEY in env, then: node import-assets.mjs fleet.csv
import { readFile } from "node:fs/promises";
const KEY = process.env.DIRTFLEET_API_KEY;
if (!KEY) throw new Error("Set DIRTFLEET_API_KEY");
const [, , file] = process.argv;
if (!file) throw new Error("Usage: node import-assets.mjs <fleet.csv>");
// CSV columns expected: nickname,assetClass,serial,vin,customerAssetNumber
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(",");
return Object.fromEntries(cols.map((c, i) => [c, cells[i]?.trim() ?? ""]));
});
const CHUNK = 50;
let total = 0;
for (let i = 0; i < rows.length; i += CHUNK) {
const batch = rows.slice(i, i + CHUNK).map((r) => ({
nickname: r.nickname,
assetClass: r.assetClass,
serial: r.serial || null,
vin: r.vin || null,
customerAssetNumber: r.customerAssetNumber || 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();
if (!body.ok) {
console.error("Batch failed:", body);
break;
}
total += body.summary.succeeded;
console.log(`Chunk ${i / CHUNK + 1}: ${body.summary.succeeded} ok, ${body.summary.failed} failed`);
for (const r of body.results) {
if (!r.ok) console.error(` Row ${i + r.index}:`, r.fieldErrors ?? r.formError);
}
}
console.log(`Done. Imported ${total} assets.`);Recipe 2
Export last week's hours to a TSV
Accounting wants a copy of every meter reading for the prior week, dropped on a shared drive. Cursor pagination keeps the script working when the fleet grows past 100 logs.
#!/usr/bin/env node
// node 20+ — node export-hours-weekly.mjs > weekly-hours.tsv
const KEY = process.env.DIRTFLEET_API_KEY;
if (!KEY) throw new Error("Set DIRTFLEET_API_KEY");
const sinceDate = new Date();
sinceDate.setDate(sinceDate.getDate() - 7);
const since = sinceDate.toISOString();
let cursor = "";
let printed = 0;
console.log("loggedAt\tassetId\thoursReading\tdelta\tnote");
while (true) {
const url = new URL("https://dirtfleet.app/api/v1/hours");
url.searchParams.set("since", since);
url.searchParams.set("limit", "200");
if (cursor) url.searchParams.set("cursor", cursor);
const res = await fetch(url, { headers: { Authorization: `Bearer ${KEY}` } });
const body = await res.json();
if (!body.ok) { console.error(body); process.exit(1); }
for (const log of body.hoursLogs) {
console.log([
log.loggedAt, log.assetId, log.hoursReading ?? "", log.delta ?? "",
(log.note ?? "").replace(/\t/g, " "),
].join("\t"));
printed++;
}
if (!body.nextCursor) break;
cursor = body.nextCursor;
}
console.error(`Exported ${printed} logs since ${since}`);Recipe 3
Mirror open flags to a Postgres table
Nightly cron pulls the current open-flags set and upserts into your data warehouse for BI tooling. Webhooks would be push-based; this is the pull alternative for analysts who don't want a receiver process.
#!/usr/bin/env python3
# pip install httpx psycopg[binary]
import os, httpx, psycopg
KEY = os.environ["DIRTFLEET_API_KEY"]
PG = os.environ["DATABASE_URL"]
with psycopg.connect(PG, autocommit=True) as conn:
with conn.cursor() as cur:
cur.execute("""
CREATE TABLE IF NOT EXISTS df_open_flags (
id TEXT PRIMARY KEY,
asset_id TEXT NOT NULL,
severity TEXT NOT NULL,
note TEXT,
raised_by_id TEXT,
created_at TIMESTAMPTZ NOT NULL,
synced_at TIMESTAMPTZ NOT NULL DEFAULT now()
)
""")
cursor = ""
synced_ids = set()
while True:
params = {"limit": 200, "status": "OPEN"}
if cursor: params["cursor"] = cursor
r = httpx.get(
"https://dirtfleet.app/api/v1/flags",
params=params,
headers={"Authorization": f"Bearer {KEY}"},
timeout=30,
)
r.raise_for_status()
body = r.json()
for f in body["flags"]:
cur.execute("""
INSERT INTO df_open_flags (id, asset_id, severity, note, raised_by_id, created_at)
VALUES (%s, %s, %s, %s, %s, %s)
ON CONFLICT (id) DO UPDATE
SET severity = EXCLUDED.severity,
note = EXCLUDED.note,
synced_at = now()
""", (f["id"], f["assetId"], f["severity"], f["note"], f["raisedById"], f["createdAt"]))
synced_ids.add(f["id"])
if not body.get("nextCursor"): break
cursor = body["nextCursor"]
# Delete rows that are no longer OPEN in DirtFleet
if synced_ids:
cur.execute(
"DELETE FROM df_open_flags WHERE id != ALL(%s)",
(list(synced_ids),),
)
print(f"Synced {len(synced_ids)} open flags")Recipe 4
Slack alert on URGENT work-order creation
Receiver-side: when DirtFleet POSTs a workorder.created with priority URGENT, message the on-call Slack channel. Signature verification included.
#!/usr/bin/env node
// node 20+ — set DIRTFLEET_WEBHOOK_SECRET + SLACK_WEBHOOK_URL
import express from "express";
import { createHmac, timingSafeEqual } from "node:crypto";
const SECRET = process.env.DIRTFLEET_WEBHOOK_SECRET;
const SLACK_HOOK = process.env.SLACK_WEBHOOK_URL;
if (!SECRET || !SLACK_HOOK) throw new Error("Set both env vars");
const app = express();
app.post(
"/dirtfleet",
express.raw({ type: "application/json", limit: "256kb" }),
async (req, res) => {
const sig = req.header("x-fleetgo-signature") ?? "";
const ts = Number(req.header("x-fleetgo-timestamp") ?? 0);
if (Math.abs(Math.floor(Date.now() / 1000) - ts) > 300)
return res.status(401).end();
const raw = (req.body).toString("utf8");
const expected =
"v1=" + createHmac("sha256", SECRET).update(`${ts}.${raw}`).digest("hex");
if (
expected.length !== sig.length ||
!timingSafeEqual(Buffer.from(expected), Buffer.from(sig))
)
return res.status(401).end();
const env = JSON.parse(raw);
if (env.event === "workorder.created" && env.data?.priority === "URGENT") {
await fetch(SLACK_HOOK, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
text: `🚨 URGENT WO #${env.data.number}: ${env.data.title}`,
}),
});
}
res.status(200).end();
},
);
app.listen(3000, () => console.log("listening on :3000"));Recipe 5
Daily fleet-snapshot email
One-call summary email each morning: open RED flags, work orders due today, assets whose registration expires within 30 days. Pulls from three endpoints and renders one email.
#!/usr/bin/env node
// node 20+ — set DIRTFLEET_API_KEY + RESEND_API_KEY (or substitute your mailer)
const KEY = process.env.DIRTFLEET_API_KEY;
const RESEND = process.env.RESEND_API_KEY;
const TO = process.env.DAILY_EMAIL_TO ?? "fleet-ops@example.com";
if (!KEY || !RESEND) throw new Error("Set DIRTFLEET_API_KEY + RESEND_API_KEY");
async function df(path, params = {}) {
const url = new URL(`https://dirtfleet.app${path}`);
for (const [k, v] of Object.entries(params)) url.searchParams.set(k, v);
const r = await fetch(url, { headers: { Authorization: `Bearer ${KEY}` } });
if (!r.ok) throw new Error(`${path}: HTTP ${r.status}`);
return r.json();
}
const today = new Date().toISOString().slice(0, 10);
const [flags, urgentWO] = await Promise.all([
df("/api/v1/flags", { severity: "RED", limit: 100 }),
df("/api/v1/work-orders", { priority: "URGENT,HIGH", limit: 100 }),
]);
const body = `
<h2>DirtFleet daily — ${today}</h2>
<h3>Open RED flags (${flags.flags.length})</h3>
<ul>
${flags.flags.map((f) =>
`<li>${f.assetId} — ${(f.note ?? "").slice(0, 80)}</li>`,
).join("")}
</ul>
<h3>URGENT/HIGH work orders (${urgentWO.workOrders.length})</h3>
<ul>
${urgentWO.workOrders.map((w) =>
`<li>WO#${w.number} — ${w.title} (${w.priority})</li>`,
).join("")}
</ul>
`;
await fetch("https://api.resend.com/emails", {
method: "POST",
headers: {
Authorization: `Bearer ${RESEND}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
from: "DirtFleet Daily <daily@your-domain.example.com>",
to: TO,
subject: `DirtFleet daily — ${flags.flags.length} red, ${urgentWO.workOrders.length} urgent`,
html: body,
}),
});
console.log("Sent.");Recipe 6
Auto-reorder consumables on tool.low_stock
When a consumable tool crosses its reorder threshold, fire a POST to a supplier endpoint (Grainger / Fastenal / Amazon Business) with the SKU + quantity-to-order. Closes the loop from "welding rods just hit 3 of 10" to "new box ships in 24h" without anyone watching the dashboard. The receiver verifies the HMAC signature first (same pattern as the asset webhook receiver — see /docs/api/example).
// Express receiver — on tool.low_stock, POST to your supplier.
// Set DIRTFLEET_WEBHOOK_SECRET + SUPPLIER_URL in env.
import express from "express";
import { createHmac, timingSafeEqual } from "crypto";
const app = express();
app.use(express.raw({ type: "application/json" }));
const SECRET = process.env.DIRTFLEET_WEBHOOK_SECRET;
const SUPPLIER = process.env.SUPPLIER_URL;
const REPLAY_WINDOW_MS = 5 * 60 * 1000;
app.post("/webhooks/dirtfleet", async (req, res) => {
const sig = req.header("x-fleetgo-signature");
const ts = Number(req.header("x-fleetgo-timestamp"));
const event = req.header("x-fleetgo-event");
if (!sig || !ts || !event) return res.status(400).send();
if (Math.abs(Date.now() / 1000 - ts) * 1000 > REPLAY_WINDOW_MS) {
return res.status(401).send("stale");
}
const expected =
"v1=" +
createHmac("sha256", SECRET).update(`${ts}.${req.body}`).digest("hex");
if (!timingSafeEqual(Buffer.from(expected), Buffer.from(sig))) {
return res.status(401).send("bad sig");
}
if (event !== "tool.low_stock") return res.status(200).send("ignored");
const { data } = JSON.parse(req.body.toString("utf8"));
// Order back to 4× threshold so we don't ping-pong on the next use.
const reorderTo = (data.thresholdUsed ?? 5) * 4;
await fetch(SUPPLIER, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
tool_id: data.toolId,
current_qty: data.newStockLevel,
reorder_qty: reorderTo - data.newStockLevel,
}),
});
res.status(200).send("ok");
});
app.listen(3000);More
Where to go from here
- Full REST API reference — every endpoint with curl examples.
- Webhook docs + AsyncAPI spec at /asyncapi.yaml.
- SDK codegen guide — typed clients in TS / Go / Python / Ruby.
- Zapier / Slack — no-code and low-code paths for common destinations.
Have a recipe you wish was here? Email support@dirtfleet.app — common asks get added to this page.