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.
| Protocol | HTTPS only (TLS 1.2+) |
| Base URL | https://myportal-api.neutralfuels.net |
| Authentication | API key in the X-API-Key request header |
| Format | JSON (application/json) |
| Methods | GET only - the API is read-only |
| Scope | Your key returns only your account's data |
Endpoints at a glance
| Method | Endpoint | Purpose |
|---|---|---|
| GET | /api/v1/external/transactions | List your fuelling transactions for a date range |
| GET | /api/v1/external/health | Verify your key and check remaining quota |
Authentication
Every request must include your API key in the X-API-Key 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.
Quickstart
Confirm your key and see your quota:
curl -H "X-API-Key: nfk_YOUR_KEY" \
"https://myportal-api.neutralfuels.net/api/v1/external/health"{
"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 -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"Conventions
- Dates (
start_date,end_date) use the formatYYYY-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 asmeta.timezone(e.g.Asia/Dubai). - The date range is inclusive of the whole end day -
end_date=2026-05-31includes 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.
List transactions
Returns your fuelling transactions within a date range, most recent first.
Query parameters
| Parameter | Type | Default | Notes | |
|---|---|---|---|---|
start_date | string | Required | - | Start of range (inclusive). YYYY-MM-DD. |
end_date | string | Required | - | End of range (inclusive of the full day). YYYY-MM-DD. |
limit | integer | Optional | 500 | Page size. Min 1, max 1000. |
offset | integer | Optional | 0 | Records 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
{
"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
| Field | Type | Description |
|---|---|---|
transaction_id | integer · null | Unique identifier. May be null for some historically imported records - use it for de-duplication when present (see Polling). |
transaction_datetime | string | When the fuelling occurred, ISO 8601 in your local timezone (with offset). |
vehicle_no | string · null | Vehicle / unit number on the fuel card. |
vehicle_registration | string · null | Vehicle registration / plate, when available. |
quantity | number · null | Fuel quantity dispensed, as recorded by the terminal (typically litres). |
odometer | number · null | Odometer reading captured at fuelling, when available. |
odometer_unit_of_measure | string · null | Unit for odometer (e.g. km), when recorded. |
product_name | string · null | Fuel product / grade (e.g. B20). |
site_name | string · null | Site / depot / terminal where fuelling occurred. |
pump_no | integer · null | Pump number at the site. |
Meta fields
| Field | Type | Description |
|---|---|---|
count | integer | Records returned in this response (this page). |
total | integer | Total records matching the query across all pages. |
limit | integer | Page size applied. |
offset | integer | Offset applied. |
timezone | string | IANA timezone the timestamps are expressed in. |
start_date / end_date | string | Echo of the requested range. |
Health check
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
{
"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.
Pagination
Responses are paged with limit and offset. Use meta.total to know how many records match, and page until you have them all:
- Request with
offset=0and your chosenlimit(e.g. 500). - Read
meta.total. - Increase
offsetbylimiteach call untiloffset ≥ meta.total(or a page returns fewer thanlimitrecords).
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.
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:
| Header | Meaning |
|---|---|
X-RateLimit-Limit-Day | Your daily request allowance. |
X-RateLimit-Remaining-Day | Requests remaining in the current day. |
X-RateLimit-Reset-Day | Seconds until the daily window resets. |
X-RateLimit-Limit-Minute | Your per-minute allowance. |
X-RateLimit-Remaining-Minute | Requests remaining in the current minute. |
X-RateLimit-Reset-Minute | Seconds 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.
Errors
| Status | Meaning | When |
|---|---|---|
| 200 | OK | Request succeeded. |
| 400 | Bad Request | Dates not YYYY-MM-DD, or the range exceeds 366 days. |
| 401 | Unauthorized | X-API-Key missing, unknown, disabled, or expired. |
| 422 | Unprocessable Entity | A query parameter is out of range (e.g. limit > 1000 or < 1). |
| 429 | Too Many Requests | Daily or per-minute limit hit. See Retry-After. |
| 5xx | Server Error | Transient - retry with exponential backoff. |
Error responses are JSON with a detail field:
{ "detail": "date window must be <= 366 days" }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`);
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_idso 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.
Support
For API keys, quota changes, or integration help, contact your Neutral Fuels account manager or email us: