PumpLine API Reference

Base URL: https://api.pumpline.lk/v1 Version: 0.1.0

Public REST API for fuel station data in Sri Lanka. Provides real-time congestion, status, fuel availability, and payment method information using an event-sourced dual-authority model (operators + community reports).


Getting Started

How to Request API Credentials

PumpLine API access is managed by an admin. To get started:

  1. Contact the PumpLine team — email api@pumpline.lk with your app name and use case.
  2. An admin creates your API key via the admin panel and sends you the key (format: pk_...).
  3. The key is shown only once — store it securely. If lost, a new key must be issued.
  4. Include the key in every request via the X-API-Key header.

For station operators: 1. An admin creates your operator account and assigns your station(s). 2. You receive an email + temporary password. 3. Login via POST /v1/auth/login to get JWT tokens. 4. Use the access token to update your station data.


Authentication

API Key (Public Consumers)

All public-facing endpoints require an API key passed via the X-API-Key header.

X-API-Key: pk_your_api_key_here

JWT Bearer Token (Operators & Admins)

Operator and admin endpoints require a JWT access token obtained via the login endpoint.

Authorization: Bearer <access_token>

Access tokens expire after 30 minutes. Use the refresh token to get a new one without logging in again.


Rate Limits

All API requests are subject to rate limiting based on your API key tier.

Tiers

Tier Requests/min Daily Limit Use Case
Free 60 5,000 Hobby projects, prototyping
Basic 300 50,000 Production apps
Pro 1,000 Unlimited High-traffic applications

Additional Endpoint Limits

