Overview #
The Truely API is a stateless, JSON-only HTTP service. Every endpoint lives under
https://truely.uk/api/v1/. Authentication is by Bearer token. Errors
conform to RFC 7807
(application/problem+json). All timestamps are ISO 8601 UTC.
All responses carry an X-Request-ID for support correlation.
Under the hood the API runs on Vercel Edge in London, reads from the same Supabase cluster as the website, and is served from a multi-region CDN. Per-key 24-hour sliding-window rate limiting is enforced at the edge before any database read. All traffic is HTTPS-only.
Quick start #
Three steps from zero to your first response. Mint a key, send a request, parse JSON.
- Create a free account at truely.uk/signup and open the Developer tab. Click Mint key. The plaintext key is shown once — store it now (it never appears again).
- Send a
GETrequest to/api/v1/postcode/{pc}with your key in theAuthorizationheader. - Parse the JSON. Done.
curl https://truely.uk/api/v1/postcode/SW18%201PB \ -H "Authorization: Bearer tk_live_REPLACE_WITH_YOUR_KEY"
// Node 18+ (fetch is global) const res = await fetch( "https://truely.uk/api/v1/postcode/SW18%201PB", { headers: { Authorization: `Bearer ${process.env.TRUELY_API_KEY}` } } ); // HTTP errors come through as RFC 7807 problem+json — read the body. if (!res.ok) { const problem = await res.json(); throw new Error(`${problem.status} ${problem.title}: ${problem.detail}`); } const report = await res.json(); console.log(report.borough, report.council_tax.band_d_gbp_per_year);
# Python 3.10+ — `requests` (or stdlib urllib if you prefer). import os, requests r = requests.get( "https://truely.uk/api/v1/postcode/SW18%201PB", headers={"Authorization": f"Bearer {os.environ['TRUELY_API_KEY']}"}, timeout=10, ) if r.status_code != 200: problem = r.json() raise RuntimeError(f"{problem['status']} {problem['title']}: {problem['detail']}") report = r.json() print(report["borough"], report["council_tax"]["band_d_gbp_per_year"])
// Go 1.21+ package main import ( "encoding/json" "fmt" "net/http" "os" ) func main() { req, _ := http.NewRequest("GET", "https://truely.uk/api/v1/postcode/SW18%201PB", nil) req.Header.Set("Authorization", "Bearer "+os.Getenv("TRUELY_API_KEY")) res, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer res.Body.Close() var report map[string]interface{} {} json.NewDecoder(res.Body).Decode(&report) fmt.Println(report["borough"]) }
Successful response (truncated for brevity):
{
"postcode": "SW18 1PB",
"borough": "Wandsworth",
"ward": "Thamesfield",
"parliamentary_constituency": "Putney",
"geo": { "lat": 51.45, "lng": -0.19 },
"council_tax": {
"band_d_gbp_per_year": 1028,
"most_common_band": "C",
"measurement_year": 2025,
"source": "MHCLG / VOA"
},
"deprivation": { "imd_decile": 7, "imd_release": "2025", "source": "MHCLG" },
"air_quality": { "pm25_ug_per_m3": 7.0, "no2_ug_per_m3": 20.6, "source": "DEFRA PCM" },
"broadband": { "pct_gigabit_capable": 100, "source": "Ofcom" },
"flood_risk": { "rivers_and_sea": "very_low", "surface_water": "low", "source": "Environment Agency" },
"greenspace": { "nearest_park_distance_m": 31, "source": "OS" },
"meta": {
"request_id": "3f0e2c8b-...",
"fetched_at": "2026-05-19T12:34:56Z",
"license": "Open Government Licence v3.0",
"docs": "https://truely.uk/api"
}
}
Authentication #
Every request must include a Bearer token in the Authorization header.
Keys are 32 characters of base62 (plus the tk_live_ or tk_test_
prefix). They are case-sensitive.
Authorization: Bearer tk_live_AbCdEf0123456789ZyXwVuTsRqPoNm
Truely stores only a SHA-256 hash of your key — the plaintext exists in our systems for the duration of a single HTTP response, then is discarded. If you lose your key, you cannot recover it; rotate to a new one in your developer dashboard. Old keys can be revoked independently, so rotation is zero-downtime.
Keys prefixed tk_test_ behave identically but are scoped to a test
tier with a relaxed rate limit and clearly-marked usage in your dashboard. Both
prefixes hit the same production endpoint and the same data.
Rate limits #
Rate limiting is per-key, on a rolling 24-hour window. The window slides continuously — every successful request "ages out" 24 hours later. Three response headers report your standing on every 2xx response:
| Header | Type | Description |
|---|---|---|
| X-RateLimit-Limit | integer | Daily request ceiling for your key. |
| X-RateLimit-Remaining | integer | Requests left in the current 24-hour window after this one. |
| X-RateLimit-Reset | integer | Unix seconds when the window will be fully clear (worst case). |
Exceeding the limit returns 429 Too Many Requests. The response body
includes limit, used, tier and a link to
upgrade. Use a token-bucket or fixed-pacing strategy client-side — exponential
back-off on 429 with jitter is the conventional pattern.
Pricing & tiers #
Every Truely account ships with a free tier. Paid tiers upgrade your rate ceiling, unlock additional endpoints, and add an SLA-backed status page. Billing is via Stripe — see truely.uk/pricing for the monthly figures.
Free
- 25 requests/day
- All v1 endpoints
- Community support
- Test keys included
Developer
- 2,500 requests/day
- Higher concurrency
- Email support, 1-business-day
- Up to 5 keys per account
Team
- 25,000 requests/day
- 99.9% uptime SLA
- Slack support channel
- Custom CORS allow-list
Need higher volume, on-prem mirroring, or a custom data licence? Talk to us.
Endpoints #
The Phase A API ships with a single full-report endpoint. Subsequent phases add the borough, compare, and twins endpoints — they will appear here without breaking the v1 contract.
Description
Returns the full Truely public-records report for a single UK postcode. The
postcode is normalised server-side, so SW181PB, sw18 1pb
and SW18%201PB all resolve to the same record.
Path parameters
| Field | Type | Description |
|---|---|---|
| pc | string | UK postcode. Any common spacing or casing is accepted. URL-encode the space if you include one (SW18%201PB). |
Response — 200 OK
| Field | Type | Description |
|---|---|---|
| postcode | string | Normalised postcode, e.g. SW18 1PB. |
| borough | string | Local authority district name (e.g. Wandsworth). |
| ward | string | Electoral ward name. |
| parliamentary_constituency | string | UK Parliament constituency. |
| geo | object | { lat, lng } in WGS84 decimal degrees. |
| council_tax | object | Band D rate (GBP/year), most-common band, measurement year, source attribution. |
| deprivation | object | IMD decile (1–10, 10 = least deprived), release year, source. |
| air_quality | object | Annual mean PM2.5 and NO₂ (µg/m³) from DEFRA's Pollution Climate Mapping. |
| broadband | object | Percentage of premises gigabit-capable, source (Ofcom Connected Nations). |
| flood_risk | object | Worst-band rivers/sea and surface-water rating from the Environment Agency. |
| greenspace | object | Nearest publicly accessible park distance in metres. |
| meta | object | request_id, fetched_at (ISO 8601 UTC), licence, docs URL. |
Possible errors
| Status | Title | When |
|---|---|---|
| 400 | Malformed postcode | Path segment is not a recognisable UK postcode shape. |
| 401 | Authentication required / Invalid API key | Missing, malformed, or unknown Bearer token. |
| 404 | Postcode not found | Postcode is well-formed but not present in the ONS register (terminated or never issued). |
| 429 | Rate limit exceeded | Per-key 24-hour quota reached. |
| 500 | Upstream error | Transient failure reading from Supabase or postcodes.io. Retry with back-off. |
Caching
Successful responses are CDN-cached for one hour with stale-while-revalidate. The authentication and rate-limit checks run before the CDN consults its cache, so your quota is always counted accurately.
Coming soon
The same authentication pattern applies to all endpoints below. They are queued in the order shown.
GET /api/v1/borough/{slug}— borough-level aggregates and a list of sampled postcodes.GET /api/v1/compare?a=&b=— pair-aware comparison of two postcodes, the same body the website shows.GET /api/v1/twins/{pc}— Truely Twins: nearest similar postcodes by combined-pillar similarity, threshold ≥ 70.GET /api/v1/health— service status. Unauthenticated; returns 200 when all upstream data sources have been refreshed in the last 48 hours.
Errors #
Every non-2xx response is application/problem+json following
RFC 7807. The
shape is identical across endpoints, so you can write a single error handler
once and reuse it everywhere.
{
"type": "https://truely.uk/api/errors/429",
"title": "Rate limit exceeded",
"status": 429,
"detail": "You have used 25 of your 25 requests/day quota for the free tier...",
"request_id": "3f0e2c8b-...",
"limit": 25,
"used": 25,
"tier": "free",
"window": "24h",
"upgrade": "https://truely.uk/pricing"
}
Status reference
| Status | Meaning | Retry? |
|---|---|---|
| 400Bad Request | Malformed input — fix the request before retrying. | No |
| 401Unauthorised | Missing, malformed, or revoked API key. | No |
| 403Forbidden | Non-HTTPS request, or CORS origin not in the key's allow-list. | No |
| 404Not Found | Postcode (or other resource) exists in the URL shape but not in the data. | No |
| 429Too Many Requests | Rate limit exceeded. Honour Retry-After if present. | Yes, with back-off |
| 500Server Error | Transient upstream failure. Truely will see this in the logs. | Yes, idempotent |
Response headers #
| Header | On | Description |
|---|---|---|
| X-Request-ID | every response | Unique per request. Include it in any support enquiry. |
| X-RateLimit-Limit | 2xx, 429 | Daily ceiling for the calling key. |
| X-RateLimit-Remaining | 2xx, 429 | Quota left after the current request. |
| X-RateLimit-Reset | 2xx, 429 | Unix seconds when the whole window will be clear. |
| Cache-Control | 2xx | public, max-age=0, s-maxage=3600, stale-while-revalidate=7200 on cacheable endpoints; no-store on errors. |
| Content-Type | 2xx | application/json; charset=utf-8. |
| Content-Type | non-2xx | application/problem+json; charset=utf-8. |
Versioning #
The API is versioned in the URL path: /api/v1/. Within v1, Truely
will only add fields — never remove or rename one. New endpoints
arrive as new paths. Removals or breaking renames will only ever happen in a v2,
and v1 will run alongside v2 for at least 12 months after v2 is
announced.
Treat every field as optional in your client. A missing field means we have no data for that postcode on that dataset (it does not mean the value is zero).
Data sources #
Every figure in a Truely response is sourced from an official UK government
publisher, refreshed on each publisher's own cadence. The source
field on every block names the publisher; the methodology
page documents each source's update frequency and known limitations.
- Council tax — MHCLG annual Band D release; VOA banding distribution.
- Deprivation (IMD) — MHCLG English Indices of Deprivation (2019 + 2025 release).
- Air quality — DEFRA Pollution Climate Mapping (annual mean).
- Broadband — Ofcom Connected Nations (twice-yearly).
- Flood risk — Environment Agency NaFRA2.
- Greenspace — Ordnance Survey OS Open Greenspace.
- Postcode geography — ONS postcodes.io.
Licence & attribution #
All data returned by the Truely API is published by UK government bodies under the Open Government Licence v3.0. You may use the data in your own products, including commercially, subject to the OGL's attribution requirement. The attribution string we recommend is:
Contains UK public sector information licensed under the Open Government Licence v3.0. Aggregated and delivered by Truely (https://truely.uk).
The Truely name, the truely wordmark, the editorial copy that surrounds the data, and the Truely Score composite are not OGL-licensed and remain Truely's property.
Status & uptime #
The free tier is best-effort. The Developer tier targets 99.5% monthly uptime. The Team tier carries a 99.9% SLA with service-credit remedies — see the Terms for the exact mechanism. Truely posts incident updates to status.truely.uk (incident page coming with the Team tier launch).
You can call GET /api/v1/health from your own monitoring to confirm
the API is reachable and that all upstream datasets have refreshed in the last
48 hours.
Support #
Email hello@truely.uk for any of the
following — include the X-Request-ID from a failing response if
you have one:
- Integration questions, schema clarifications, or feedback on the API surface.
- Reports of incorrect data — please cite the postcode and the figure that looks wrong, and we'll trace it back to the source.
- Security disclosures: encrypt to our PGP key if your finding is sensitive (key on request).
- Custom data licences, on-prem mirroring, higher-volume access.
Changelog #
Backwards-compatible additions land continuously. Breaking changes are only possible across major versions; none are planned for v1.
| Date | Endpoint | Change |
|---|---|---|
| 2026-05-19 | /v1/postcode/{pc} | v1 launch. Full postcode report, Bearer auth, RFC 7807 errors, 24-hour rolling rate limit. |