Neutral Fuels Transactions API

Customer Transactions API

Secure, read-only access to your Neutral Fuels fuelling transactions - built for server-to-server integration with your fleet, accounting, and analytics systems.

API v1 · Stable Base URL https://myportal-api.neutralfuels.net API key required
Introduction

Overview

The Neutral Fuels Customer Transactions API gives you programmatic, read-only access to your own fuel-card transaction data. It is designed for backend integrations - ETL pipelines, dashboards, fleet/telematics platforms, and accounting reconciliation.

ProtocolHTTPS only (TLS 1.2+)
Base URLhttps://myportal-api.neutralfuels.net
AuthenticationAPI key in the X-API-Key request header
FormatJSON (application/json)
MethodsGET only - the API is read-only
ScopeYour key returns only your account's data

Endpoints at a glance

MethodEndpointPurpose
GET/api/v1/external/transactionsList your fuelling transactions for a date range
GET/api/v1/external/healthVerify your key and check remaining quota
Security

Authentication

Every request must include your API key in the X-API-Key header:

HTTP header
X-API-Key: nfk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
  • Your key is issued by Neutral Fuels and is tied to one account. There is no parameter to request another account's data - the account is derived from the key.
  • Keys begin with the prefix nfk_. Treat your key like a password.
  • A missing, unknown, disabled, or expired key returns 401 with body {"detail":"Invalid API key"}. Disabling or rotating a key takes effect immediately.

