# 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.1–50) | | `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 A–Z | **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 E10 fuel price direction forecast for the next 7 days, based on live price data signals. Always analyses E10 — the most widely available fuel and the one with the most price history. | Parameter | Type | Description | |---|---|---| | `lat` | float | Optional. Decimal latitude. Enables regional prediction (50km radius). | | `lng` | float | Optional. Decimal longitude. Required if `lat` is provided. | **Example request:** ``` GET /api/prediction GET /api/prediction?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 } } } ``` **`region_key` values:** | Value | Meaning | |---|---| | `"national"` | No coordinates provided. `current_avg` and signals use national data. `regional_momentum` is disabled. | | `"regional"` | Coordinates provided. `current_avg` uses stations within 50km. `regional_momentum` is the primary signal (50% weight). Falls back to national average if no stations found in radius. | **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 | Average price in pence (e.g. `143.9` = 143.9p). Regional if lat/lng given, else national. | **Signal weights:** | Scope | Signal | Weight | |---|---|---| | National | trend | 45% | | National | brand_behaviour | 25% | | National | day_of_week | 20% | | National | price_stickiness | 10% | | Regional | regional_momentum | 50% | | Regional | trend | 20% | | Regional | day_of_week | 15% | | Regional | brand_behaviour | 10% | | Regional | price_stickiness | 5% | **Signal structure** (each signal in `signals`): | Field | Type | Notes | |---|---|---| | `score` | float (-1.0 to 1.0) | Negative = falling pressure, positive = rising | | `confidence` | float (0.0–1.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 or coordinates) | **LLM-backed prediction** — separately, the nightly `oil:predict` command generates an oil price direction from Brent crude data and stores it in `price_predictions`. This feeds into `AlertScoringService` (Signal 4) but is not exposed directly through this endpoint. See [LLM Prediction Providers](llm-prediction-providers.md). --- ## 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." } ```