Files
fuel-price/.claude/rules/api-data.md
Ovidiu U 5ad89e977d Add fuel API ingestion and historic storage design spec
Includes verified API authentication flow, correct base URL, all DB
table schemas for stations, current prices, history, and archive.
Fuel types corrected to match live API (B7_STANDARD, B7_PREMIUM).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-03 18:09:50 +01:00

13 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_token with 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_token to regenerate before expiry if needed
  • Include token in every API request: Authorization: Bearer {token}

Fuel Finder REST API The Fuel Finder API is a REST API that gives a simple, consistent way to request, create and update data. REST stands for Representational State Transfer which is an architectural software style in which standard HTTP request methods are used to retrieve and modify representations of data. This is identical to the process of retrieving a web page or submitting a web form.

Representational State Transfer (REST) web services In a RESTful API, each data resource has a unique URL and is manipulated using standard HTTP verbs such as:

GET to request a resource POST to create a resource (not used for read-only endpoints) PUT to change a resource (not used for read-only endpoints) DELETE to remove a resource (not used for read-only endpoints) Example: request a price resource GET: https://api.fuelfinder.service.gov.uk/v1/prices/GB-12345 HTTP/1.1 The request uses GET and does not include a request body.

In a RESTful API, a resource is modified by POSTing a revised resource representation, in this case JSON, to the same resource URL:

POST: https://api.fuelfinder.service.gov.uk/v1/ Content-Type: text/json { "CustomerName": "Joe Bloggs", "Address": "", "etc": etc } REST builds on the features of HTTP. Because each resource has a globally unique URL and can be fetched with GET, REST APIs can benefit from existing network components such as caches and proxies.

The JSON data format Responses use JSON (JavaScript Object Notation). JSON is a compact, widely used format for storing and exchanging data. Most programming languages support JSON, which makes it well suited to HTTP-based API services.

Endpoints

  • Endpoints
  • Method Endpoint
  • GET Fetch all PFS fuel prices
  • GET Fetch incremental PFS fuel prices
  • GET Fetch PFS information
  • GET Fetch incremental PFS information
