Build Your Own UK Savings Rate Tracker (Python, Google Sheets or Home Assistant)

Build Your Own UK Savings Rate Tracker (Python, Google Sheets or Home Assistant)

JJonny Pease

20 Oct 2026 · 12 min read

Build a tracker that checks your savings account every day and messages you when its rate drops or a better account appears. Copy-paste code for Python, Google Sheets and Home Assistant, for about £6 a month.

What you're building: a savings rate tracker that pings you

A small script that checks the savings account you hold once a day, and messages you when its rate drops or a better account turns up. It runs on a laptop, a Raspberry Pi, a Google Sheet or Home Assistant, uses the DepositScout UK savings rates API, and costs about £6 a month.

What the UK savings rates API gives you

Everything lives under https://depositscout.com/api/developer/v1 and comes back as savings rates JSON. Four endpoints:

  • GET /rates: every UK savings account we list, highest AER first. Narrow it with type, provider, amount (your balance in pounds) and access, and limit gives you the top results that match.
  • GET /providers: the banks and building societies we cover, with their slugs.
  • GET /switch-offers: current account switching bonuses.
  • GET /history: every rate change for one account. It's on the Enterprise plan, so the tracker below keeps its own history instead.

type takes easy-access, fixed-rate, notice, cash-isa, regular-saver, junior-isa, lifetime-isa, stocks-shares-isa, childrens-savings or all. So it's a cash ISA rates API too: type=cash-isa.

There's also GET /rates/{sourceKey}, which returns a single account by its permanent ID (sourceKey). That's the cheapest way to watch the account you already have.

Getting a key, and what a tracker costs

  1. Sign up at /developers/signup with an email or Google, and confirm your email.
  2. We review new accounts before switching on live access, and email you when it's on. Your account starts with 20 free credits.
  3. In your developer dashboard, open Keys and create one. ds_test_… keys are free and never charged, but return sample data: use one to check your code works. ds_live_… keys return real rates and spend credits.

Is it a free savings rates API? Not quite. Test keys are free, and the 20 starter credits cover about ten days of the tracker below. After that, £1 buys 10 credits, top-ups start at £5, and credits never expire.

What matters is that /rates charges 1 credit per row it returns, not per call. /rates/{sourceKey} is 1 credit. The tracker makes two calls a day: one for your account and one for the single best rival (limit=1). That's 2 credits a day, about 60 a month, or roughly £6.

How often you check makes a big difference:

How you check Credits a month Cost a month
Once a day: your account plus the best rival about 60 about £6
Once a day: the top 5 about 150 about £15
Every hour: the top 5 about 3,600 about £360

Rates change at most once a day, so checking every hour gets you nothing extra.

Your first request

Put your key in an environment variable, then ask for the top five easy access accounts:

export DS_API_KEY="ds_live_your_key_here"

curl "https://depositscout.com/api/developer/v1/rates?type=easy-access&limit=5" \
  -H "Authorization: Bearer $DS_API_KEY"

That costs 5 credits. The response, trimmed to the useful fields (the values here are an example):

{
  "data": [
    {
      "sourceKey": "chase-saver",
      "provider": "Chase UK",
      "accountName": "Chase Saver",
      "accountType": "Easy Access",
      "rate": "4.50",
      "interestPaid": "Monthly",
      "minBalance": 0,
      "fscsDetails": "Protected up to £120,000 by the FSCS",
      "depositscoutUrl": "https://depositscout.com/banks/chase/chase-saver",
      "lastCheckedAt": "2026-10-20T07:12:00.000Z",
      "updatedAt": "2026-10-07T06:00:00.000Z"
    }
  ],
  "meta": {
    "creditsCharged": 5,
    "creditsBalance": 15,
    "attribution": "Rates powered by DepositScout — depositscout.com",
    "nextCursor": null
  }
}

Three things to know:

  • rate is the AER as a string ("4.50"), so convert it before comparing.
  • sourceKey is the account's permanent ID. Store that, not the name, because banks rename accounts.
  • depositscoutUrl is the account's page on DepositScout. Put it in your alert so you can check the details before you move.

Pick your tool

All three routes lead to the same tracker. Pick the one you already use.

Python savings rates in 10 lines

import os, requests

