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).
PumpLine API access is managed by an admin. To get started:
api@pumpline.lk with your app name and use case.pk_...).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.
All public-facing endpoints require an API key passed via the X-API-Key header.
X-API-Key: pk_your_api_key_here
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.
All API requests are subject to rate limiting based on your API key tier.
| 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 |
| 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 |
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
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.
PumpLine uses Google Places IDs as station identifiers. These are stable, unique strings assigned by Google to every place (e.g., ChIJN1t_tDeuEmsRUsoyG83frY4).
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
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.
| 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
422error listing the invalid types and the valid options.
GET /v1/healthHealth check endpoint. No authentication required.
When to use: Monitoring, uptime checks, load balancer health probes.
Response 200 OK
{
"status": "ok"
}
POST /v1/auth/loginAuthenticate 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/refreshRefresh 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);
});
GET /v1/stationsDiscover 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}/congestionGet 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}/statusGet 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}/fuelsGet 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-pricesGet 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}/availabilityGet 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-methodsGet 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}/historyGet 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 |
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/congestionReport 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-availabilityReport 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/statusReport 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-methodsReport 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-pricesReport 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.');
}
}
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}/congestionUpdate 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}/statusUpdate 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}/fuelsUpdate 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-availabilityUpdate 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-methodsUpdate 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-pricesUpdate 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}` }
});
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 Type | Source | Categories |
|---|---|---|
operator_update |
Authenticated operator | All 4 categories |
community_report |
Public API key holder | All 4 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) |
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.
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 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)
All errors follow this format:
{
"detail": "Error description"
}
| 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 |
| Endpoint | Limit |
|---|---|
| Community reports | 1 per IP per station per category per 10 minutes |
| API key (general) | 60 requests/minute (configurable per key) |
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.