https://www.fuel-finder.service.gov.uk/api/v1/pfs/fuel-prices?batch-number
[
  {
    "node_id": "0028acef5f3afc41c7e7d56fb285a940dfb64d6fea01cb4accd79c148321112d",
    "public_phone_number": null,
    "trading_name": "Alex Fuel Station",
    "fuel_prices": [
      {
        "fuel_type": "E5",
        "price": 159.9,
        "price_last_updated": "2026-02-17T16:03:04.938Z",
        "price_change_effective_timestamp": "2026-02-17T16:00:00.000Z"
      },
      {
        "fuel_type": "E10",
        "price": 132.9,
        "price_last_updated": "2026-02-17T16:03:04.938Z",
        "price_change_effective_timestamp": "2026-02-17T16:00:00.000Z"
      },
      {
        "fuel_type": "B7_STANDARD",
        "price": 141.9,
        "price_last_updated": "2026-02-17T16:03:04.938Z",
        "price_change_effective_timestamp": "2026-02-17T16:00:00.000Z"
      }
    ]
  },
  {
    "node_id": "01da92125c3751767044d06b202f45da5933f0e16e256fa3e98a16af8386308d",
    "public_phone_number": "",
    "trading_name": "Star Garage",
    "fuel_prices": [
      {
        "fuel_type": "E5",
        "price": 159.9,
        "price_last_updated": "2026-02-17T16:03:04.938Z",
        "price_change_effective_timestamp": "2026-02-17T16:00:00.000Z"
      }
    ]
  },
  {
    "node_id": "020592cd81196efdb61ab2135f837ddf3d2bee4e64346810270f0b088b4c09d8",
    "public_phone_number": null,
    "trading_name": "Blue Hills Fuel Station",
    "fuel_prices": [
      {
        "fuel_type": "E5",
        "price": 159.9,
        "price_last_updated": "2026-02-17T16:03:04.938Z",
        "price_change_effective_timestamp": "2026-02-17T16:00:00.000Z"
      },
      {
        "fuel_type": "B7_STANDARD",
        "price": 141.9,
        "price_last_updated": "2026-02-17T16:03:04.938Z",
        "price_change_effective_timestamp": "2026-02-17T16:00:00.000Z"
      }
    ]
  }
]
https://www.fuel-finder.service.gov.uk/api/v1/pfs?batch-number=1
[
  {
    "node_id": "9b275ab576eeba3c6677984be15ee22a74e54fdfe8e5ea700e84a03178dc4ac1",
    "public_phone_number": null,
    "trading_name": "TEST",
    "is_same_trading_and_brand_name": true,
    "brand_name": "TEST",
    "temporary_closure": false,
    "permanent_closure": false,
    "permanent_closure_date": null,
    "is_motorway_service_station": false,
    "is_supermarket_service_station": false,
    "location": {
      "address_line_1": "HALL & WOODHOUSE, TAPLOW BOATYARD, MILL LANE, TAPLOW, MAIDENHEAD, SL6 0AA",
      "address_line_2": null,
      "city": "MAIDENHEAD",
      "country": "England",
      "county": null,
      "postcode": "SL6 0AA",
      "latitude": 51.5268585,
      "longitude": -0.700361
    },
    "amenities": [
      "water_filling"
    ],
    "opening_times": {
      "usual_days": {
        "monday": {
          "open": "00:00:00",
          "close": "00:00:00",
          "is_24_hours": false
        },
        "tuesday": {
          "open": "00:00:00",
          "close": "00:00:00",
          "is_24_hours": false
        },
        "wednesday": {
          "open": "00:00:00",
          "close": "00:00:00",
          "is_24_hours": false
        },
        "thursday": {
          "open": "00:00:00",
          "close": "00:00:00",
          "is_24_hours": false
        },
        "friday": {
          "open": "00:00:00",
          "close": "00:00:00",
          "is_24_hours": false
        },
        "saturday": {
          "open": "00:00:00",
          "close": "00:00:00",
          "is_24_hours": false
        },
        "sunday": {
          "open": "00:00:00",
          "close": "23:59:00",
          "is_24_hours": true
        }
      },
      "bank_holiday": {
        "type": "bank holiday",
        "open_time": "00:00:00",
        "close_time": "00:00:00",
        "is_24_hours": false
      }
    },
    "fuel_types": [
      "E10",
      "E5",
      "HVO",
      "B10"
    ]
  },
  {
    "node_id": "4fd9a4c6b48358b9b5c95989fba100fdcbb87c9e909ed4ce1ad96f64ffb8b56a",
    "public_phone_number": "+44 7723608248",
    "trading_name": "TEST FORECOURT 1",
    "is_same_trading_and_brand_name": true,
    "brand_name": "TEXACO ONE",
    "temporary_closure": false,
    "permanent_closure": null,
    "permanent_closure_date": null,
    "is_motorway_service_station": false,
    "is_supermarket_service_station": false,
    "location": {
      "address_line_1": "NEWPORT",
      "address_line_2": "",
      "city": "BROUGH",
      "country": "ENGLAND",
      "county": "EAST YORKSHIRE",
      "postcode": "HU15 2RD",
      "latitude": 51.258503,
      "longitude": -3.417567
    },
    "amenities": [
      "adblue_packaged",
      "adblue_pumps",
      "car_wash",
      "customer_toilets"
    ],
    "opening_times": {
      "usual_days": {
        "monday": {
          "open": "06:00:01",
          "close": "23:00:01",
          "is_24_hours": false
        },
        "tuesday": {
          "open": "06:00:01",
          "close": "23:00:01",
          "is_24_hours": false
        },
        "wednesday": {
          "open": "06:00:01",
          "close": "23:00:01",
          "is_24_hours": false
        },
        "thursday": {
          "open": "06:00:01",
          "close": "23:00:01",
          "is_24_hours": false
        },
        "friday": {
          "open": "06:00:01",
          "close": "23:00:01",
          "is_24_hours": false
        },
        "saturday": {
          "open": "06:00:01",
          "close": "23:00:01",
          "is_24_hours": false
        },
        "sunday": {
          "open": "06:00:01",
          "close": "23:00:01",
          "is_24_hours": false
        }
      },
      "bank_holiday": {
        "type": "standard",
        "open_time": "06:00:01",
        "close_time": "23:00:01",
        "is_24_hours": false
      }
    },
    "fuel_types": [
      "B10"
    ]
  },
  {
    "node_id": "91bdda1c07fa05110a31639cc66932f9ed8bd388d4f6be542a423365bcfd53e1",
    "public_phone_number": "+442071930000",
    "trading_name": "SUPERFUEL LOUGHBOROUGH 12",
    "is_same_trading_and_brand_name": true,
    "brand_name": "SUPERFUEL STATION 4",
    "temporary_closure": false,
    "permanent_closure": null,
    "permanent_closure_date": null,
    "is_motorway_service_station": false,
    "is_supermarket_service_station": false,
    "location": {
      "address_line_1": "14 LONDON ROAD",
      "address_line_2": "FUELVILLE",
      "city": "LOUGHBOROUGH",
      "country": "ENGLAND",
      "county": "LEICESTERSHIRE",
      "postcode": "LE11 9AA",
      "latitude": 50.503343,
      "longitude": -2.12444
    },
    "amenities": [
      "adblue_packaged",
      "adblue_pumps",
      "car_wash",
      "customer_toilets",
      "water_filling"
    ],
    "opening_times": {
      "usual_days": {
        "monday": {
          "open": "06:00:00",
          "close": "22:00:00",
          "is_24_hours": false
        },
        "tuesday": {
          "open": "06:00:00",
          "close": "22:00:00",
          "is_24_hours": false
        },
        "wednesday": {
          "open": "06:00:00",
          "close": "22:00:00",
          "is_24_hours": false
        },
        "thursday": {
          "open": "06:00:00",
          "close": "22:00:00",
          "is_24_hours": false
        },
        "friday": {
          "open": "06:00:00",
          "close": "22:00:00",
          "is_24_hours": false
        },
        "saturday": {
          "open": "06:00:00",
          "close": "22:00:00",
          "is_24_hours": false
        },
        "sunday": {
          "open": "06:00:00",
          "close": "22:00:00",
          "is_24_hours": false
        }
      },
      "bank_holiday": {
        "type": "standard",
        "open_time": "08:00:00",
        "close_time": "20:00:00",
        "is_24_hours": false
      }
    },
    "fuel_types": [
      "E5",
      "HVO",
      "B10",
      "B7_PREMIUM",
      "B7_STANDARD"
    ]
  }
]

