6.7 KiB
External API & Data Sources
UK Fuel Finder API (gov.uk) — PRIMARY SOURCE
- Base URL:
https://www.fuel-finder.service.gov.uk/api/v1/ - Returns: all UK station prices + station metadata (~14,500 stations)
- Update frequency: stations report within 30 minutes of price change
- Our polling interval: every 15 minutes via scheduler (incremental), full refresh once daily
Authentication
OAuth 2.0 using JSON body POST (not form-encoded). Credentials in .env as FUEL_FINDER_CLIENT_ID / FUEL_FINDER_CLIENT_SECRET.
Get token:
POST /api/v1/oauth/generate_access_token
Content-Type: application/json
{"client_id": "...", "client_secret": "..."}
Response: {"access_token": "eyJ...", "token_type": "Bearer", "expires_in": 3600, "refresh_token": "eyJ..."}
Refresh token:
POST /api/v1/oauth/regenerate_access_token
Content-Type: application/json
{"client_id": "...", "client_secret": "..."}
Token caching strategy (FuelPriceService):
- Cache the
access_tokenwith TTL =expires_in- 60 seconds (3540s) - Cache key:
fuel_finder_access_token - On cache miss: call
generate_access_token, store result, return token - Use the
refresh_tokento regenerate before expiry if needed - Include token in every API request:
Authorization: Bearer {token}
Endpoints
GET /api/v1/pfs/fuel-prices?batch-number— all/incremental station pricesGET /api/v1/pfs?batch-number— all/incremental station metadata
Fuel prices response fields (array of stations):
node_id— station identifiertrading_name— station namefuel_prices[]— array of{fuel_type, price, price_last_updated, price_change_effective_timestamp}- Fuel types:
E5,E10,B7_STANDARD,B7_PREMIUM,B10,HVO - Price is a float (e.g.
159.9= 159.9p) — multiply × 100 and store as integer pence
Station metadata response fields (array of stations):
node_id,trading_name,brand_nameis_supermarket_service_station,is_motorway_service_stationtemporary_closure,permanent_closurelocation—{address_line_1, city, postcode, latitude, longitude}amenities— string array (e.g.car_wash,adblue_pumps)fuel_types— string array of available fuel typesopening_times— per-day open/close times (not used in scoring)
FuelPriceService responsibilities
- Fetch OAuth token (cache it)
- GET all station prices
- Upsert
stationstable with metadata - Insert new rows into
station_pricesonly when price has changed for that station+fuel combo - Call StationTaggingService to set
is_supermarketandbrand - Dispatch
PricesUpdatedEventfor downstream processing
Deduplication
Only insert a new station_prices row if price differs from the most recent stored price
for that (station_id, fuel_type) combination. Avoids row explosion on unchanged prices.
Credentials in .env
FUEL_FINDER_CLIENT_ID=
FUEL_FINDER_CLIENT_SECRET=
FUEL_FINDER_BASE_URL=https://api.fuel-finder.service.gov.uk
Postcodes.io — location resolution
- Free, no API key required
- Handled by
PostcodeService::resolve(string $query): ?LocationResult - Returns
LocationResultDTO withquery,displayName,lat,lng - Results cached for 30 days — cache key
postcode:{normalised_input} - Failed lookups are NOT cached — retried on next request
- Input is auto-detected:
| Input type | Example | Endpoint |
|---|---|---|
| Full postcode | SW1A 1AA |
GET /postcodes/{postcode} |
| Outcode (district) | PE7 |
GET /outcodes/{outcode} |
| Place / city name | Manchester |
GET /places?q={query}&limit=1 |
Anonymous search flow: user types a postcode/city → PostcodeService::resolve() → lat/lng stored in a JSON cookie (30 days) alongside the query string. On return visits, cookie lat/lng is used directly — postcodes.io is only called when the search term changes.
Registered users: postcode resolved once on registration, lat/lng stored on users table — not re-resolved unless postcode changes.
FRED API (St. Louis Fed) — Brent crude prices
- Series:
DCOILBRENTEU(daily Brent spot price, USD/barrel) - Endpoint:
GET https://api.stlouisfed.org/fred/series/observations - Params:
series_id=DCOILBRENTEU,sort_order=desc,limit=30,file_type=json - Free API key required — stored as
FRED_API_KEYin.env - Handled by
OilPriceService::fetchBrentPrices() - Fetched daily at 7am via
oil:predict --fetchscheduler command - FRED uses
"."as a placeholder for non-trading days (weekends/holidays) — filtered out before insert - Stored in
brent_pricestable, upserted ondateprimary key
Anthropic API — oil price direction prediction
- Endpoint:
POST https://api.anthropic.com/v1/messages - Model:
claude-haiku-4-5-20251001(configurable viaANTHROPIC_MODELin.env) - Key stored as
ANTHROPIC_API_KEYin.env - Handled by
OilPriceService::generateLlmPrediction() - Called once daily after FRED fetch — sends last 30 days of Brent prices + pre-computed EWMA context
- Response must be JSON:
{"direction": "rising|falling|flat", "confidence": 0-85, "reasoning": "..."} - Model sometimes wraps JSON in markdown code fences — these are stripped before
json_decode - Confidence is capped at 85 regardless of what the model returns
- On any failure (API error, malformed JSON, invalid direction) → falls back to EWMA silently
- Result stored in
price_predictionstable withsource = 'llm'
EWMA fallback (OilPriceService::generateEwmaPrediction()):
- Compares 3-day EWMA vs 7-day EWMA on chronological Brent price data
- Threshold: ±1.5% change → rising/falling; below → flat
- Confidence capped at 65 (simpler model)
- Used when: no
ANTHROPIC_API_KEYset, or LLM call fails - Result stored in
price_predictionstable withsource = 'ewma'
OneSignal — push notifications
- REST API:
https://oapi.onesignal.com/notifications - App ID + REST API key stored in .env as
ONESIGNAL_APP_ID,ONESIGNAL_API_KEY - Target by
player_id(stored inusers.push_token) - No official Laravel package needed — use Laravel HTTP client (
Http::post(...)) - Free plan: 10,000 subscribers — sufficient for v1
Vonage — WhatsApp + SMS
- Package:
vonage/client-corevia Composer - Credentials:
VONAGE_KEY,VONAGE_SECRET,VONAGE_WHATSAPP_FROMin .env - WhatsApp: Messages API, utility template category (pre-approved)
- SMS: SMS API, alphanumeric sender ID "FuelAlert"
- All Vonage calls go through NotificationDispatchService — never call Vonage directly from components
HTTP client
Use Laravel's built-in Http facade for all external API calls.
Always set a timeout: Http::timeout(10)->get(...).
Wrap in try/catch — log failures, never let a failed API call crash the scheduler.