Authentication is by API key only. Neutral Fuels web-portal logins are not accepted here, and your API key works only on the /api/v1/external/* endpoints documented on this page.

Get going in 60 seconds

Quickstart

Confirm your key and see your quota:

cURL
curl -H "X-API-Key: nfk_YOUR_KEY" \
  "https://myportal-api.neutralfuels.net/api/v1/external/health"
Response
{
  "mcs_customer_id": 42,
  "customer_name": "Your Company",
  "usage": { "day": { "used": 1, "limit": 30 }, "minute": { "used": 1, "limit": 5 } }
}

Then pull a month of transactions:

cURL
curl -H "X-API-Key: nfk_YOUR_KEY" \
  "https://myportal-api.neutralfuels.net/api/v1/external/transactions?start_date=2026-05-01&end_date=2026-05-31&limit=100"
Ground rules

Conventions

  • Dates (start_date, end_date) use the format YYYY-MM-DD - e.g. 2026-05-31.
  • Timestamps in responses are ISO 8601 with a timezone offset, in your account's local timezone - e.g. 2026-06-07T20:10:00+04:00. The timezone is also returned as meta.timezone (e.g. Asia/Dubai).
  • The date range is inclusive of the whole end day - end_date=2026-05-31 includes transactions up to 23:59:59 local time on 31 May.
  • All endpoints are GET; parameters are passed in the query string. Responses are JSON; success is HTTP 200.
Endpoint

List transactions

GET/api/v1/external/transactions

Returns your fuelling transactions within a date range, most recent first.

Query parameters

ParameterTypeDefaultNotes
start_datestringRequired-Start of range (inclusive). YYYY-MM-DD.
end_datestringRequired-End of range (inclusive of the full day). YYYY-MM-DD.
limitintegerOptional500Page size. Min 1, max 1000.
offsetintegerOptional0Records to skip (for paging). Min 0.

The maximum span between start_date and end_date is 366 days; larger ranges return 400. Results are ordered most-recent-first with a stable secondary order, so paging never skips or duplicates rows.

Example request

curl -H "X-API-Key: nfk_YOUR_KEY" \
  "https://myportal-api.neutralfuels.net/api/v1/external/transactions?start_date=2026-05-01&end_date=2026-05-31&limit=500&offset=0"
import requests

r = requests.get(
    "https://myportal-api.neutralfuels.net/api/v1/external/transactions",
    headers={"X-API-Key": "nfk_YOUR_KEY"},
    params={"start_date": "2026-05-01", "end_date": "2026-05-31", "limit": 500},
    timeout=30,
)
r.raise_for_status()
data = r.json()
print(data["meta"]["total"], "transactions")
const url = new URL("https://myportal-api.neutralfuels.net/api/v1/external/transactions");
url.search = new URLSearchParams({ start_date: "2026-05-01", end_date: "2026-05-31", limit: "500" });

const res = await fetch(url, { headers: { "X-API-Key": "nfk_YOUR_KEY" } });
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
console.log(data.meta.total, "transactions");

Example response 200

JSON
{
  "data": [
    {
      "transaction_id": 208766590,
      "transaction_datetime": "2026-06-07T20:10:00+04:00",
      "vehicle_no": "93705",
      "vehicle_registration": null,
      "quantity": 120.88,
      "odometer": 1229986.0,
      "odometer_unit_of_measure": null,
      "product_name": "B20",
      "site_name": "Main Depot",
      "pump_no": 1
    }
  ],
  "meta": {
    "count": 1,
    "total": 1843,
    "limit": 1,
    "offset": 0,
    "timezone": "Asia/Dubai",
    "start_date": "2026-05-01",
    "end_date": "2026-05-31"
  }
}

Transaction fields

FieldTypeDescription
transaction_idinteger · nullUnique identifier. May be null for some historically imported records - use it for de-duplication when present (see Polling).
transaction_datetimestringWhen the fuelling occurred, ISO 8601 in your local timezone (with offset).
vehicle_nostring · nullVehicle / unit number on the fuel card.
vehicle_registrationstring · nullVehicle registration / plate, when available.
quantitynumber · nullFuel quantity dispensed, as recorded by the terminal (typically litres).
odometernumber · nullOdometer reading captured at fuelling, when available.
odometer_unit_of_measurestring · nullUnit for odometer (e.g. km), when recorded.
product_namestring · nullFuel product / grade (e.g. B20).
site_namestring · nullSite / depot / terminal where fuelling occurred.
pump_nointeger · nullPump number at the site.

Meta fields

FieldTypeDescription
countintegerRecords returned in this response (this page).
totalintegerTotal records matching the query across all pages.
limitintegerPage size applied.
offsetintegerOffset applied.
timezonestringIANA timezone the timestamps are expressed in.
start_date / end_datestringEcho of the requested range.
Endpoint

Health check

GET/api/v1/external/health

A lightweight call to confirm your key is valid and read your current usage. Useful for connectivity checks and monitoring remaining quota.

Example response 200

JSON
{
  "mcs_customer_id": 42,
  "customer_name": "Your Company",
  "usage": {
    "day":    { "used": 3, "limit": 30 },
    "minute": { "used": 1, "limit": 5 }
  }
}

This endpoint counts toward your rate limit like any other request.

Working with results

Pagination

Responses are paged with limit and offset. Use meta.total to know how many records match, and page until you have them all:

  1. Request with offset=0 and your chosen limit (e.g. 500).
  2. Read meta.total.
  3. Increase offset by limit each call until offset ≥ meta.total (or a page returns fewer than limit records).

Each page is a separate request and counts toward your rate limit, so prefer a larger limit (up to 1000) to reduce the number of calls.

Keeping your data in sync

To pick up new transactions over time, re-fetch a recent date window (for example the last 7 days) on a schedule, and reconcile by transaction_id (insert new ids, update existing ones).

Do not rely on a "give me everything since timestamp T" cursor. Some records are added shortly after fuelling and can be back-filled out of strict time order, so a high-water-mark cursor can miss late-arriving rows. Re-fetching a recent window and de-duplicating by transaction_id is the reliable pattern.

Fair use

Rate limits

Each API key has two limits:

  • A daily limit (default 30 requests/day, resets at 00:00 UTC).
  • A per-minute burst limit (default 5 requests/minute).

Your key's specific limits may differ from the defaults - read them from the response headers below or from the health endpoint.

Every response includes these headers:

HeaderMeaning
X-RateLimit-Limit-DayYour daily request allowance.
X-RateLimit-Remaining-DayRequests remaining in the current day.
X-RateLimit-Reset-DaySeconds until the daily window resets.
X-RateLimit-Limit-MinuteYour per-minute allowance.
X-RateLimit-Remaining-MinuteRequests remaining in the current minute.
X-RateLimit-Reset-MinuteSeconds until the minute window resets.

When a limit is exceeded the API returns 429 with a Retry-After header (seconds to wait) and body {"detail":"Rate limit exceeded"}. Honour Retry-After and design jobs to stay within your daily quota - fetch each date range once with a large limit rather than polling frequently.

When things go wrong

Errors

StatusMeaningWhen
200OKRequest succeeded.
400Bad RequestDates not YYYY-MM-DD, or the range exceeds 366 days.
401UnauthorizedX-API-Key missing, unknown, disabled, or expired.
422Unprocessable EntityA query parameter is out of range (e.g. limit > 1000 or < 1).
429Too Many RequestsDaily or per-minute limit hit. See Retry-After.
5xxServer ErrorTransient - retry with exponential backoff.

Error responses are JSON with a detail field:

JSON
{ "detail": "date window must be <= 366 days" }
Copy & paste

Code examples

A complete, production-ready fetch that handles paging and rate limits.

# pip install requests
import time, requests

BASE = "https://myportal-api.neutralfuels.net"
API_KEY = "nfk_YOUR_KEY"   # load from a secret / env var in production

def fetch_transactions(start_date, end_date, page_size=1000):
    """Yield every transaction in the range, handling paging + 429s."""
    s = requests.Session()
    s.headers["X-API-Key"] = API_KEY
    offset = 0
    while True:
        r = s.get(f"{BASE}/api/v1/external/transactions", timeout=30, params={
            "start_date": start_date, "end_date": end_date,
            "limit": page_size, "offset": offset,
        })
        if r.status_code == 429:
            time.sleep(int(r.headers.get("Retry-After", "60")))
            continue
        r.raise_for_status()
        body = r.json()
        for row in body["data"]:
            yield row
        offset += body["meta"]["limit"]
        if offset >= body["meta"]["total"]:
            break

rows = list(fetch_transactions("2026-05-01", "2026-05-31"))
print(f"Fetched {len(rows)} transactions")
// Node 18+ has built-in fetch (else: npm install node-fetch)
const BASE = "https://myportal-api.neutralfuels.net";
const API_KEY = process.env.NF_API_KEY;   // keep the key out of source control

async function fetchTransactions(startDate, endDate, pageSize = 1000) {
  const all = [];
  let offset = 0;
  while (true) {
    const url = new URL(`${BASE}/api/v1/external/transactions`);
    url.search = new URLSearchParams({
      start_date: startDate, end_date: endDate,
      limit: String(pageSize), offset: String(offset),
    });
    const res = await fetch(url, { headers: { "X-API-Key": API_KEY } });
    if (res.status === 429) {
      await new Promise(r => setTimeout(r, (+res.headers.get("Retry-After") || 60) * 1000));
      continue;
    }
    if (!res.ok) throw new Error(`HTTP ${res.status}`);
    const body = await res.json();
    all.push(...body.data);
    offset += body.meta.limit;
    if (offset >= body.meta.total) break;
  }
  return all;
}

const rows = await fetchTransactions("2026-05-01", "2026-05-31");
console.log(`Fetched ${rows.length} transactions`);
Recommendations

Best practices & security

  • Keep your key secret. Store it in a secrets manager or environment variable - never in client-side code, mobile apps, browsers, public repositories, or shared documents.
  • Use it server-side only. This API is for backend integrations.
  • Always use HTTPS. Never send the key over plain HTTP.
  • Handle 429 gracefully by honouring Retry-After, and schedule jobs to stay within your daily quota.
  • Reconcile by transaction_id so re-fetching a window is idempotent.
  • If a key is exposed or no longer needed, contact Neutral Fuels to revoke and reissue it - revocation is immediate.
We're here to help

Support

For API keys, quota changes, or integration help, contact your Neutral Fuels account manager or email us:

oshada.palitharathna@neutralfuels.com