res = requests.get(
    "https://depositscout.com/api/developer/v1/rates",
    params={"type": "easy-access", "limit": 5},
    headers={"Authorization": f"Bearer {os.environ['DS_API_KEY']}"},
    timeout=30,
)
res.raise_for_status()
for p in res.json()["data"]:
    print(f"{p['rate']}%  {p['provider']} {p['accountName']}  ({p['sourceKey']})")

Google Sheets

IMPORTDATA can't send an Authorization header, so use Apps Script. In your sheet, go to Extensions → Apps Script. Then open Project Settings → Script Properties, add DS_API_KEY with your key, and paste this in:

function refreshRates() {
  const key = PropertiesService.getScriptProperties().getProperty("DS_API_KEY");
  const res = UrlFetchApp.fetch(
    "https://depositscout.com/api/developer/v1/rates?type=easy-access&limit=5",
    { headers: { Authorization: "Bearer " + key } }
  );
  const body = JSON.parse(res.getContentText());
  const rows = body.data.map(p => [p.provider, p.accountName, Number(p.rate), p.sourceKey, p.lastCheckedAt]);

  const book = SpreadsheetApp.getActive();
  const sheet = book.getSheetByName("Rates") || book.insertSheet("Rates");
  sheet.clearContents();
  sheet.appendRow(["Provider", "Account", "AER %", "sourceKey", "Last checked"]);
  if (rows.length) sheet.getRange(2, 1, rows.length, rows[0].length).setValues(rows);
  sheet.getRange(rows.length + 3, 1).setValue(body.meta.attribution);
}

Run it once from the editor to give it permission, then add a trigger: Triggers → Add trigger → refreshRates → Time-driven → Day timer. Use a trigger rather than a custom =DS_RATES() formula. Sheets can recalculate a formula whenever it likes, and every recalculation spends credits.

Home Assistant savings rate sensor

Add your key to secrets.yaml:

depositscout_auth: "Bearer ds_live_your_key_here"

Then add a rest sensor to configuration.yaml:

rest:
  - resource: "https://depositscout.com/api/developer/v1/rates?type=easy-access&limit=1"
    scan_interval: 86400
    headers:
      Authorization: !secret depositscout_auth
    sensor:
      - name: "Best easy access rate"
        unique_id: depositscout_best_easy_access
        value_template: "{{ value_json.data[0].rate | float }}"
        unit_of_measurement: "%"
        json_attributes_path: "$.data[0]"
        json_attributes:
          - provider
          - accountName
          - sourceKey

Don't skip scan_interval: 86400 (once a day). Home Assistant's default is every 30 seconds. That would hit the 1,000-calls-a-day limit by mid-morning, after spending £100 of credits. Each Home Assistant restart also refreshes the sensor, which costs a credit.

Build the savings rate alert

This is the payoff: a savings rate alert that only speaks up when it matters.

The rule, in plain English. Ping me if:

  1. my account's rate is lower than it was last time I checked, or
  2. the best account of the same type that takes my balance pays more than 0.25 percentage points above mine.

Each better account should only trigger one message, not one every day.

Step 1: find your account's sourceKey

Your bank's slug is in its DepositScout page address (depositscout.com/banks/<slug>), or you can get it from /providers. List that bank's accounts and copy the sourceKey of yours:

curl "https://depositscout.com/api/developer/v1/rates?provider=chase&limit=5" \
  -H "Authorization: Bearer $DS_API_KEY"

Step 2: the tracker (Python, with Telegram)

You track one account with /rates/{sourceKey}, and compare it with the top result from /rates for the same type and balance. The script keeps the rate it saw last time in a small JSON file, so it knows when yours has dropped. That's your own rate history, with no need for /history.

#!/usr/bin/env python3
"""Daily savings rate alert. Rates powered by DepositScout."""
import json, os, pathlib, requests

API = "https://depositscout.com/api/developer/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['DS_API_KEY']}"}

MY_ACCOUNT = "your-source-key"   # from step 1
ACCOUNT_TYPE = "easy-access"     # the same type as your account
BALANCE = 20000                  # pounds: only compare accounts that take it
GAP = 0.25                       # percentage points before it's worth moving
STATE = pathlib.Path(__file__).with_name("tracker-state.json")


def get(path, **params):
    res = requests.get(f"{API}{path}", headers=HEADERS, params=params, timeout=30)
    res.raise_for_status()
    return res.json()["data"]