Endpoint Limit Scope
GET /v1/stations (search) 10/min Per API key
POST /.../reports/* 30/min Per IP address
PUT /v1/operator/stations/* 30/min Per JWT user
All endpoints (no API key) 120/min Per IP address

Response Headers

Every response includes rate limit headers:

X-RateLimit-Limit: 60              # Per-minute limit
X-RateLimit-Daily-Limit: 5000      # Daily limit (or "unlimited")
X-RateLimit-Daily-Remaining: 4832  # Remaining daily requests
X-RateLimit-Daily-Used: 168        # Requests used today

429 Too Many Requests

When you exceed a rate limit, you'll receive:

{
  "error": "rate_limit_exceeded",
  "detail": "Rate limit exceeded: 60 per 1 minute"
}

Or for daily cap exceeded:

{
  "error": "daily_limit_exceeded",
  "detail": "Daily API limit of 5000 requests exceeded. Resets at midnight UTC.",
  "limit": 5000,
  "used": 5000,
  "reset_in_seconds": 14400
}

The Retry-After header indicates how many seconds to wait before retrying.


How Station IDs Work

PumpLine uses Google Places IDs as station identifiers. These are stable, unique strings assigned by Google to every place (e.g., ChIJN1t_tDeuEmsRUsoyG83frY4).

The Flow

User searches by location
    → PumpLine calls Google Places Nearby Search
    → Google returns gas stations with place IDs
    → PumpLine uses the place ID as station_id
    → All reports, state, and metadata are linked to that ID

Why Google Place IDs?

How to get a station_id

You don't create them — they come from the GET /v1/stations search endpoint. Every station in the response includes its station_id, which you then use for all other endpoints.


Enums & Types

Type Values
CongestionLevel none, low, medium, high
DataSource operator, community, none
FuelStatus available, low, out_of_stock
FuelType petrol_92, petrol_95, diesel, super_diesel, kerosene
StationProvider lanka_ioc, ceypetco, sinopec, other
ReportCategory congestion, fuel_availability, status, payment_methods, fuel_pricing
UserRole admin, operator

Fuel type validation: Any endpoint that accepts fuel data will reject unknown fuel type keys with a 422 error listing the invalid types and the valid options.


Endpoints

Health

GET /v1/health

Health check endpoint. No authentication required.

When to use: Monitoring, uptime checks, load balancer health probes.

Response 200 OK

{
  "status": "ok"
}

Auth

POST /v1/auth/login

Authenticate a user (operator or admin) and receive JWT tokens.

When to use: Operator app login screen. Call this once, then store the tokens. Use the access token for authenticated requests and the refresh token to get new access tokens when they expire.

Request Body

{
  "email": "operator@example.com",
  "password": "your-password"
}

Response 200 OK

{
  "access_token": "eyJhbGciOiJIUzI1NiIs...",
  "refresh_token": "eyJhbGciOiJIUzI1NiIs...",
  "token_type": "bearer"
}

Errors | Status | Detail | |--------|--------| | 401 | Invalid credentials | | 403 | Account not verified |

App implementation:

// Login and store tokens
const { access_token, refresh_token } = await api.post('/v1/auth/login', {
  email, password
});
localStorage.setItem('access_token', access_token);
localStorage.setItem('refresh_token', refresh_token);

// Use access token for authenticated requests
api.defaults.headers['Authorization'] = `Bearer ${access_token}`;

POST /v1/auth/refresh

Refresh an expired access token using a valid refresh token.

When to use: When an API call returns 401, call this with the stored refresh token to get a new access token without making the user log in again.

Request Body

{
  "refresh_token": "eyJhbGciOiJIUzI1NiIs..."
}

Response 200 OK

{
  "access_token": "eyJhbGciOiJIUzI1NiIs...",
  "refresh_token": "eyJhbGciOiJIUzI1NiIs...",
  "token_type": "bearer"
}

App implementation:

// Axios interceptor to auto-refresh on 401
api.interceptors.response.use(null, async (error) => {
  if (error.response?.status === 401) {
    const { access_token } = await api.post('/v1/auth/refresh', {
      refresh_token: localStorage.getItem('refresh_token')
    });
    localStorage.setItem('access_token', access_token);
    error.config.headers['Authorization'] = `Bearer ${access_token}`;
    return api.request(error.config); // Retry original request
  }
  return Promise.reject(error);
});

Stations

GET /v1/stations

Discover nearby fuel stations by location. Calls Google Places API behind the scenes, caches results for 5 minutes, and enriches each station with its current PumpLine state (congestion, fuels, status).

Auth: API Key

When to use: Map view — show nearby stations with pins, distance, and quick status overview. This is typically the first call your app makes after getting the user's location.

Query Parameters | Parameter | Type | Required | Default | Description | |-----------|------|----------|---------|-------------| | lat | float | Yes | — | Latitude (-90 to 90) | | lng | float | Yes | — | Longitude (-180 to 180) | | radius | int | No | 5000 | Search radius in meters (1000–20000) |

Response 200 OK

{
  "data": [
    {
      "station_id": "ChIJN1t_tDeuEmsRUsoyG83frY4",
      "name": "Lanka IOC - Colombo 07",
      "address": "123 Galle Road, Colombo 07",
      "lat": 6.9271,
      "lng": 79.8612,
      "distance_m": 245.3,
      "congestion": "low",
      "congestion_source": "operator",
      "is_open": true,
      "fuels": {
        "petrol_92": "available",
        "petrol_95": "low",
        "diesel": "available"
      },
      "payment_methods": ["cash", "visa", "mastercard"],
      "last_updated": "2026-03-22T10:30:00Z"
    }
  ],
  "meta": {
    "timestamp": "2026-03-22T10:35:00Z",
    "count": 12,
    "radius": 5000
  }
}

App implementation:

// Get user's location, then fetch nearby stations
const { coords } = await navigator.geolocation.getCurrentPosition();
const response = await api.get('/v1/stations', {
  params: { lat: coords.latitude, lng: coords.longitude, radius: 5000 },
  headers: { 'X-API-Key': API_KEY }
});

// Render stations on a map
response.data.data.forEach(station => {
  addMapPin(station.lat, station.lng, {
    title: station.name,
    distance: `${station.distance_m}m`,
    congestion: station.congestion,
    isOpen: station.is_open,
  });
});

GET /v1/stations/{station_id}

Get full details about a specific station, including data source attribution for every field.

Auth: API Key

When to use: Station detail page — when a user taps a station on the map to see full info. Shows everything including operating hours, per-fuel source attribution, and last update timestamps.

Path Parameters | Parameter | Type | Description | |-----------|------|-------------| | station_id | string | Google Places ID (from the stations list) |

Response 200 OK

{
  "data": {
    "station_id": "ChIJN1t_tDeuEmsRUsoyG83frY4",
    "name": "Lanka IOC - Colombo 07",
    "address": "123 Galle Road, Colombo 07",
    "lat": 6.9271,
    "lng": 79.8612,
    "phone": "+94 11 234 5678",
    "congestion": "low",
    "congestion_source": "operator",
    "is_open": true,
    "is_open_source": "operator",
    "operating_hours": {
      "monday": { "open": "06:00", "close": "22:00" },
      "tuesday": { "open": "06:00", "close": "22:00" },
      "sunday": null
    },
    "fuels": {
      "petrol_92": "available",
      "petrol_95": "low",
      "diesel": "available"
    },
    "fuels_source": {
      "petrol_92": "operator",
      "petrol_95": "community",
      "diesel": "operator"
    },
    "payment_methods": ["cash", "visa", "mastercard"],
    "payment_source": "operator",
    "last_operator_update": "2026-03-22T08:00:00Z",
    "last_community_update": "2026-03-22T10:15:00Z"
  },
  "meta": {
    "timestamp": "2026-03-22T10:35:00Z"
  }
}

Errors | Status | Detail | |--------|--------| | 401 | Invalid or missing API key | | 404 | Station not found |

App implementation:

// User taps a station from the map
const stationId = selectedStation.station_id;
const detail = await api.get(`/v1/stations/${stationId}`, {
  headers: { 'X-API-Key': API_KEY }
});

// Show source badges — "Reported by operator" vs "Community consensus"
const { congestion_source, fuels_source } = detail.data.data;
// congestion_source: "operator" → show verified badge
// fuels_source.petrol_95: "community" → show community badge

GET /v1/stations/{station_id}/congestion

Get just the current congestion level for a station.

Auth: API Key

When to use: Live congestion badge — poll every 30-60 seconds to show real-time queue length on a map pin or station card without fetching the entire station detail.

Response 200 OK

{
  "station_id": "ChIJN1t_tDeuEmsRUsoyG83frY4",
  "congestion": "high",
  "congestion_source": "community",
  "last_updated": "2026-03-22T10:30:00Z"
}

Errors | Status | Detail | |--------|--------| | 401 | Invalid or missing API key | | 404 | Station not found |

App implementation:

// Poll congestion every 30s for visible stations
const interval = setInterval(async () => {
  const { data } = await api.get(`/v1/stations/${stationId}/congestion`, {
    headers: { 'X-API-Key': API_KEY }
  });
  updateCongestionBadge(data.congestion); // "low" → green, "medium" → yellow, "high" → red
}, 30000);

GET /v1/stations/{station_id}/status

Get the current open/closed status and operating hours for a station.

Auth: API Key

When to use: Open/closed indicator — show a green "Open" or red "Closed" badge, plus today's operating hours.

Response 200 OK

{
  "station_id": "ChIJN1t_tDeuEmsRUsoyG83frY4",
  "is_open": true,
  "is_open_source": "operator",
  "operating_hours": {
    "monday": { "open": "06:00", "close": "22:00" },
    "tuesday": { "open": "06:00", "close": "22:00" },
    "wednesday": { "open": "06:00", "close": "22:00" },
    "thursday": { "open": "06:00", "close": "22:00" },
    "friday": { "open": "06:00", "close": "22:00" },
    "saturday": { "open": "07:00", "close": "20:00" },
    "sunday": null
  },
  "last_updated": "2026-03-22T08:00:00Z"
}

Errors | Status | Detail | |--------|--------| | 401 | Invalid or missing API key | | 404 | Station not found |

App implementation:

const { data } = await api.get(`/v1/stations/${stationId}/status`, {
  headers: { 'X-API-Key': API_KEY }
});

// Show open/closed badge
const badge = data.is_open ? '🟢 Open' : '🔴 Closed';

// Show today's hours
const today = ['sunday','monday','tuesday','wednesday','thursday','friday','saturday'][new Date().getDay()];
const hours = data.operating_hours?.[today];
const hoursText = hours ? `${hours.open} – ${hours.close}` : 'Closed today';

GET /v1/stations/{station_id}/fuels

Get current fuel availability for each fuel type at a station.

Auth: API Key

When to use: Fuel availability cards — show petrol 92, petrol 95, diesel, etc. with color-coded status (green = available, yellow = low, red = out of stock). Also shows whether each fuel's status came from the operator or community reports.

Response 200 OK

{
  "station_id": "ChIJN1t_tDeuEmsRUsoyG83frY4",
  "fuels": {
    "petrol_92": "available",
    "petrol_95": "low",
    "diesel": "out_of_stock"
  },
  "fuels_source": {
    "petrol_92": "operator",
    "petrol_95": "community",
    "diesel": "operator"
  },
  "last_updated": "2026-03-22T10:30:00Z"
}

Errors | Status | Detail | |--------|--------| | 401 | Invalid or missing API key | | 404 | Station not found |

App implementation:

const { data } = await api.get(`/v1/stations/${stationId}/fuels`, {
  headers: { 'X-API-Key': API_KEY }
});

// Render fuel cards with color coding
Object.entries(data.fuels).forEach(([fuel, status]) => {
  const color = { available: 'green', low: 'orange', out_of_stock: 'red' }[status];
  const source = data.fuels_source[fuel]; // "operator" or "community"
  renderFuelCard(fuel, status, color, source);
});

GET /v1/stations/{station_id}/fuel-prices

Get current fuel prices per litre (in LKR) for a station.

Auth: API Key

When to use: Price display — show current fuel prices alongside availability. Prices come from operators or community reports, with the same dual-authority override system.

Response 200 OK

{
  "station_id": "ChIJN1t_tDeuEmsRUsoyG83frY4",
  "fuel_prices": {
    "petrol_92": 365.00,
    "petrol_95": 420.00,
    "diesel": 340.00,
    "super_diesel": 385.00
  },
  "fuel_prices_source": {
    "petrol_92": "operator",
    "petrol_95": "operator",
    "diesel": "community",
    "super_diesel": "operator"
  },
  "last_updated": "2026-03-22T10:30:00Z"
}

Errors | Status | Detail | |--------|--------| | 401 | Invalid or missing API key | | 404 | Station not found |

App implementation:

const { data } = await api.get(`/v1/stations/${stationId}/fuel-prices`, {
  headers: { 'X-API-Key': API_KEY }
});

// Show prices alongside fuel cards
Object.entries(data.fuel_prices).forEach(([fuel, price]) => {
  const source = data.fuel_prices_source[fuel];
  renderPriceTag(fuel, `LKR ${price.toFixed(2)}`, source);
});

GET /v1/stations/{station_id}/availability

Get a full fuel availability breakdown showing both operator and community data side by side, along with the resolved (override-applied) values.

Auth: API Key

When to use: Detailed fuel availability view — when you want to show users not just the current status, but who reported it and how many community reports exist. Useful for building trust indicators ("3 users confirmed petrol 92 is out of stock") and showing when operator data might be stale.

Response 200 OK

{
  "data": {
    "station_id": "ChIJN1t_tDeuEmsRUsoyG83frY4",
    "fuels": {
      "petrol_92": "out_of_stock",
      "petrol_95": "low",
      "diesel": "available"
    },
    "fuels_source": {
      "petrol_92": "community",
      "petrol_95": "operator",
      "diesel": "operator"
    },
    "operator_fuels": {
      "petrol_92": "available",
      "petrol_95": "low",
      "diesel": "available"
    },
    "operator_updated_at": "2026-03-22T06:00:00Z",
    "community_fuels": {
      "petrol_92": "out_of_stock"
    },
    "community_report_count": 5,
    "community_updated_at": "2026-03-22T10:45:00Z",
    "last_updated": "2026-03-22T10:45:00Z"
  },
  "meta": {
    "timestamp": "2026-03-22T10:50:00Z"
  }
}

App implementation:

const { data } = await api.get(`/v1/stations/${stationId}/availability`, {
  headers: { 'X-API-Key': API_KEY }
});

const avail = data.data;

Object.entries(avail.fuels).forEach(([fuel, status]) => {
  const source = avail.fuels_source[fuel];
  const opStatus = avail.operator_fuels[fuel];
  const commStatus = avail.community_fuels[fuel];

  // Show resolved status with trust indicator
  if (source === 'community' && opStatus && opStatus !== status) {
    // Community overrode the operator
    showFuelCard(fuel, status, {
      badge: `${avail.community_report_count} users reported`,
      operatorSays: opStatus, // "Operator says: available"
    });
  } else {
    showFuelCard(fuel, status, { badge: 'Station confirmed' });
  }
});

Errors | Status | Detail | |--------|--------| | 401 | Invalid or missing API key | | 404 | Station not found |


GET /v1/stations/{station_id}/payment-methods

Get accepted payment methods for a station.

Auth: API Key

When to use: Payment info section — show icons for accepted payment methods (cash, Visa, Mastercard, etc.) so users know before they drive there.

Response 200 OK

{
  "station_id": "ChIJN1t_tDeuEmsRUsoyG83frY4",
  "payment_methods": ["cash", "visa", "mastercard", "dialog_pay"],
  "payment_source": "operator",
  "last_updated": "2026-03-22T08:00:00Z"
}

Errors | Status | Detail | |--------|--------| | 401 | Invalid or missing API key | | 404 | Station not found |

App implementation:

const { data } = await api.get(`/v1/stations/${stationId}/payment-methods`, {
  headers: { 'X-API-Key': API_KEY }
});

// Show payment method icons
data.payment_methods.forEach(method => {
  const icon = { cash: '💵', visa: '💳', mastercard: '💳', dialog_pay: '📱' }[method];
  renderPaymentBadge(method, icon);
});

GET /v1/stations/{station_id}/history

Get the event history for a station — all operator updates and community reports in reverse chronological order.

Auth: API Key

When to use: Activity feed — show recent reports and updates on the station detail page so users can see how fresh the data is and what's been changing.

Query Parameters | Parameter | Type | Required | Default | Description | |-----------|------|----------|---------|-------------| | limit | int | No | 50 | Number of events (1–200) | | offset | int | No | 0 | Pagination offset |

Response 200 OK

{
  "data": [
    {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "event_type": "community_report",
      "category": "congestion",
      "data": { "level": "high" },
      "created_at": "2026-03-22T10:30:00Z"
    },
    {
      "id": "550e8400-e29b-41d4-a716-446655440001",
      "event_type": "operator_update",
      "category": "fuel_availability",
      "data": { "fuels": { "petrol_92": "available" } },
      "created_at": "2026-03-22T08:00:00Z"
    }
  ],
  "meta": {
    "timestamp": "2026-03-22T10:35:00Z",
    "count": 2,
    "limit": 50,
    "offset": 0
  }
}

App implementation:

const { data } = await api.get(`/v1/stations/${stationId}/history`, {
  params: { limit: 20 },
  headers: { 'X-API-Key': API_KEY }
});

// Render activity feed
data.data.forEach(event => {
  const timeAgo = formatRelativeTime(event.created_at);
  const label = event.event_type === 'operator_update' ? '🏪 Station update' : '👤 Community report';
  renderFeedItem(`${label}: ${event.category} — ${timeAgo}`);
});

Errors | Status | Detail | |--------|--------| | 401 | Invalid or missing API key |


Community Reports

All community report endpoints require an API Key. Rate limited to 1 report per IP per station per category every 10 minutes. When 3+ community reports agree on a value that differs from the operator, the community consensus overrides.

When to use: "Report" buttons on the station detail page — e.g., "Report high congestion", "Report fuel out", "Report closed".

POST /v1/stations/{station_id}/reports (Combined)

Submit a community report using the combined endpoint. Accepts any category.

Request Body

The data field shape depends on the category:

{ "category": "congestion", "data": { "level": "high" } }
{ "category": "fuel_availability", "data": { "fuels": { "petrol_92": "out_of_stock" } } }
{ "category": "status", "data": { "is_open": false } }
{ "category": "payment_methods", "data": { "methods": ["cash", "visa"] } }

POST /v1/stations/{station_id}/reports/congestion

Report congestion level at a station.

Auth: API Key

Request Body

{
  "level": "high"
}

level: "low" | "medium" | "high"

Response 201 Created

{
  "data": { "success": true, "event_id": "550e8400-..." },
  "meta": { "timestamp": "2026-03-22T10:35:00Z" }
}

POST /v1/stations/{station_id}/reports/fuel-availability

Report fuel availability at a station.

Auth: API Key

Request Body

{
  "fuels": {
    "petrol_92": "out_of_stock",
    "diesel": "available"
  }
}

Fuel status: "available" | "low" | "out_of_stock"

Response 201 Created — same format as congestion report.


POST /v1/stations/{station_id}/reports/status

Report open/closed status of a station.

Auth: API Key

Request Body

{
  "is_open": false
}

Response 201 Created — same format as congestion report.


POST /v1/stations/{station_id}/reports/payment-methods

Report accepted payment methods at a station.

Auth: API Key

Request Body

{
  "methods": ["cash", "visa"]
}

Response 201 Created — same format as congestion report.


POST /v1/stations/{station_id}/reports/fuel-prices

Report fuel prices at a station (per litre in LKR).

Auth: API Key

Request Body

{
  "prices": {
    "petrol_92": 370.00,
    "diesel": 345.00
  }
}

Response 201 Created — same format as congestion report.

Community Report Errors | Status | Detail | |--------|--------| | 401 | Invalid or missing API key | | 422 | Invalid data for category | | 422 | Invalid fuel type(s) — must be one of: petrol_92, petrol_95, diesel, super_diesel, kerosene | | 429 | Rate limit exceeded — try again in 10 minutes |

App implementation:

// Individual endpoints (preferred — simpler, typed request bodies)
await api.post(`/v1/stations/${stationId}/reports/congestion`,
  { level: 'high' },
  { headers: { 'X-API-Key': API_KEY } }
);

await api.post(`/v1/stations/${stationId}/reports/fuel-availability`,
  { fuels: { petrol_95: 'out_of_stock' } },
  { headers: { 'X-API-Key': API_KEY } }
);

// Handle rate limit
try {
  await submitReport();
} catch (e) {
  if (e.response?.status === 429) {
    showToast('You already reported this recently. Try again in 10 minutes.');
  }
}

Operator Endpoints

All operator endpoints require JWT auth with operator or admin role. Operators can only update stations assigned to them. Admins can update any station.

When to use: Operator dashboard / station management app. Station owners use these to publish authoritative data that takes priority over community reports.

PUT /v1/operator/stations/{station_id}/congestion

Update congestion level for a station.

Auth: Bearer Token (operator/admin)

Request Body

{
  "level": "high"
}

level: "low" | "medium" | "high"

Response 200 OK

{
  "data": { "success": true, "event_id": "..." },
  "meta": { "timestamp": "..." }
}

PUT /v1/operator/stations/{station_id}/status

Update open/closed status and operating hours.

Auth: Bearer Token (operator/admin)

Request Body

{
  "is_open": true,
  "operating_hours": {
    "monday": { "open": "06:00", "close": "22:00" },
    "tuesday": { "open": "06:00", "close": "22:00" },
    "sunday": null
  }
}

All fields are optional. operating_hours maps day names to {"open": "HH:MM", "close": "HH:MM"} or null for closed days.

Response 200 OK — same format as congestion.


PUT /v1/operator/stations/{station_id}/fuels

Update fuel availability (simple — just fuel status map).

Auth: Bearer Token (operator/admin)

Request Body

{
  "fuels": {
    "petrol_92": "available",
    "petrol_95": "low",
    "diesel": "out_of_stock"
  }
}

Response 200 OK — same format as congestion.


PUT /v1/operator/stations/{station_id}/fuel-availability

Update fuel availability with optional notes.

Auth: Bearer Token (operator/admin)

Request Body

{
  "fuels": {
    "petrol_92": "available",
    "petrol_95": "low",
    "diesel": "out_of_stock"
  },
  "notes": "Diesel delivery expected at 2pm"
}

Response 200 OK — same format as congestion.


PUT /v1/operator/stations/{station_id}/payment-methods

Update accepted payment methods.

Auth: Bearer Token (operator/admin)

Request Body

{
  "methods": ["cash", "visa", "mastercard", "dialog_pay"]
}

Response 200 OK — same format as congestion.


PUT /v1/operator/stations/{station_id}/fuel-prices

Update fuel prices per litre (in LKR).

Auth: Bearer Token (operator/admin)

Request Body

{
  "prices": {
    "petrol_92": 365.00,
    "petrol_95": 420.00,
    "diesel": 340.00,
    "super_diesel": 385.00
  }
}

Response 200 OK — same format as congestion.

Operator Errors | Status | Detail | |--------|--------| | 401 | Invalid or expired Bearer token | | 403 | Not authorized for this station / Operator access required | | 422 | Invalid fuel type(s) — must be one of: petrol_92, petrol_95, diesel, super_diesel, kerosene |

App implementation (Operator Dashboard):

// Operator updates fuel availability for their station
await api.put(`/v1/operator/stations/${stationId}/fuels`, {
  fuels: {
    petrol_92: 'available',
    petrol_95: 'available',
    diesel: 'low'
  }
}, {
  headers: { 'Authorization': `Bearer ${accessToken}` }
});

// Operator opens/closes their station
await api.put(`/v1/operator/stations/${stationId}/status`, {
  is_open: false  // Station closed for the night
}, {
  headers: { 'Authorization': `Bearer ${accessToken}` }
});

Data Model

Dual-Authority Override System

Station data is maintained through an event-sourced model with two data sources:

Override rule: If 3 or more community reports agree on a value that differs from the operator's value within the 2-hour window, the community consensus overrides the operator data. The *_source fields in responses indicate which authority the current value came from ("operator", "community", or "none").

Example: An operator says congestion is "low", but 3 users report "high" within 2 hours. The API will return congestion: "high" with congestion_source: "community" until the reports expire or the operator posts a newer update.

Event Types

Event Type Source Categories
operator_update Authenticated operator All 4 categories
community_report Public API key holder All 4 categories

Categories

Category Operator Data Community Data
congestion Level (low/medium/high) Level (low/medium/high)
status is_open (bool), operating_hours is_open (bool)
fuel_availability Per-fuel status map Per-fuel status map
payment_methods Methods list Methods list
fuel_pricing Per-fuel price (LKR/litre) Per-fuel price (LKR/litre)

Data Expiry

Not all data expires at the same rate. Some fields are persistent settings, others are time-sensitive.

Source Category TTL Rationale
Operator congestion 24 hours Queue length changes throughout the day
Operator status (is_open) 24 hours Open/closed status changes daily
Operator operating_hours Never Business hours rarely change
Operator fuel_availability Never Operator fuel data persists until explicitly updated
Operator payment_methods Never Accepted payments rarely change
Operator fuel_pricing Never Operator prices persist until explicitly updated
Community All categories 2 hours Crowd-sourced data reflects current conditions

Why? An operator who sets their hours to "6am–10pm" or marks "petrol_92: available" shouldn't have that data vanish after 24 hours — those are persistent settings. But congestion and open/closed status are time-sensitive and should expire so stale data doesn't mislead users.


Typical App Integration

Mobile App (FuelCheck)

App opens
  → Get user location
  → GET /v1/stations?lat=...&lng=...&radius=5000
  → Render map with station pins

User taps station pin
  → GET /v1/stations/{id}  (full detail)
  → Show detail sheet with congestion, fuels, status, payments

User reports congestion
  → POST /v1/stations/{id}/reports { category: "congestion", data: { level: "high" } }

Background polling (every 30s)
  → GET /v1/stations/{id}/congestion  (lightweight, just congestion)
  → Update badge color on visible pins

Operator Dashboard

Operator logs in
  → POST /v1/auth/login

Update morning status
  → PUT /v1/operator/stations/{id}/status   { is_open: true }
  → PUT /v1/operator/stations/{id}/fuels    { fuels: { petrol_92: "available", ... } }
  → PUT /v1/operator/stations/{id}/congestion { level: "low" }

Token expires
  → POST /v1/auth/refresh  (auto-refresh via interceptor)

Error Responses

All errors follow this format:

{
  "detail": "Error description"
}

Common Status Codes

Code Meaning
200 Success
201 Created
204 No Content (success, no body)
401 Unauthorized — invalid/missing credentials or API key
403 Forbidden — insufficient permissions
404 Not Found — station doesn't exist in our database yet
409 Conflict (e.g., duplicate email)
422 Validation Error — invalid request body
429 Rate Limit Exceeded

Rate Limits

Endpoint Limit
Community reports 1 per IP per station per category per 10 minutes
API key (general) 60 requests/minute (configurable per key)

Interactive Documentation

When the server is running, visit: - Swagger UI: https://api.pumpline.lk/docs — shows all public and operator endpoints - ReDoc: https://api.pumpline.lk/redoc

Admin endpoints are hidden from public docs. See ADMIN-SETUP.md for admin API reference.

← Home