Skip to content

Costs and your ERP

Most integrations with Fleet exist for one reason: fleet costs have to end up in the ERP. Fuel, maintenance, tyres, insurance and licences, tolls and fines, all coded to the right accounts, departments and projects, without double entry and without double counting. This page explains the model; the Odoo and Business Central recipes put it to work.

Fleet holds the codes. The ERP holds the accounting rules.

  • Fleet records what happened: which vehicle, which supplier, how many litres, which parts, which department or project it was for, the VAT on the receipt and how it was paid.
  • Your ERP decides how to book it: which G/L account, which tax code, which journal, what the approval limits are.

So Fleet never stores G/L accounts as accounting truth. It stores dimension codes your ERP gives it, and external references that tie Fleet objects to ERP records. Your integration translates.

Request-first (Fleet requests) Record-first (cost ledger)
When Spend needs approval or a PO before it happens: services, repairs, tyres, parts The cost already happened: fuel-card transactions, receipts, claims, renewals
Flow Fleet request → approved in Fleet → your ERP raises the PR / PO / bill → ERP writes the outcome back Fleet records the cost → your ERP pulls it from the export queue and posts it
Actual cost The ERP bill (Fleet records it from your write-back) The Fleet record
Endpoints /v1/fleet_requests, …/financial_events, webhooks fleet_request.* /v1/cost_entries, /v1/cost_exports, webhooks cost_entry.*

Most organizations use both. Neither double counts: when your ERP bill for a fleet request arrives, the ledger entries for the linked work order are marked superseded and the bill becomes the cost.

Every cost in Fleet appears once in /v1/cost_entries, as ERP-ready lines:

{
"id": "6f1c2b0e-8a43-4c1e-9d55-0c2b7f3e9a10",
"object": "cost_entry",
"status": "posted",
"revision": 1,
"category": "fuel",
"subcategory": "diesel",
"incurred_on": "2026-10-01",
"posting_date": "2026-10-01",
"amount": "7600.00",
"currency": "KES",
"tax": { "amount": "1048.28", "rate": 16 },
"quantity": "40", "unit": "litre", "unit_price": "190.00",
"vehicle_id": "…", "branch_id": "…", "vendor_id": "…",
"settlement": "fuel_card",
"payee": { "type": "vendor", "id": "…" },
"references": { "receipt_number": "R-55102", "tax_invoice_number": "KRAMW004202610010123" },
"dimensions": { "department": "OPS", "project": "P-104" },
"source": { "type": "fuel_log", "id": "…", "origin": "api", "url": "/v1/fuel_logs/…" }
}
  • Categories are fuel, ev_charging, maintenance (one line each for parts, labour and service on a completed work order), tires, renewals and expenses. Totals match Fleet’s Operating Cost Summary report, so what you post reconciles with what fleet managers see.
  • Settlement says who is owed: vendor_bill (a payable), employee_reimbursement (an approved claim), prepaid_card (an allocation against a prepaid card account, not a new payable), fuel_card, cash, stock_issue (parts from inventory), internal_labor, financing (lease or loan payments).
  • Dimensions combine branch defaults, vehicle defaults and the record’s own codes, most specific last.
  • Entries are never deleted. A change raises revision. A deleted or no-longer-qualifying record becomes void; an ERP bill replacing a work order makes it superseded. Reverse what you posted in both cases.

GET /v1/cost_exports?system=business_central is a queue of what your ERP has not seen in its current form:

  • pending_new: never exported. Post it.
  • pending_update: changed since you exported it. last_export holds what you posted; post the difference or reverse and re-post.
  • pending_void: you exported it and it has since been voided or superseded. Reverse it.

After posting, acknowledge with the revision you posted and your document id:

Terminal window
curl https://api.get-fleet.com/v1/cost_exports \
-H "Authorization: Bearer $FLEET_API_KEY" -H "Content-Type: application/json" \
-d '{"system": "business_central", "entries": [{"id": "6f1c…", "revision": 1, "external_id": "PI-10442"}]}'

If the entry changed while you were posting it, the acknowledgement is still recorded for the revision you posted and comes back stale: the entry stays in the queue as pending_update (or pending_void) with last_export showing what you posted. Nothing is lost between reading and acknowledging, and nothing is posted twice. Each ERP has its own queue; records your ERP itself created through the API (they carry its external reference) are never offered back to it.

Set these up once, from your ERP, with accounting:write:

  1. Dimensions and their values. Create a dimension per ERP dimension (department, project, cost centre, donor, activity…), then push the value list with PUT /v1/accounting_dimensions/{key}/values. Send deactivate_missing: true to make it a full sync. Values can have a parent (sub-activity under activity) and validity dates. Fleet only accepts active codes on records.

  2. Default codes on branches (PUT /v1/branches/{id}/dimensions) and vehicles (dimensions on the vehicle), so people rarely have to pick them.

  3. External references for the objects your ERP needs ids for: vehicles (asset or analytic account), vendors (partner / vendor no.), users (employee), expense types, and cost categories (maintenance.parts, fuel, renewals.insurance…), whose metadata can carry the account and tax code your integration should use:

    PUT /v1/external_references
    { "data": [
    { "object_type": "cost_category", "object_key": "fuel", "system": "odoo", "external_id": "611000",
    "metadata": { "account": "611000", "tax": "VAT 16% (purchases)" } },
    { "object_type": "vehicle", "object_key": "6f1c…", "system": "odoo", "external_id": "77" }
    ] }

    Objects then carry external_ids ({"odoo": "77"}), and every list takes external_id=odoo:77.

GET /v1/fleet_requests/{id}/readiness?system=odoo runs these checks for one request before anyone approves it: missing amounts or categories, required dimensions, and unmapped vehicles, suppliers or categories.

Fuel logs, expenses and EV charging sessions carry tax_amount, tax_rate, tax_invoice_number (for example the KRA eTIMS invoice number), supplier_invoice_number, payment_method and payment_reference. They flow into the ledger as tax and references, so input VAT can be claimed from the ERP side.

  • Soft period close. When your ERP closes a period, set closed_through with PATCH /v1/accounting_settings. A cost dated in a closed period that changes later keeps its incurred_on but gets a posting_date in the first open day, so you never have to reopen a closed period. Post by posting_date.
  • Currencies are explicit. Each entry is in its transaction currency (a fuel log at a Ugandan branch is in UGX). GET /v1/cost_summaries converts with Fleet’s monthly rates and, where a rate is missing, says so (missing_fx: true) instead of guessing.

If the ERP is where a cost is first captured (a supplier invoice for tyres, an insurance renewal), create it in Fleet so vehicle cost reports are complete: POST /v1/expenses, /v1/vehicle_renewals, /v1/fuel_logs or /v1/work_orders. Send external_system and external_id: re-sending the same pair returns the existing record instead of a duplicate, and the ledger knows not to export it back to you.