Returns monthly ONS V3.0 affordability estimates for a user
curl --request GET \
--url https://api.sandbox.goteal.co/expenditure/{user_id}/ons \
--header 'X-API-KEY: <api-key>'import requests
url = "https://api.sandbox.goteal.co/expenditure/{user_id}/ons"
headers = {"X-API-KEY": "<api-key>"}
response = requests.get(url, headers=headers)
print(response.text)const options = {method: 'GET', headers: {'X-API-KEY': '<api-key>'}};
fetch('https://api.sandbox.goteal.co/expenditure/{user_id}/ons', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.sandbox.goteal.co/expenditure/{user_id}/ons",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_HTTPHEADER => [
"X-API-KEY: <api-key>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"net/http"
"io"
)
func main() {
url := "https://api.sandbox.goteal.co/expenditure/{user_id}/ons"
req, _ := http.NewRequest("GET", url, nil)
req.Header.Add("X-API-KEY", "<api-key>")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.get("https://api.sandbox.goteal.co/expenditure/{user_id}/ons")
.header("X-API-KEY", "<api-key>")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.sandbox.goteal.co/expenditure/{user_id}/ons")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Get.new(url)
request["X-API-KEY"] = '<api-key>'
response = http.request(request)
puts response.read_body{
"expenditures": [
{
"date": "2026-01-01T12:00Z",
"cost_of_living": "333.21",
"housing": "372.96",
"total": "706.17"
}
]
}{
"errors": [
"No X-API-KEY header provided or wrong value"
]
}{
"errors": [
"[date_of_birth] param is missing on the user and its payslips",
"[post_codes] param is missing on the user and its payslips",
"payroll periods are missing"
]
}Expenditure
Get ONS expenditure
Estimates cost of living and housing from the user’s payroll periods, date of birth, and postcode using the Teal V3.0 ONS Family Spending model (table A6).
Net pay is split across calendar months by overlapping days. Age is taken as of the last day of each month from that period’s date of birth (else the user’s) and adjusts cost of living only. Region (from each period’s postcode prefix, else the user’s first postcode that has an ONS region) adjusts housing only.
Months are skipped, not returned, when their net pay is zero or negative (refunds, reversals) or when no postcode for the month has an ONS region (for example GY, JE, IM or an unknown postcode).
404 lists each missing or invalid input as its own string in
errors:
User [<user_id>] not found[date_of_birth] param is missing on the user and its payslips[post_codes] param is missing on the user and its payslips[date_of_birth] param <date> is after the payslip month[date_of_birth] param <date> gives an age over 150, the ONS model limitPostcode <postcode> has no ONS region; supported: England, Wales, Scotland, Northern Ireland(only when no month has a postcode with a region)payroll periods are missingpayroll periods have no positive net pay
GET
/
expenditure
/
{user_id}
/
ons
Returns monthly ONS V3.0 affordability estimates for a user
curl --request GET \
--url https://api.sandbox.goteal.co/expenditure/{user_id}/ons \
--header 'X-API-KEY: <api-key>'import requests
url = "https://api.sandbox.goteal.co/expenditure/{user_id}/ons"
headers = {"X-API-KEY": "<api-key>"}
response = requests.get(url, headers=headers)
print(response.text)const options = {method: 'GET', headers: {'X-API-KEY': '<api-key>'}};
fetch('https://api.sandbox.goteal.co/expenditure/{user_id}/ons', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.sandbox.goteal.co/expenditure/{user_id}/ons",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_HTTPHEADER => [
"X-API-KEY: <api-key>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"net/http"
"io"
)
func main() {
url := "https://api.sandbox.goteal.co/expenditure/{user_id}/ons"
req, _ := http.NewRequest("GET", url, nil)
req.Header.Add("X-API-KEY", "<api-key>")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.get("https://api.sandbox.goteal.co/expenditure/{user_id}/ons")
.header("X-API-KEY", "<api-key>")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.sandbox.goteal.co/expenditure/{user_id}/ons")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Get.new(url)
request["X-API-KEY"] = '<api-key>'
response = http.request(request)
puts response.read_body{
"expenditures": [
{
"date": "2026-01-01T12:00Z",
"cost_of_living": "333.21",
"housing": "372.96",
"total": "706.17"
}
]
}{
"errors": [
"No X-API-KEY header provided or wrong value"
]
}{
"errors": [
"[date_of_birth] param is missing on the user and its payslips",
"[post_codes] param is missing on the user and its payslips",
"payroll periods are missing"
]
}Monthly cost of living and housing estimates from the Teal V3.0 ONS Family Spending model (table A6).
expenditures is newest month first.
Cost of living is age-adjusted from each payroll period’s identity_information.date_of_birth, falling back to the user’s date_of_birth. Housing is region-adjusted from each period’s identity_information.address.post_code, falling back to the user’s first post_codes entry that has an ONS region. Age is calculated as of the last day of each month.
Some months are left out of expenditures:
- Months whose net pay is zero or negative (refunds, reversals). They are skipped rather than returned as
0.00or negative amounts. - Months where neither the payroll period’s postcode nor any of the user’s
post_codeshas an ONS region (for example GY, JE, IM or an unknown postcode).
404 returns { "errors": [...] } with one string per missing or invalid input:
| Error | Cause |
|---|---|
User [<user_id>] not found | No such user for this client |
[date_of_birth] param is missing on the user and its payslips | No date of birth on a month’s payroll period or on the user |
[post_codes] param is missing on the user and its payslips | No postcode on a payroll period or on the user |
[date_of_birth] param <date> is after the payslip month | Date of birth is after the month it applies to, for example in the future |
[date_of_birth] param <date> gives an age over 150, the ONS model limit | Age is above the model’s oldest band |
Postcode <postcode> has no ONS region; supported: England, Wales, Scotland, Northern Ireland | No month has a postcode with an ONS region. One string per postcode tried |
payroll periods are missing | No payroll periods with net pay and dates |
payroll periods have no positive net pay | Every month has zero or negative net pay |