def notify(text):
    requests.post(
        f"https://api.telegram.org/bot{os.environ['TELEGRAM_BOT_TOKEN']}/sendMessage",
        json={"chat_id": os.environ["TELEGRAM_CHAT_ID"], "text": text},
        timeout=30,
    ).raise_for_status()


mine = get(f"/rates/{MY_ACCOUNT}")                               # 1 credit
rivals = get("/rates", type=ACCOUNT_TYPE, amount=BALANCE, limit=1)  # 1 credit
if not rivals:
    raise SystemExit("No accounts matched that type and balance.")
best = rivals[0]

my_rate = float(mine["rate"])
best_rate = float(best["rate"])
state = json.loads(STATE.read_text()) if STATE.exists() else {}
messages = []

last_rate = state.get("my_rate")
if last_rate is not None and my_rate < last_rate:
    messages.append(
        f"Your {mine['provider']} {mine['accountName']} rate dropped "
        f"from {last_rate:.2f}% to {my_rate:.2f}%."
    )

if (
    best["sourceKey"] != MY_ACCOUNT
    and best_rate - my_rate > GAP
    and state.get("told_about") != best["sourceKey"]
):
    messages.append(
        f"{best['provider']} {best['accountName']} pays {best_rate:.2f}%, "
        f"{best_rate - my_rate:.2f} points more than yours. {best['depositscoutUrl']}"
    )
    state["told_about"] = best["sourceKey"]

if messages:
    notify("\n\n".join(messages) + "\n\nRates powered by DepositScout")

state["my_rate"] = my_rate
STATE.write_text(json.dumps(state))

To set up Telegram, message @BotFather, send /newbot, and copy the token it gives you. Send your new bot any message, then open https://api.telegram.org/bot<token>/getUpdates and copy the chat.id from the reply.

Step 3: run it once a day

Put your secrets in a .env file next to the script:

export DS_API_KEY="ds_live_your_key_here"
export TELEGRAM_BOT_TOKEN="123456:your-bot-token"
export TELEGRAM_CHAT_ID="123456789"

Then add a line to your crontab (crontab -e). Most rate changes land in the morning, so 11am catches them the same day:

0 11 * * * cd /home/pi/rate-tracker && . ./.env && /usr/bin/python3 tracker.py >> tracker.log 2>&1

The same alert in Google Sheets (by email)

Here's the same rule as an Apps Script that emails you. Keep DS_API_KEY in Script Properties as before:

const MY_ACCOUNT = "your-source-key";
const ACCOUNT_TYPE = "easy-access";
const BALANCE = 20000;
const GAP = 0.25;

function dsGet(path) {
  const key = PropertiesService.getScriptProperties().getProperty("DS_API_KEY");
  const res = UrlFetchApp.fetch("https://depositscout.com/api/developer/v1" + path, {
    headers: { Authorization: "Bearer " + key },
  });
  return JSON.parse(res.getContentText()).data;
}

function checkMyRate() {
  const props = PropertiesService.getScriptProperties();
  const mine = dsGet("/rates/" + encodeURIComponent(MY_ACCOUNT));
  const best = dsGet("/rates?type=" + ACCOUNT_TYPE + "&amount=" + BALANCE + "&limit=1")[0];
  if (!best) return;

  const myRate = Number(mine.rate);
  const bestRate = Number(best.rate);
  const lastRate = props.getProperty("MY_RATE");
  const messages = [];

  if (lastRate !== null && myRate < Number(lastRate)) {
    messages.push("Your " + mine.provider + " " + mine.accountName + " rate dropped from " +
      Number(lastRate).toFixed(2) + "% to " + myRate.toFixed(2) + "%.");
  }
  if (best.sourceKey !== MY_ACCOUNT && bestRate - myRate > GAP && props.getProperty("TOLD_ABOUT") !== best.sourceKey) {
    messages.push(best.provider + " " + best.accountName + " pays " + bestRate.toFixed(2) + "%. " + best.depositscoutUrl);
    props.setProperty("TOLD_ABOUT", best.sourceKey);
  }
  if (messages.length) {
    MailApp.sendEmail(Session.getEffectiveUser().getEmail(), "Savings rate alert",
      messages.join("\n\n") + "\n\nRates powered by DepositScout");
  }
  props.setProperty("MY_RATE", String(myRate));
}

