Files
fuel-price/docs/api-reference.md
Ovidiu U 1860cf0a49
Some checks failed
linter / quality (push) Has been cancelled
tests / ci (8.3) (push) Has been cancelled
tests / ci (8.4) (push) Has been cancelled
tests / ci (8.5) (push) Has been cancelled
feat: add API key authentication and update tests
Adds `VerifyApiKey` middleware protecting all API routes with `X-Api-Key` header validation. Wraps `/api/stations`, `/api/stats/searches`, and `/api/prediction` in throttled middleware group (60 req/min). Updates StationSearchTest to use `RefreshDatabase`, adds `meta` assertion checks, and validates `fuel_type` in HTTP request assertions. Removes auth routes from API docs and replaces with API key authentication instructions. Adds `api_secret_key` config option.
2026-04-05 20:27:41 +01:00

283 lines
7.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# FuelAlert API Reference
Base URL: `https://fuel-price.test/api`
All endpoints return JSON. All endpoints require an `X-Api-Key` header.
**Authentication:**
```
X-Api-Key: your-secret-key
```
All requests without a valid key return `403 Forbidden`.
---
## Stations
### GET `/api/stations`
Returns nearby petrol stations with live prices for a given fuel type.
**Location — provide one of:**
| Parameter | Type | Description |
|---|---|---|
| `postcode` | string | UK postcode, outcode, or place name (resolved via postcodes.io) |
| `lat` + `lng` | float | Decimal lat/lng (required if no postcode) |
**Optional:**
| Parameter | Type | Default | Notes |
|---|---|---|---|
| `fuel_type` | string | — | **Required.** See fuel type aliases below |
| `radius` | float | `10.0` | Search radius in km (0.150) |
| `sort` | string | `"price"` | `"price"`, `"distance"`, `"updated"`, or `"brand"` |
| `pricing_mode` | string | — | `"pump"` (reserved, no effect yet) |
**Sort values:**
| Value | Sorts by |
|---|---|
| `price` | Price ascending (cheapest first) — **default** |
| `distance` | Distance ascending (closest first) |
| `updated` | Price freshness descending (most recently updated first) |
| `brand` | Brand name AZ |
**Fuel type aliases** (`fuel_type` accepts any of these):
| Alias | Maps to |
|---|---|
| `petrol`, `unleaded`, `e10` | E10 (standard unleaded) |
| `premium_unleaded`, `e5` | E5 (super unleaded) |
| `diesel`, `b7_standard` | B7 Standard (standard diesel) |
| `premium_diesel`, `b7_premium` | B7 Premium (premium diesel) |
| `b10` | B10 (biodiesel blend) |
| `hvo` | HVO (hydrotreated vegetable oil) |
**Example request:**
```
GET /api/stations?postcode=SW1A1AA&fuel_type=petrol&radius=5&sort=price
GET /api/stations?lat=51.5074&lng=-0.1278&fuel_type=diesel&radius=10&sort=distance
GET /api/stations?postcode=M11AE&fuel_type=petrol&sort=updated
GET /api/stations?postcode=M11AE&fuel_type=petrol&sort=brand
```
**Response:**
```json
{
"data": [
{
"station_id": "0028acef5f3afc41...",
"name": "Alex Fuel Station",
"brand": "BP",
"is_supermarket": false,
"address": "123 High Street, London",
"postcode": "SW1A 1AA",
"lat": 51.5074,
"lng": -0.1278,
"distance_km": 1.23,
"fuel_type": "e10",
"price_pence": 14390,
"price": 143.9,
"price_updated_at": "2026-04-05T08:00:00.000Z"
}
],
"meta": {
"count": 12,
"fuel_type": "e10",
"radius_km": 10.0,
"lowest_pence": 14290,
"highest_pence": 14890,
"cheapest_price_pence": 14290,
"avg_pence": 14523.5
}
}
```
**Notes:**
- `price_pence` is the raw integer (e.g. `14390` = 143.90p). `price` is the float in pence (e.g. `143.9`).
- Closed stations (temporary or permanent) are excluded.
- Every call logs a search record used by the stats endpoint.
**Error — postcode not found:**
```json
{ "errors": { "postcode": ["Postcode not found."] } }
```
**Error — missing location:**
```json
{ "errors": { "lat": ["The lat field is required when postcode is not present."] } }
```
---
## Stats
### GET `/api/stats/searches`
Aggregate search statistics. Useful for a "social proof" counter on the frontend.
| Parameter | Type | Default | Notes |
|---|---|---|---|
| `period` | string | `"week"` | `"week"` (7 days) or `"month"` (30 days) |
**Example request:**
```
GET /api/stats/searches
GET /api/stats/searches?period=month
```
**Response:**
```json
{
"total_searches": 3842,
"unique_searchers": 1201,
"avg_results": 14.3,
"avg_lowest_price": 141.2,
"avg_highest_price": 148.7,
"avg_price": 144.9,
"period": "week",
"message": "Helped 1201 drivers find cheaper fuel this week so far!"
}
```
**Notes:**
- `avg_lowest_price`, `avg_highest_price`, `avg_price` are in **pence** as floats (e.g. `141.2` = 141.2p).
- `unique_searchers` is based on hashed IP — approximate unique users.
---
## Prediction
### GET `/api/prediction`
National (or regional) fuel price direction forecast for the next 7 days, based on live price data signals.
| Parameter | Type | Description |
|---|---|---|
| `fuel_type` | string | **Required.** Same aliases as `/api/stations` |
| `lat` | float | Optional. Enables regional momentum signal |
| `lng` | float | Optional. Enables regional momentum signal |
**Example request:**
```
GET /api/prediction?fuel_type=diesel
GET /api/prediction?fuel_type=petrol&lat=51.5074&lng=-0.1278
```
**Response:**
```json
{
"fuel_type": "e10",
"current_avg": 143.9,
"predicted_direction": "down",
"predicted_change_pence": -2.1,
"confidence_score": 74.5,
"confidence_label": "high",
"action": "wait",
"reasoning": "National prices have been falling at 0.3p/day. Supermarkets led last week — independents typically follow within 48h.",
"prediction_horizon_days": 7,
"region_key": "national",
"methodology": "multi_signal_live_fallback",
"signals": {
"trend": {
"score": -0.8,
"confidence": 0.92,
"direction": "down",
"detail": "Slope: -0.30p/day over 5 days (R²=0.92) [Adaptive lookback active]",
"data_points": 5,
"enabled": true,
"slope": -0.3,
"r_squared": 0.92
},
"day_of_week": {
"score": 0.0,
"confidence": 0.0,
"direction": "stable",
"detail": "Insufficient history for day-of-week pattern.",
"data_points": 0,
"enabled": true
},
"brand_behaviour": {
"score": -1.0,
"confidence": 0.6,
"direction": "down",
"detail": "Supermarkets fell 3.5p vs majors 0.5p (divergence: 3.0p). Expect majors to follow.",
"data_points": 2800,
"enabled": true
},
"national_momentum": {
"score": 0.0,
"confidence": 0.0,
"direction": "stable",
"detail": "National momentum disabled for national predictions",
"data_points": 0,
"enabled": false
},
"regional_momentum": {
"score": 0.0,
"confidence": 0.0,
"direction": "stable",
"detail": "No coordinates provided for regional momentum analysis",
"data_points": 0,
"enabled": false
},
"price_stickiness": {
"score": 0.05,
"confidence": 0.8,
"direction": "stable",
"detail": "Sticky prices (avg hold: 5.2 days) — more predictable.",
"data_points": 160,
"enabled": true
}
}
}
```
**Key fields:**
| Field | Values | Meaning |
|---|---|---|
| `predicted_direction` | `"up"`, `"down"`, `"stable"` | Price trend direction |
| `action` | `"fill_now"`, `"wait"`, `"no_signal"` | Consumer-facing recommendation |
| `confidence_label` | `"high"` (≥70), `"medium"` (≥40), `"low"` (<40) | Signal strength |
| `predicted_change_pence` | float | Expected p/litre change over 7 days |
| `current_avg` | float | Current national average in pence (e.g. `143.9` = 143.9p) |
**Signal structure** (each signal in `signals`):
| Field | Type | Notes |
|---|---|---|
| `score` | float (-1.0 to 1.0) | Negative = falling pressure, positive = rising |
| `confidence` | float (0.01.0) | How reliable this signal is |
| `direction` | `"up"` / `"down"` / `"stable"` | Signal direction |
| `detail` | string | Human-readable explanation |
| `data_points` | int | Number of price records used |
| `enabled` | bool | False if signal was skipped (missing data/coords) |
**Error — unknown fuel type:**
```json
{ "errors": { "fuel_type": ["Unknown fuel type. Use: diesel, petrol, e10, e5, hvo, b10."] } }
```
---
## Error Shapes
**Validation error (422):**
```json
{
"message": "The fuel type field is required.",
"errors": {
"fuel_type": ["The fuel type field is required."]
}
}
```
**Forbidden (403)** missing or invalid `X-Api-Key`:
```json
{ "message": "Forbidden." }
```