FuelPriceService responsibilities

  1. Fetch OAuth token (cache it)
  2. GET all station prices
  3. Upsert stations table with metadata
  4. Insert new rows into station_prices only when price has changed for that station+fuel combo
  5. Call StationTaggingService to set is_supermarket and brand
  6. Dispatch PricesUpdatedEvent for 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 — postcode → lat/lng

  • URL: https://api.postcodes.io/postcodes/{postcode}
  • Free, no API key required
  • Called once on user registration / when postcode changes
  • Store resolved lat + lng on users table
  • Cache postcode lookups for 30 days (postcodes rarely change coordinates)

FRED API (St. Louis Fed) — Brent crude direction

  • Series: DCOILBRENTEU (daily Brent spot price)
  • URL: https://api.stlouisfed.org/fred/series/observations?series_id=DCOILBRENTEU&api_key={key}&sort_order=desc&limit=10&file_type=json
  • Free API key required — stored as FRED_API_KEY in .env
  • Fetched once daily via scheduler at 7am
  • Stored in brent_prices table: (date DATE, price_usd DECIMAL(8,2))
  • Only the 5-day trend direction is used by the scoring engine

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 in users.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-core via Composer
  • Credentials: VONAGE_KEY, VONAGE_SECRET, VONAGE_WHATSAPP_FROM in .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.