Add a daily trigger for checkMyRate: Triggers → Add trigger → Time-driven → Day timer → 11am to midday.

The same alert in Home Assistant

Add a second sensor for your own account. It goes in the same rest: list as before, under the first entry:

  - resource: "https://depositscout.com/api/developer/v1/rates/your-source-key"
    scan_interval: 86400
    headers:
      Authorization: !secret depositscout_auth
    sensor:
      - name: "My savings rate"
        unique_id: depositscout_my_rate
        value_template: "{{ value_json.data.rate | float }}"
        unit_of_measurement: "%"

Then add this automation to automations.yaml. The template trigger only fires when the gap first goes past 0.25 points, so you hear about it once:

- alias: "Savings rate alert"
  triggers:
    - trigger: state
      entity_id: sensor.my_savings_rate
      id: mine
    - trigger: template
      value_template: >
        {{ states('sensor.best_easy_access_rate') | float(0)
           - states('sensor.my_savings_rate') | float(0) > 0.25 }}
      id: better
  conditions:
    - condition: template
      value_template: >
        {{ trigger.id == 'better' or (
             trigger.to_state.state | float(0) > 0 and
             trigger.from_state.state | float(0) > trigger.to_state.state | float(0)) }}
  actions:
    - action: notify.notify
      data:
        title: "Savings rate alert"
        message: >
          Your rate: {{ states('sensor.my_savings_rate') }}%.
          Best easy access: {{ state_attr('sensor.best_easy_access_rate', 'provider') }}
          {{ state_attr('sensor.best_easy_access_rate', 'accountName') }}
          at {{ states('sensor.best_easy_access_rate') }}%.
          Rates powered by DepositScout.

Rules of the road

  • Once a day is enough. We check rates every day, and most changes appear in the morning. Responses are cached on our side for 60 seconds, so checking more often returns the same data and still spends credits.
  • Limits: live keys on the standard (Hobby) plan get 60 requests a minute and 1,000 a day per account. Test keys get 10 a minute. Go over and you get a 429 with a Retry-After header.
  • Keep limit small. You pay per row, so limit=1 for "the best one" and limit=5 for a short list.
  • Checking more often? Send the ETag. Every response has an ETag header. Send it back as If-None-Match, and if nothing has changed you get a free 304 with no body.
  • Credit DepositScout if anyone else sees the data. On a public dashboard or shared sheet, show "Rates powered by DepositScout" with a link to depositscout.com (it's in meta.attribution). In alerts and messages, say it in words. The examples above already do.
  • Keep your key private. Use an environment variable, Script Properties or secrets.yaml, and never commit it. Add .env and tracker-state.json to .gitignore. If a key leaks, rotate it in the dashboard: that cancels the old key and gives you a new one.
  • Test keys are for building only. They return sample data and mustn't power anything you rely on.

Frequently asked questions

Is the DepositScout API free?

Partly. Test keys are free and return sample data, and new accounts get 20 free credits. After that, £1 buys 10 credits, top-ups start at £5, and credits don't expire. The daily tracker in this guide costs about £6 a month.

Is there a rate limit?

Yes: 60 requests a minute and 1,000 a day on a live Hobby key, and 10 a minute on a test key. Going over returns a 429 with a Retry-After header telling you when to try again.

Does it cover cash ISAs?

Yes. Use type=cash-isa, or junior-isa and lifetime-isa. Add access=fixed with minTermMonths and maxTermMonths for fixed-rate ISAs of a particular length.

How often does the data update?

We check rates every day, and most changes appear in the morning. Each account has lastCheckedAt (when we last confirmed the rate on the provider's website) and updatedAt (when the record last changed).

Can I use it commercially?

Yes. The Data Licence allows the data in free or paid products, sites, apps and spreadsheets, as long as you show the attribution and don't resell or redistribute the raw data. Read the licence before you launch anything.

Want pagination, error codes and the full field list? The API tutorial and the API docs cover them. Would rather not write code at all? Rate alerts email you about better rates for free.

Related Topics

savings rates api
rate tracker
python
google sheets
home assistant
side project

Request API access

Tell us what you're building and we'll get back to you about API access and commercial licensing.

[email protected]

We'll only use these details to respond to your API access enquiry.