# Authentication
Source: https://docs.vepler.com/authentication
Send your API key with every request, and manage keys in the dashboard.
Every request needs an API key in the `x-api-key` header.
```bash cURL theme={null}
curl "https://api.vepler.com/v1/address/postcodes/SW1A2AA" \
-H "x-api-key: $VEPLER_API_KEY"
```
```typescript TypeScript SDK theme={null}
import { SDK } from "@vepler/sdk";
const sdk = new SDK({ apiKeyAuth: process.env.VEPLER_API_KEY });
```
```python Python theme={null}
import os
import requests
response = requests.get(
"https://api.vepler.com/v1/address/postcodes/SW1A2AA",
headers={"x-api-key": os.environ["VEPLER_API_KEY"]},
)
```
A request without a valid key, or from a deactivated account, gets `401`. There is no test mode: every key is live, and requests to charged endpoints are billed.
## Keys
Create and manage keys under [API keys](https://app.vepler.com/dashboard/api-keys) in the dashboard.
* A key is `vpr_live_` followed by 64 hexadecimal characters.
* The full key is shown once, when you create it. Only a fingerprint is stored, so a lost key cannot be recovered: create a new one.
* Every key on your account draws on the same credit balance and has access to the same products.
* You can rename a key and give it an expiry date. After that date it no longer works.
* Revoking a key cannot be undone. A revoked key can keep working for up to 60 seconds.
To replace a key, create the new one, deploy it, then revoke the old one.
## Keeping keys safe
Call the API from your server. A key in browser code, a mobile app or a public repository can be copied and used against your balance. Keep it in an environment variable or a secrets manager.
# Errors
Source: https://docs.vepler.com/concepts/errors
Status codes, error bodies, when to retry and what a failed request costs.
A failed request returns a 4xx or 5xx status. Most error bodies look like this:
```json theme={null}
{
"error": {
"type": "invalid_request_error",
"code": "uprn_not_found",
"message": "UPRN not found: 999999999999",
"param": "uprn"
}
}
```
| Field | Meaning |
| - | - |
| `type` | `invalid_request_error`, `authentication_error`, `rate_limit_error` or `api_error`. |
| `code` | Identifies the failure, for example `postcode_not_found` or `rate_limit_exceeded`. Branch on `code`, not on `message`. Some errors have none. |
| `message` | What went wrong, in words. |
| `param` | The parameter at fault, when the API can tell. |
| `suggestions` | Values to try instead. Only some not-found errors include it, such as an unknown postcode. |
## Other error bodies
Some failures use a different body, so do not assume `error` is always an object.
| Status | When | Body |
| - | - | - |
| 401 | The API key is missing, wrong, expired or revoked, or the account has been deactivated | `{ "error": "API key required" }` |
| 403 | The key cannot use this endpoint | `{ "error": "Forbidden", "message": "..." }` |
| 400 | A parameter fails the endpoint's schema, such as a `limit` above its maximum. Search suggest is the exception and uses the standard body. | `{ "success": false, "error": { "name": "ZodError", "message": "..." } }`, where `message` is a JSON-encoded array of the failing fields |
| 400, 404, 500, 504 | Some errors on `/v1/area-reference/...` | `{ "success": false, "error": "...", "details": "...", "code": "..." }`; `details` and `code` are not always present |
| 404 | The path matches no endpoint | Plain text: `404 Not Found` |
| 500 | An unexpected failure on the server | Plain text: `Internal Server Error` |
An unknown path returns either 404 (plain text) or 403 with the message `This endpoint is not available`, depending on the path.
## Status codes
| Status | Meaning | Retry |
| - | - | - |
| 400 | The request is invalid. | No. Fix the request. |
| 401 | The API key is missing, wrong, expired or revoked, or the account has been deactivated. Send the key in the `x-api-key` header. | No |
| 402 | The account has no credit for a billable endpoint (`insufficient_credits`), or too little for a title deed purchase. | Once the account has enough credit |
| 403 | The account does not include this product, or a property query filters on an attribute that needs an extended permission. | No |
| 404 | Nothing matches the identifier you sent. | No |
| 422 | Valuation endpoints only: the request is valid but the property cannot be valued. | No |
| 429 | A rate limit or the account's daily credit cap was reached. | After `Retry-After` |
| 500 | The server could not complete the request. | Yes, with backoff |
| 502 | Title deeds only: HM Land Registry could not be reached or rejected the purchase. | Yes, but read `money_state` first on a purchase |
| 503 | A dataset or service is briefly unavailable. | Yes, with backoff |
## Rate limits and credit
A 429 has the code `rate_limit_exceeded` and a `Retry-After` header giving the seconds to wait. Two limits produce it:
* Search suggest allows each account a set number of requests per minute.
* Billable endpoints share a daily credit cap per account, which resets at midnight UTC. `Retry-After` then counts down to midnight, so stop rather than retry.
The `message` states your account's limit. A 402 has no `Retry-After`: the same request succeeds once the account has enough credit. See [Usage and limits](/guides/rate-limiting).
## What a failed request costs
Requests that end in a 4xx or 5xx status are not charged. The exception is a title deed purchase: its error body carries `money_state`, which is `charged`, `not_charged` or `unknown`. Read it before you retry or refund. `unknown` means the outcome could not be established: call `GET /v1/title-deeds/{titleNumber}/purchase-status`, which reports what the account was charged, before retrying. A `202` means the purchase succeeded and is still being delivered; do not treat it as a failure.
## With the SDK
SDK methods throw on any 4xx or 5xx response.
```typescript theme={null}
import { SDK } from "@vepler/sdk";
import { SDKBaseError } from "@vepler/sdk/sdk/models/errors";
const sdk = new SDK({ apiKeyAuth: process.env.VEPLER_API_KEY });
try {
const address = await sdk.address.lookupUprn({ uprn: "100023336956" });
console.log(address.formatted);
} catch (err) {
if (!(err instanceof SDKBaseError)) throw err;
console.error(err.statusCode, err.headers.get("retry-after"), err.body);
}
```
Every HTTP error is an `SDKBaseError`, with the raw response text in `body`. The subclass depends on whether the body matched the documented shape, so branch on `statusCode` rather than on the class. The SDK's other error classes and its built-in retries are on [TypeScript SDK](/sdk/typescript#errors).
## Over HTTP
Read the body as text first, because some errors are plain text. This helper returns a code and message from any of the shapes above:
```typescript theme={null}
interface ApiError {
status: number;
code?: string;
message: string;
}
async function readError(res: Response): Promise {
const text = await res.text();
try {
const body = JSON.parse(text) as { error?: unknown; message?: unknown; details?: unknown; code?: unknown };
if (typeof body.error === "object" && body.error !== null) {
const { code, message } = body.error as { code?: string; message?: string };
return { status: res.status, code, message: message ?? text };
}
const parts = [body.error, body.message, body.details].filter((part) => typeof part === "string");
return {
status: res.status,
code: typeof body.code === "string" ? body.code : undefined,
message: parts.join(": ") || text,
};
} catch {
return { status: res.status, message: text };
}
}
```
To retry a 429, 500, 502 or 503, wait for `Retry-After` when the response has one, back off exponentially when it does not, and cap the attempts. Give up when `Retry-After` runs to midnight UTC, and do not retry other 4xx statuses.
# Pagination
Source: https://docs.vepler.com/concepts/pagination
How list endpoints page their results, with the limits for each endpoint.
Most list endpoints page with `limit` and `offset`. Transactions and streets use a cursor. Lookups by ID and nearest-match searches return a single page.
## Offset pagination
`limit` is the page size and `offset` is the number of records to skip. Send them in the query string on GET endpoints and in the JSON body on POST endpoints. A page looks like this:
```json theme={null}
{
"object": "list",
"url": "/v1/property/query",
"data": [...],
"has_more": true,
"total_count": 1250
}
```
Increase `offset` by the number of records you received and stop when `has_more` is `false`. Where the table below gives an `offset` maximum, stop there even if `has_more` is `true`. `total_count` is the number of matches across all pages, except where the table below says otherwise.
```typescript theme={null}
import { SDK } from "@vepler/sdk";
import type { PropertyDataApi } from "@vepler/sdk/sdk/models/shared";
const sdk = new SDK({ apiKeyAuth: process.env.VEPLER_API_KEY });
const properties: PropertyDataApi[] = [];
let offset = 0;
while (true) {
const page = await sdk.property.searchProperties({
area: [{ type: "postcode", value: "SW1A 2AA" }],
query: [
{
operator: "AND",
groups: [{ conditions: [{ field: "roomDetails.beds", comparator: "gte", value: 2 }] }],
},
],
limit: 100,
offset,
});
properties.push(...page.data);
if (!page.hasMore || page.data.length === 0) break;
offset += page.data.length;
}
```
Over HTTP, send `X-API-Version: 2` when you page property or listings endpoints. Without it they return `{ "success": true, "result": [...], "totalSize": 1250, "size": 25 }`, which has no `has_more`. The SDK always sends the header. See [Versions](/concepts/versions).
## Limits by endpoint
"None" means the API sets no maximum.
| Endpoint | `limit` default | `limit` max | `offset` max | Notes |
| - | - | - | - | - |
| `POST /v1/property/query` | 25 | 10,000 | 9,999 | `limit + offset` may not exceed 10,000. A deeper page is a 400 with code `pagination_window_exceeded`, and `has_more` turns `false` at that depth. |
| `POST /v1/listings/query/advanced` | 25 | 100 | 10,000 | Keep `limit + offset` within 10,000. `has_more` can stay `true` there and a deeper request fails, so narrow the query. |
| `POST /v1/planning/query` | 50 | 100 | 10,000 | An `offset` above 10,000 is treated as 10,000 and returns that page again with `has_more` still `true`. Narrow the query instead of paging past 10,000. |
| `GET /v1/epc/{locationId}` | 100 | 1,000 | None | A `limit` outside 1 to 1,000 is clamped. |
| `POST /v1/epc/query` | 25 | 10,000 | 100,000 | `has_more` and `total_count` sit inside a `pagination` object. |
| `GET /v1/address/postcodes/{postcode}` | 50 | 100 | 10,000 | Values outside the range are clamped, not rejected. Keep `limit + offset` within 10,000. |
| `POST /v1/schools/query` | 25 | 100 | None | |
| `POST /v1/companies/query` | 25 | 100 | None | The body is `{ success, results, total, limit, offset, hasMore }`. |
| `POST /v1/companies/persons/query` | 25 | 50 | None | Same body as the companies query. |
| `GET /v1/companies/{companyNumber}/officers` and `/pscs` | 25 | 100 | 10,000 | Values outside the range are clamped. Check `hasMore`. |
| `GET /v1/companies/persons/{personId}/appointments` | 25 | 100 | 10,000 | Values outside the range are clamped. Check `hasMore`; there is no total. |
| `POST /v1/sites/query` | 25 | 1,000 | 100,000 | `total_count` counts no higher than 10,000, so `has_more` turns `false` there. |
| `POST /v1/buildings/query` | 25 | 100 | 10,000 | Larger values are reduced to these maxima. `total_count` counts no higher than 10,000. |
| `POST /v1/energy/substations/query`, `GET /v1/energy/ecr`, `POST /v1/energy/connection-queue/query` | 100 | 1,000 | None | |
| `POST /v1/heritage/designations/query`, and the same path under `conservation` and `land-constraint` | 25 | 100 | 10,000 | |
| `GET /v1/search/suggest` | 10 | 100 | 10,000 | Larger values are treated as the maxima. See [Search and autocomplete](/guides/search-and-autocomplete). |
| `POST /v1/air-quality/query` | 25 | 100 | 10,000 | `has_more` is `true` whenever a page is full, so the last page can be empty. `total_count` counts the records on this page only. |
| `GET /v1/geography/areas/{code}/descendants` | 100 | 1,000 | None | `has_more` is always `false`. Stop when `offset` plus the records received reaches `total_count`. |
## Cursor pagination
### Transactions
`POST /v1/transactions/query` and `GET /v1/transactions/by-uprn/{uprn}` take `limit` (default 25, maximum 100) and `cursor`. Each page has `has_more` and `next_cursor`. Send `next_cursor` back as `cursor`, with the same `sort`, to get the next page; it is `null` on the last page. A cursor that cannot be read, or was made under a different sort, is ignored, and you get the first page again. These responses have no `total_count`.
```typescript theme={null}
import type { Transaction } from "@vepler/sdk/sdk/models/shared";
const sales: Transaction[] = [];
let cursor: string | undefined;
do {
const page = await sdk.transactions.getTransactionsByUprn({
uprn: "100023336956",
limit: 100,
cursor,
});
sales.push(...page.data);
cursor = page.nextCursor ?? undefined;
} while (cursor);
```
### Streets
`POST /v1/location/street/query` returns up to `identifier.limit` streets (default 10, maximum 50) for name, coordinate and postcode lookups. To get the next page, send `starting_after` with the `id` of the last street on the page, such as `gb-usrn:21701338`. SDK 6.8.0 does not have `starting_after`, so page streets over HTTP.
## Endpoints that return one page
These do not page. Ask for what you need in one request, or split the IDs across several.
| Endpoint | Returns |
| - | - |
| `GET /v1/property/{locationIds}` | Up to `limit` properties (default 25) for the location IDs you send. Set `limit` when you send more than 25 IDs; `offset` has no effect. |
| `GET /v1/property/propertyId/{propertyIds}` and `GET /v1/property/sources/{sourceIds}` | Up to 25 properties per request, whatever `limit` and `offset` say. |
| `POST /v1/property/properties/by-slugs` | Up to 100 properties per request, whatever `limit` says. Send at most 100 slugs at a time. |
| `GET /v1/listings/location/{locationId}` | Up to 25 listings. |
| `POST /v1/listings/query` | Up to `limit` listings (default 25, maximum 100). `offset` has no effect, so page with `POST /v1/listings/query/advanced`. |
| `GET /v1/address/postcodes/{postcode}/nearest` | Up to `limit` postcodes (default 10, maximum 20). |
| `GET /v1/address/reverse` | Up to `limit` addresses (default 5, maximum 20). |
| `GET /v1/buildings/by-point` | Up to `limit` buildings (default and maximum 50). |
| `GET /v1/energy/substations/nearest` | Up to `limit` substations (default 10, maximum 100). |
| `GET /v1/schools/autocomplete` | Up to `limit` schools (default 10, maximum 25). |
# Versions
Source: https://docs.vepler.com/concepts/versions
How the API changes between releases, and how to keep up.
## One live version
The API is released often, and every release is served under the same `/v1` paths. Each release has a number such as `6.26.2`. The current number is `info.version` in the [OpenAPI document](https://api.vepler.com/openapi).
You always call the latest release. There is no way to request an older one, except for the response formats described below.
## What a release can change
Any release can add endpoints, optional parameters and response fields. Write your client to ignore fields it does not recognise.
Changes that can break an existing integration are marked as breaking in the [changelog](https://vepler.com/changelog), which lists customer-facing changes release by release. Check it when something you rely on behaves differently.
## Formats you can choose with `X-API-Version`
**Lists.** These endpoints return a list in one of two formats:
* property by location ID, by property ID, by source ID and by slug, and the property query;
* listings by location, the listings query, the source query and the advanced query;
* prosperity by code.
Without the header: `{ "success": true, "result": [...], "totalSize": 120, "size": 25 }`. With `X-API-Version: 2`: `{ "object": "list", "url": "...", "data": [...], "has_more": true, "total_count": 120 }`.
**Single listings.** `GET /v1/listings/{id}` and `GET /v1/listings/source/{provider}/{key}` return `{ "success": true, "result": { ... } }` without the header, and the listing itself, with `"object": "listing"`, with `X-API-Version: 2`.
Other endpoints have one format and ignore `X-API-Version: 2`. The TypeScript SDK sends `X-API-Version: 2` on every request, so SDK calls always get the second format.
**Search suggest.** Send the release you built against, such as `X-API-Version: 2.150.0`, and `GET /v1/search/suggest` answers in the format of that time: releases before `3.0.0` get the earlier format, `3.0.0` and later the current one. Without a release number, suggest uses your account's default. The SDK always sends `2`, which carries no release number, and replaces any value you set. See [Search and autocomplete](/guides/search-and-autocomplete).
## The TypeScript SDK
`@vepler/sdk` is generated from the API's OpenAPI document and published to npm. Each SDK release takes the number of the API release it was generated from: SDK 6.8.0 was generated from API 6.8.0. The SDK is not republished for every API release, so an endpoint or field can reach the API before it reaches the SDK.
## Data versions
Some datasets are versioned separately from API releases:
* **Heritage, Conservation and Land Constraint.** `GET /v1/{heritage|conservation|land-constraint}/snapshots` lists the published snapshots, newest first. Pass `snapshotId` to a query to read one snapshot, so results stay the same while you page or while a newer snapshot is published.
* **Geography.** `GET /v1/geography/release` returns the release the geography endpoints answer from: a named set of publisher vintages that fit together. Pass `release`, such as `2025.1`, to the ancestors, descendants and children endpoints to pin one.
# Address billing
Source: https://docs.vepler.com/guides/address-billing
What each address and search endpoint charges, how search sessions are billed, and how repeat lookups are charged.
Address endpoints are charged in credits for successful responses only. A response with an error status, such as a `404` for an unknown UPRN, costs nothing.
There is no session token to create or send. Search sessions are opened and settled by the server.
## Charges by endpoint
| Endpoint | Charge |
| - | - |
| `GET /v1/search/suggest` | None per request. See [search sessions](#search-sessions). |
| `GET /v1/address/uprn/{uprn}` | Once per lookup, at the `address_uprn` rate. |
| `GET /v1/address/postcodes/{postcode}` | Once per request at the `address_base` rate, however many addresses it returns. Each page is a new request. |
| `POST /v1/address/resolve` | Per match returned. A match with coordinates, which come only with `options.includeCoordinates`, is charged at the `address_uprn` rate; one without, at `address_base`. Street matches are charged per street, at `street_open` or `street_ngd` by dataset. |
| `GET /v1/address/reverse` | Per address returned. |
| `POST /v1/address/postcodes/bulk` | Per address returned, across all the postcodes in the request. |
| `POST /v1/address/cleanse` | Per address submitted, whether or not it was matched. |
| `GET /v1/address/postcodes/{postcode}/validate`, `GET /v1/address/postcodes/{postcode}/nearest`, `GET /v1/address/outcodes/{outcode}`, `GET /v1/address/changelog` | Free. |
Reverse, bulk and cleanse responses are charged at the `address_uprn` or `address_base` rate according to the source of the addresses returned. The [pricing page](https://app.vepler.com/dashboard/pricing) in the dashboard lists every rate, including any your agreement changes.
## Search sessions
`GET /v1/search/suggest` is not charged per request. When a query narrows to 100 or fewer addresses, the response carries a `search_id` and a search session opens. Each session costs 1 credit, charged about 11 minutes after it opens, whether or not you then look an address up.
A later query whose addresses all fall inside one of the last eight sets returned to the same API key in the past 10 minutes joins that session rather than opening a new one, whether or not you send the `search_id` back. Sending it back narrows within the set without fetching again.
The full record for the address the user picks comes from `GET /v1/address/uprn/{uprn}`, charged as above. A search that ends in a selection is charged for its session and for that lookup.
If you send `X-API-Version` with a release earlier than `3.0.0`, such as `2.150.0` (see [Versions](/concepts/versions)), suggest uses its earlier format: there are no sessions, and each request that returns at least one address is charged once at the `address_base` rate. Accounts kept on that format are charged the same way.
## Repeat lookups and retries
By default, every successful request is charged, including a lookup of an address you have retrieved before. A retry of a request that completed on the server, for example after a timeout on your side, is charged again.
Distinct-record billing can be set for an account by agreement. With it, each address, street or property record is charged at most once per line item in each licence year. Each licence year runs from the anniversary of the date your organisation first registered. Postcode lookups and search sessions are still charged every time.
## Checking a charge
A charged response usually carries billing headers, including `X-Credits-Charged`. [Usage and limits](/guides/rate-limiting) explains them and where else to see what you spent.
# Property attributes
Source: https://docs.vepler.com/guides/property-attributes
Choose what the property endpoints return, see what each attribute costs, and find the names that filters and sorts accept.
Five endpoints return property records and take an `attributes` parameter:
* `GET /v1/property/{locationIds}`
* `GET /v1/property/propertyId/{propertyIds}`
* `GET /v1/property/sources/{sourceIds}`
* `POST /v1/property/properties/by-slugs`
* `POST /v1/property/query`
List the dotted paths you want. Only those are returned, plus `locationId`, which is always included. The `GET` endpoints take a comma-separated string with no spaces, `POST /v1/property/properties/by-slugs` takes an array, and `POST /v1/property/query` takes either. Without `attributes` you get `locationId`, `propertyId`, `slug` and `address`.
A group path such as `epc` returns everything under it.
## The attribute catalogue
`GET /v1/property/attributes` lists every attribute, with the names that filters and sorts accept. It is free to call.
```bash curl theme={null}
curl https://api.vepler.com/v1/property/attributes \
-H "x-api-key: $VEPLER_API_KEY"
```
```typescript TypeScript theme={null}
import { SDK } from "@vepler/sdk";
const vepler = new SDK({ apiKeyAuth: process.env.VEPLER_API_KEY });
const catalogue = await vepler.property.getPropertyAttributes();
```
| Field | Contents |
| - | - |
| `attributes` | One entry per attribute: `path`, `group`, `type`, `description`, `enumValues` where the values are fixed, `tier`, `requiresPermission` and `filterable`. |
| `groups` | Each top-level group: `key`, `label`, `attributeCount` and the highest `tier` among its attributes. |
| `tiers` | Each billing tier: `code`, `label` and its list price in `credits` per property record. |
| `pencePerCredit` | What one credit costs in pence on pay-as-you-go. |
| `filterFields` | Every name a filter condition accepts, with the attribute it targets (`target`) and its `kind`. |
| `comparators` | Every comparator name. |
| `sortFields` | Every name `sort[].field` accepts. |
| `defaultAttributes` | The attributes returned when you omit `attributes`. |
| `grantedPermissions` | The extended permissions your account holds. |
| `generatedAt` | When the catalogue was built. |
`tiers[].credits` and `pencePerCredit` are `null` when the price could not be read.
## What a record costs
Property records are charged per record returned, by tier:
* Every record is charged `tier_core`.
* Each attribute you request by its catalogue `path` adds that attribute's `tier`, for every record in the response, whether or not the record has a value for it.
* Any tier above `tier_core` also adds `tier_detail`.
So `["address", "roomDetails.beds", "pricing.currentSale"]` costs `tier_core`, `tier_detail` and `tier_market` per record. The catalogue gives list prices. The [pricing page](https://app.vepler.com/dashboard/pricing) in the dashboard shows the rates your account pays, and the [query builder](https://app.vepler.com/dashboard/query-builder) adds up the cost per record as you pick attributes.
## Filtering and sorting
`POST /v1/property/query` filters with conditions, each a `field`, a `comparator` and a `value`:
```json theme={null}
{
"area": [{ "type": "postcode", "value": "SW1A" }],
"query": [
{
"operator": "AND",
"groups": [
{
"conditions": [
{ "field": "beds", "comparator": "gte", "value": 3 },
{ "field": "pricing.currentSale", "comparator": "lte", "value": 1500000 }
]
}
]
}
],
"sort": [{ "field": "pricing.currentSale", "order": "asc" }],
"attributes": ["address", "roomDetails.beds", "pricing.currentSale"],
"limit": 25
}
```
`query` is required; send `[]` to apply no filters. A condition's `field` takes any name in `filterFields`: an attribute's own path, where its `filterable` is `true`, or an alias such as `beds` for `roomDetails.beds`. Any other name is rejected with a `400`.
The comparators a field accepts depend on its `kind`:
| `kind` | Comparators |
| - | - |
| `term` | `eq`, `ne`, `in`, `nin` |
| `range` | `eq`, `ne`, `in`, `nin`, `gt`, `gte`, `lt`, `lte` |
`in` and `nin` take an array. The other names in `comparators`, such as `contains`, are rejected with a `400`.
`sort` takes up to three entries, each a `field` from `sortFields` and an `order` of `asc` or `desc`.
## Permissions
Some attributes need an extended permission, named in `requiresPermission`. `grantedPermissions` lists the ones your account holds. An attribute you lack the permission for is left out of responses, without an error. Filtering on one is rejected with a `403`.
# Usage and limits
Source: https://docs.vepler.com/guides/rate-limiting
The limits on your account, what a 429 or 402 response means, and how to see what each request cost.
Two limits apply to each account, across all of its API keys:
| Limit | Default | Applies to |
| - | - | - |
| Suggest requests | 600 a minute | `GET /v1/search/suggest` |
| Daily spend | 50,000 credits a day, reset at midnight UTC | Every endpoint except suggest, `GET /v1/property/attributes`, the heritage and conservation `types` lists, flood risk, title deed reads and some health checks |
The suggest limit allows a burst of up to 600 requests, then refills at 10 a second. Daily spend counts the credits charged each UTC day, search sessions included. Endpoints that cost nothing but are not exempt, such as postcode validation, still return `402` at a zero balance and `429` once the daily limit is reached.
Your account can have different figures; the message in a `429` response states the one applied to it. To change them, email [hello@vepler.com](mailto:hello@vepler.com).
## 429 responses
Once you reach either limit, requests return `429` with a `Retry-After` header giving the seconds to wait:
```json theme={null}
{
"error": {
"type": "rate_limit_error",
"message": "Rate limit exceeded: this account is limited to 600 suggest requests per minute.",
"code": "rate_limit_exceeded"
}
}
```
A `429` from `GET /v1/search/suggest` is the request limit, and `Retry-After` is short. A `429` from any other endpoint is the daily spend limit, and `Retry-After` runs to midnight UTC.
## 402 responses
When your balance has nothing left to spend, the endpoints covered by the daily spend limit return `402` until you top up in the [dashboard](https://app.vepler.com):
```json theme={null}
{
"error": {
"type": "invalid_request_error",
"message": "Insufficient credits. Please top up your balance or subscribe to a plan.",
"code": "insufficient_credits"
}
}
```
A `402` has no `Retry-After`: retrying does not help until your balance changes.
## Retries
Only `2xx` responses are charged, so a request that fails with a `429`, `402` or `5xx` costs nothing. Title deed purchases are the exception: see [Errors](/concepts/errors). A request that timed out on your side may still have completed, and been charged, on the server; retrying it is charged again.
The SDK does not retry by default; [Retries](/sdk/typescript#retries) shows how to turn it on. Keep the give-up time short, because a daily spend `429` does not clear until midnight UTC, and do not retry timeouts or dropped connections, since those requests may already have been charged. Without the SDK, wait at least the `Retry-After` interval before retrying.
## Seeing what you spent
A charged response usually carries these headers:
| Header | Value |
| - | - |
| `X-Credits-Charged` | Credits this request cost |
| `X-Credits-Balance` | Your balance after it |
| `X-Already-Charged` | `true` when nothing was charged because this request's charge had already been recorded, otherwise `false` |
They are left out when the charge takes longer to settle than the response; the charge still completes. Search session charges never appear in headers.
The [Requests page](https://app.vepler.com/dashboard/analytics/requests) in the dashboard lists each request with what it cost, settled search sessions included, and the [pricing page](https://app.vepler.com/dashboard/pricing) lists every rate. The pricing page also shows what one credit costs.
## Spending less
* Request only the property attributes you need. Each attribute's tier is charged for every record returned; see [Property attributes](/guides/property-attributes).
* A repeat lookup of the same address or property is charged again unless your account has distinct-record billing; see [Address billing](/guides/address-billing).
# Search and autocomplete
Source: https://docs.vepler.com/guides/search-and-autocomplete
Address type-ahead with the suggest endpoint and the UPRN lookup, with what each call returns and costs.
Address autocomplete takes two calls: `GET /v1/search/suggest` while the user types, then `GET /v1/address/uprn/{uprn}` for the address they pick. Make both calls from your server. An API key in browser code can be copied and used against your balance (see [Authentication](/authentication)).
## Which endpoint
| Job | Endpoint | SDK method |
| - | - | - |
| Suggestions as the user types | `GET /v1/search/suggest` | `search.searchSuggest` |
| The full record for the address the user picked | `GET /v1/address/uprn/{uprn}` | `address.lookupUprn` |
| Every address at a postcode, for a postcode-first picker | `GET /v1/address/postcodes/{postcode}` | `address.lookupPostcode` |
| Match a complete free-text address to UPRNs | `POST /v1/address/resolve` | `address.resolveAddress` |
The rest of this page covers the first two.
## Quick start
```typescript SDK theme={null}
import { SDK } from '@vepler/sdk';
const vepler = new SDK({ apiKeyAuth: process.env.VEPLER_API_KEY });
const suggestions = await vepler.search.searchSuggest({ q: '10 Downing Street' });
// Address rows carry a UPRN. Here the first one stands in for the user's pick.
const picked = suggestions.data.find((row) => row.uprn !== undefined);
if (picked?.uprn) {
const address = await vepler.address.lookupUprn({ uprn: picked.uprn });
console.log(address.formatted, address.coordinates);
}
```
```bash cURL theme={null}
curl -G "https://api.vepler.com/v1/search/suggest" \
-H "x-api-key: $VEPLER_API_KEY" \
--data-urlencode "q=10 Downing Street"
curl "https://api.vepler.com/v1/address/uprn/100023336956" \
-H "x-api-key: $VEPLER_API_KEY"
```
## Suggest
`GET /v1/search/suggest` reads the query as a postcode, an address or a place name and returns address and location suggestions in one list.
| Parameter | Default | Notes |
| - | - | - |
| `q` | Required | What the user has typed. |
| `source` | Both | `address` for individual premises, `location` for postcodes, places and towns. |
| `classification` | `residential` | `residential`, `commercial` or `all`. Filters address results. |
| `addressable_only` | `false` | `true` leaves out historical and superseded addresses. |
| `lat`, `lng` | None | A centre point in WGS84 decimal degrees. Send both or neither. |
| `geo_mode` | `bias` | `bias` ranks addresses near the point higher and still returns distant ones. `restrict` drops addresses further than `radius_meters` from the point. |
| `radius_meters` | `10000` with `bias` | 1 to 200000. Required with `restrict`. With `bias`, it is the distance at which a match's text score is roughly halved. |
| `limit` | `10` | Rows per page. Values above 100 are treated as 100. |
| `offset` | `0` | Rows to skip. Values above 10000 are treated as 10000. |
| `search_id` | None | The `search_id` from an earlier response. See [Candidate sets](#candidate-sets). |
| `granularity` | `address` | `street` returns one row per named street. See [Streets](#streets). |
| `dataset` | `open_then_ngd` | The street data source, with `granularity=street` only. |
`geo_mode` and `radius_meters` need `lat` and `lng`. The centre point applies to address results, so it behaves most predictably with `source=address`.
A bad parameter returns 400 with `error.type` `invalid_request_error`, `error.code` `validation_error` and `error.param` naming the parameter: for example `lat` without `lng`, `geo_mode=restrict` without `radius_meters`, or `limit=0`.
### Response
An example for `q=10 Downing Street&classification=all&limit=100`:
```json theme={null}
{
"object": "list",
"url": "/v1/search/suggest",
"data": [
{
"id": "addr-100023336956",
"title": "10 Downing Street, London, SW1A 2AA",
"main_text": "10 Downing Street, London, SW1A 2AA",
"type": "address",
"source": "address",
"score": 18.4,
"relevanceScore": 1,
"uprn": "100023336956",
"matched_substrings": [
{ "offset": 0, "length": 2 },
{ "offset": 3, "length": 7 },
{ "offset": 11, "length": 6 }
],
"main_text_matched_substrings": [
{ "offset": 0, "length": 2 },
{ "offset": 3, "length": 7 },
{ "offset": 11, "length": 6 }
]
}
],
"has_more": false,
"total_count": 1,
"search_id": "3f8a2c1e-9b7d-4e6f-8a2c-1e9b7d4e6f8a",
"set_complete": true,
"metadata": { "processingTime": "38ms" }
}
```
* Show `title`. `matched_substrings` gives the character ranges of `title` that match the query, for highlighting.
* Address rows (`type: "address"`) carry only the label, the `uprn` and the match ranges: no coordinates, distance, classification or separate `postcode` field. The UPRN lookup returns coordinates, postcode and classification.
* Other rows have a `type` such as `postcode`, `street` or `locality`, no `uprn`, and a `metadata` object whose fields vary by type.
* Page with `offset` while `has_more` is `true`.
The SDK returns the same fields in camelCase: `hasMore`, `totalCount`, `searchId`, `setComplete`, and on rows `mainText` and `matchedSubstrings`.
### Candidate sets
When a query matches between 1 and 100 addresses, the server fetches all of them as a candidate set and returns its `search_id`. Queries with more matches, and requests with `source=location` or `granularity=street`, return `search_id: null`.
`set_complete: true` means `data` holds the whole set: the response is the first page and `limit` cut nothing. Request `limit=100` and every set arrives whole.
Once you hold a whole set, and while the user's query still starts with the query that produced it, filter the rows you hold instead of calling again. Keep a row when every word of the query is the start of a word in its `title`, ignoring case, where a word is a run of the letters a to z and digits. The server narrows with the same rule. If no row is left, call again without a `search_id`.
With a smaller `limit`, send the `search_id` with the next query instead, and the server answers from the set without searching again. Drop it when the new query no longer starts with the old one (the user deleted characters or edited earlier text): the server would still answer from the old set, which can miss addresses that match the new query.
A set is kept for 10 minutes from when it formed. An unknown or expired `search_id` runs a normal search, as does a narrowing request that matches nothing in the set. Either can form a new set.
### A type-ahead input
Wait for a short pause in typing (around 250 ms), do not call for fewer than 3 characters, and abort a request when a newer one replaces it, so a slow early response cannot overwrite a later one. This browser-side sketch applies the length check, the abort and local narrowing. Call it from your debounced input handler.
```typescript theme={null}
// Browser code. /api/suggest is your backend route: it calls
// vepler.search.searchSuggest({ q, limit: '100' }) and returns the result as JSON.
interface Row { title: string; uprn?: string }
interface Suggestions { data: Row[]; searchId?: string | null; setComplete?: boolean }
let held: { query: string; rows: Row[] } | null = null;
let inFlight: AbortController | null = null;
const words = (text: string): string[] => text.toLowerCase().split(/[^a-z0-9]+/).filter(Boolean);
function narrow(rows: Row[], query: string): Row[] {
const queryWords = words(query);
return rows.filter((row) => {
const titleWords = words(row.title);
return queryWords.every((word) => titleWords.some((titleWord) => titleWord.startsWith(word)));
});
}
// Call after a pause in typing. Resolves to null when a newer call replaced this one.
export async function suggest(input: string): Promise {
inFlight?.abort();
const query = input.trim();
if (query.length < 3) return [];
if (held && query.toLowerCase().startsWith(held.query.toLowerCase())) {
const rows = narrow(held.rows, query);
if (rows.length > 0) return rows; // answered from the set, no request
}
const controller = new AbortController();
inFlight = controller;
try {
const res = await fetch(`/api/suggest?q=${encodeURIComponent(query)}`, { signal: controller.signal });
if (!res.ok) throw new Error(`Suggest failed with status ${res.status}`);
const body: Suggestions = await res.json();
held = body.searchId && body.setComplete ? { query, rows: body.data } : null;
return body.data;
} catch (error) {
if (controller.signal.aborted) return null;
throw error;
}
}
```
### Streets
`granularity=street` returns one row per named street instead of one per address, for input such as `West Street, Sheffield`. Each row has `type: "street"`, a representative centre point in `location` and the street's USRN in `metadata.usrn`. Streets with the same name are told apart by the town in the query and, when you send them, by `lat`, `lng`, `radius_meters` and `geo_mode`.
`dataset` picks the source: `open` (Ordnance Survey open data under the Open Government Licence), `ngd` (the Ordnance Survey National Geographic Database) or `open_then_ngd`, the default, which reads the NGD only when the open data has no matching street. Sending `dataset` with any other granularity returns 400.
When a page includes open-data streets, the response's top-level `metadata.attribution` (not a row's `metadata`) holds the attribution you must show wherever those fields are displayed. SDK 6.8.0 has neither `dataset` nor `metadata.attribution`; call the endpoint over HTTP when you need them.
## Look up the picked address
`GET /v1/address/uprn/{uprn}` returns the full record for the `uprn` of the row the user picked.
| Parameter | Default | Notes |
| - | - | - |
| `uprn` (path) | Required | 1 to 12 digits. |
| `include` | None | `geography` adds the administrative and statistical areas for the address's postcode. |
| `dataset` | `os` | `os` for the Ordnance Survey address model, `paf` for the Royal Mail Postcode Address File. |
| Field | Meaning |
| - | - |
| `line_1`, `line_2`, `line_3` | Address lines. A line with nothing in it is an empty string. |
| `post_town` | The settlement, as Ordnance Survey names it. The postal town is `royal_mail.post_town`. |
| `postcode` | The full postcode. |
| `formatted` | The whole address on one line. |
| `coordinates` | `{ lat, lng }` in WGS84. |
| `classification_code` | AddressBase classification code for how the property is used. |
| `royal_mail` | The postal form of the address. Absent when the UPRN has no Royal Mail delivery point. |
| `geography` | Present with `include=geography`. |
The SDK names these in camelCase: `line1`, `postTown`, `classificationCode`, `royalMail`. The [API reference](https://api.vepler.com/reference) lists every field.
| Status | `error.code` | Cause |
| - | - | - |
| 400 | `invalid_uprn` | The UPRN is not 1 to 12 digits. |
| 402 | `insufficient_credits` | Your account has no credit left. |
| 404 | `uprn_not_found` | No address has this UPRN. |
| 429 | `rate_limit_exceeded` | The daily credit cap is reached. See [Limits](#limits). |
## How a search is billed
In the current suggest format:
* Suggest requests are not charged one by one.
* Each candidate set costs 1 credit. A `search_id` is charged once, about 11 minutes after its set formed, whether or not the user picks an address. Responses with `search_id: null` cost nothing.
* Narrowing a set costs nothing more. A request that sends a live `search_id` reuses it. A request without one joins an existing set when all its matches fall inside one of the last eight sets formed for the same API key in the past 10 minutes. On a key shared by many users that check can miss, so filter locally or send the `search_id`.
* A query that no longer narrows the set (characters deleted, a typo fixed, a new address started) can form a new set, which costs another credit.
In the [earlier format](#response-formats-and-the-version-header), each query that returns at least one address is charged a flat amount, and there are no candidate-set charges.
Each successful UPRN lookup is charged at the rate shown in your [dashboard](https://app.vepler.com), and a repeat lookup is charged again unless your account has distinct-record billing: see [Address billing](/guides/address-billing#repeat-lookups-and-retries). Charged responses carry billing headers, described in [Usage and limits](/guides/rate-limiting#seeing-what-you-spent); candidate-set charges are not reported on any response.
SDK methods return the response body only. To read the headers, add a response hook to the SDK's HTTP client:
```typescript theme={null}
import { SDK } from '@vepler/sdk';
import { HTTPClient } from '@vepler/sdk/lib/http';
const httpClient = new HTTPClient();
httpClient.addHook('response', (res) => {
console.log(res.url, res.headers.get('X-Credits-Charged'), res.headers.get('X-Already-Charged'));
});
const vepler = new SDK({ apiKeyAuth: process.env.VEPLER_API_KEY, httpClient });
```
## Limits
| Limit | Default | When exceeded |
| - | - | - |
| Suggest requests | 600 a minute per account | 429 `rate_limit_exceeded`, with `Retry-After` in seconds. |
| Credits spent on charged requests, such as the UPRN lookup | 50,000 per account per UTC day | Charged requests return 429 until midnight UTC, with `Retry-After` in seconds. |
Candidate-set charges count towards the daily total. Both limits can be changed for your account: email [hello@vepler.com](mailto:hello@vepler.com). See [Usage and limits](/guides/rate-limiting) for retrying after a 429.
## Response formats and the version header
Suggest has two response formats. The current one, introduced in release 3.0.0, is described on this page. The earlier one returns full address rows, including `location` coordinates and `postcode`, has no `search_id` or `set_complete`, and is charged per query.
To fix the format, send a release number containing a dot in the `X-API-Version` header on suggest requests:
* `3.0.0` or later, such as `X-API-Version: 6.26.2`, gets the current format.
* An earlier release, such as `X-API-Version: 2.150.0`, gets the earlier format.
Without a release number (no header, or the `X-API-Version: 2` that the SDK sends on every request), your account's default applies. That is the current format unless your account is set to the earlier one.
# Introduction
Source: https://docs.vepler.com/introduction
What the Vepler API covers, how to call it and where each detail is documented.
The Vepler API serves UK property data over HTTPS. Responses are JSON, apart from map tiles, census CSV files and title document PDFs.
* **Base URL:** `https://api.vepler.com`. Every endpoint is under `/v1`.
* **Authentication:** your API key in the `x-api-key` header. See [Authentication](/authentication).
* **Billing:** most requests are charged in credits against your account. The pricing page in your dashboard lists the price of each product.
## What's available
The names below match the sections of the [API reference](https://api.vepler.com/reference), which documents every endpoint, parameter and response field.
**Addresses and search**
| Section | What you get |
| - | - |
| Address | Addresses by postcode or UPRN, resolution of free-text addresses to UPRNs, reverse geocoding from a coordinate, and bulk cleansing. |
| Search | One autocomplete endpoint across properties, addresses, streets and places. |
| Streets | Streets by USRN, UPRN, coordinate, name or postcode, with their names, places and a representative point, and on request the properties on each and their count. |
**Properties and land**
| Section | What you get |
| - | - |
| Property | Property records keyed by UPRN: characteristics, tenure, market status, pricing and ownership. One property, a batch or a whole area. |
| AVM | Automated valuations. Each estimate comes with a confidence interval. |
| Listings | Properties advertised for sale or to rent, with asking price, marketing status, description, features and media. |
| Transactions | Sold prices from HM Land Registry price paid data, by area or by UPRN. |
| EPC | Energy Performance Certificates: current and potential ratings, fabric and heating, recommendations and running costs. |
| Buildings | Building footprints and attributes, by building, UPRN, area or point. |
| Sites | Registered land titles and their boundaries, by area or by property. |
| Title Deeds | Official copies of title documents, bought per title, with structured detail extracted from them. |
**Planning and designations**
| Section | What you get |
| - | - |
| Planning | Planning applications with status, development type, decision dates and documents. |
| Heritage | Scheduled monuments, world heritage sites, registered battlefields and protected wreck sites. |
| Conservation | Special Areas of Conservation, Special Protection Areas, Ramsar wetlands, ancient and native woodland, and landscape designations. |
| Land Constraint | Green Belt, Tree Preservation Zones, Article 4 Direction Areas, Agricultural Land Classification, Flood Zones 2 and 3, and Coal Mining Development High Risk Areas. |
| Flood Risk | Flood risk at a coordinate, per source, with the datasets behind it. |
**Areas and neighbourhoods**
| Section | What you get |
| - | - |
| Areas | Administrative and statistical areas with boundaries and hierarchies, and the areas that contain a point or postcode. |
| Geography | Areas by GSS code in one versioned tree: what contains an area, what it contains, and where each record came from. |
| Census | Census datasets and variables, area profiles, totals for custom areas, and map tiles. |
| Metrics | Demographic, economic and housing statistics, comparable across areas of the same type. |
| Prosperity | The Prosperity Index for small areas. |
| Safety | Recorded crime and antisocial behaviour by neighbourhood, with counts by category and period, and scores. |
| Schools, Schools Catchment, Schools Reference | Schools with inspection outcomes, performance, intake and capacity, plus catchment areas and admission areas. |
| POI | Amenities, facilities, transport and services near a location. |
| Connectivity | Broadband and mobile coverage at a location. |
| Air Quality | Modelled pollutant concentrations and air quality bands at a location. |
| Energy | Electricity network substations with capacity and headroom, supply areas, connection queues and forecasts. |
| Councils | Local authorities with contact details and service links. |
| Companies | Companies House companies, officers and people with significant control. |
**System** holds a health check for each service.
Your account may not include every product. A request to a product it does not include returns `403`.
## Versions
New releases ship often, all under `/v1`, and each has a version number. See [Versions](/concepts/versions) for what changes between releases and how to follow them.
## Tools
* [API reference](https://api.vepler.com/reference): every endpoint, with a console for test requests.
* [OpenAPI document](https://api.vepler.com/openapi): the full specification as JSON, to generate a client in any language.
* [TypeScript SDK](/sdk/typescript): `@vepler/sdk` on npm.
* [Changelog](https://vepler.com/changelog): customer-facing changes, release by release.
Questions: [hello@vepler.com](mailto:hello@vepler.com).
# Quick start
Source: https://docs.vepler.com/quickstart
Make your first request.
## 1. Create an API key
Sign in at [app.vepler.com](https://app.vepler.com) and create a key under [API keys](https://app.vepler.com/dashboard/api-keys). The key is shown once, when you create it, so copy it then. It starts with `vpr_live_`.
## 2. Make a request
Look up the addresses at a postcode:
```bash theme={null}
curl "https://api.vepler.com/v1/address/postcodes/SW1A2AA" \
-H "x-api-key: $VEPLER_API_KEY"
```
The response lists every address at the postcode, up to 50 per page.
## 3. Or use the TypeScript SDK
```bash theme={null}
npm install @vepler/sdk
```
```typescript theme={null}
import { SDK } from "@vepler/sdk";
const sdk = new SDK({ apiKeyAuth: process.env.VEPLER_API_KEY });
async function run() {
const result = await sdk.address.lookupPostcode({ postcode: "SW1A2AA" });
console.log(result);
}
run();
```
## If the request fails
| Status | Meaning |
| - | - |
| `401` | The `x-api-key` header is missing, or the key is wrong, expired or revoked. |
| `402` | Your account has no credit left for the request. |
| `403` | Your account does not include this product. |
| `429` | A rate limit or your daily credit limit was reached. Wait for the seconds in `Retry-After`; after the daily limit, that is until midnight UTC. |
[Errors](/concepts/errors) covers every status and the error body.
## Next
* Find the endpoint you need in the [API reference](https://api.vepler.com/reference). [Introduction](/introduction) lists what each section covers.
* Read [Versions](/concepts/versions) to see how the API changes and how to keep up.
# SDK examples
Source: https://docs.vepler.com/sdk/examples/general
Working @vepler/sdk code for common tasks.
Each example uses this client. For paging see [Pagination](/concepts/pagination), and for failures see [Errors](/concepts/errors).
```typescript theme={null}
import { SDK } from "@vepler/sdk";
const sdk = new SDK({ apiKeyAuth: process.env.VEPLER_API_KEY });
```
## List the addresses in a postcode
```typescript theme={null}
const postcode = await sdk.address.lookupPostcode({ postcode: "SW1A 2AA" });
for (const address of postcode.data) {
console.log(address.uprn, address.formatted);
}
```
A page holds up to 50 addresses by default and 100 at most. On this endpoint `limit` and `offset` are strings, such as `limit: "100"`.
## Turn typed text into an address
Suggestions carry a UPRN but not the full address record. Look the chosen UPRN up for that.
```typescript theme={null}
const suggestions = await sdk.search.searchSuggest({ q: "10 downing street", source: "address" });
const match = suggestions.data.find((suggestion) => suggestion.uprn !== undefined);
if (match?.uprn) {
const address = await sdk.address.lookupUprn({ uprn: match.uprn });
console.log(address.formatted);
}
```
[Search and autocomplete](/guides/search-and-autocomplete) covers type-ahead in full.
## Get properties for a list of UPRNs
`getPropertiesByLocation` returns at most `limit` properties, 25 by default, so set `limit` to the number of IDs in each request.
```typescript theme={null}
import type { PropertyDataApi } from "@vepler/sdk/sdk/models/shared";
async function getProperties(uprns: string[]): Promise {
const properties: PropertyDataApi[] = [];
for (let i = 0; i < uprns.length; i += 100) {
const batch = uprns.slice(i, i + 100);
const page = await sdk.property.getPropertiesByLocation({
locationIds: batch.join(","),
limit: String(batch.length),
});
properties.push(...page.data);
}
return properties;
}
```
## Search for properties near a point
Point coordinates are longitude first, then latitude. `attributes` chooses the fields that come back and sets the cost per record: see [Property attributes](/guides/property-attributes).
```typescript theme={null}
const nearby = await sdk.property.searchProperties({
area: [{ type: "point", coordinates: [-0.1276, 51.5034], radius: 500 }],
query: [
{
operator: "AND",
groups: [{ conditions: [{ field: "roomDetails.beds", comparator: "gte", value: 3 }] }],
},
],
sort: [{ field: "roomDetails.beds", order: "desc" }],
attributes: ["address", "roomDetails.beds"],
limit: 50,
});
for (const property of nearby.data) {
console.log(property.roomDetails?.beds, property.address?.displayAddress);
}
```
## Find planning applications nearby
With `near`, results come back closest first and each carries `distance` in metres.
```typescript theme={null}
const planning = await sdk.planning.searchPlanningApplications({
query: {
near: { lat: 51.5034, lng: -0.1276, radiusM: 500 },
receivedDateFrom: "2025-01-01",
},
limit: 50,
});
for (const application of planning.data) {
console.log(application.receivedDate, application.status, application.distance, application.description);
}
```
`radiusM` runs from 100 to 50,000.
## Get the sales history of a property
`price.amount` is in the currency's minor unit: pence for GBP.
```typescript theme={null}
const history = await sdk.transactions.getTransactionsByUprn({ uprn: "100023336956", limit: 100 });
for (const sale of history.data) {
console.log(sale.date?.toISOString().slice(0, 10), (sale.price?.amount ?? 0) / 100);
}
```
One page holds up to 100 sales. When `hasMore` is `true`, pass `nextCursor` back as `cursor` for the rest.
# Installation
Source: https://docs.vepler.com/sdk/installation
Install @vepler/sdk, set your API key and configure TypeScript.
## Requirements
* Node.js 18 or later, Bun 1 or later, or Deno 1.39 or later. The SDK uses the runtime's built-in `fetch`.
* An API key from the [dashboard](https://app.vepler.com/dashboard/api-keys). Call the API from your server: a key used in a web page is visible to anyone who opens it.
## Install
```bash npm theme={null}
npm install @vepler/sdk
```
```bash pnpm theme={null}
pnpm add @vepler/sdk
```
```bash yarn theme={null}
yarn add @vepler/sdk
```
```bash bun theme={null}
bun add @vepler/sdk
```
The package has ES module and CommonJS builds and includes its type definitions. Its one dependency is `zod`, version 3.25 or 4.
The package describes itself as beta and warns that a minor release can include breaking changes. Pin an exact version, such as `npm install --save-exact @vepler/sdk@6.8.0` (a plain install saves `^6.8.0`), and upgrade on purpose.
## Set your API key
Keep the key out of your code and read it from the environment:
```bash theme={null}
export VEPLER_API_KEY="vpr_live_..."
```
```typescript ES modules theme={null}
import { SDK } from "@vepler/sdk";
const sdk = new SDK({ apiKeyAuth: process.env.VEPLER_API_KEY });
```
```javascript CommonJS theme={null}
const { SDK } = require("@vepler/sdk");
const sdk = new SDK({ apiKeyAuth: process.env.VEPLER_API_KEY });
```
`apiKeyAuth` also accepts an async function that returns the key, if you fetch it from a secrets manager.
## Check the key
`sdk.heritage.types()` needs no product licence and is not charged, so it makes a safe first call:
```typescript theme={null}
const types = await sdk.heritage.types();
console.log(types.data.length);
```
If it throws with `statusCode` 401, the key is missing or not valid.
## TypeScript settings
```json tsconfig.json theme={null}
{
"compilerOptions": {
"target": "es2020",
"lib": ["es2020", "dom", "dom.iterable"],
"module": "nodenext",
"moduleResolution": "nodenext"
}
}
```
* `lib` needs `dom` and `dom.iterable`: the SDK's type definitions use Fetch types such as `RequestInfo` and `HeadersInit`, and without them `tsc` reports errors in those files unless `skipLibCheck` is on.
* `moduleResolution` must be `nodenext`, `node16` or `bundler` for imports from subpaths such as `@vepler/sdk/sdk/models/errors`. The older `node` setting cannot resolve them.
* With a bundler, `"module": "esnext"` and `"moduleResolution": "bundler"` also work.
## Troubleshooting
| Problem | Cause |
| - | - |
| `'apiKey' does not exist in type 'SDKOptions'` | The option is `apiKeyAuth`. |
| `Cannot find module '@vepler/sdk/sdk/models/errors'` | `moduleResolution` is `node`. Change it as above. |
| `Property 'getV1PropertyLocationIds' does not exist` | Methods are named after the operation, such as `sdk.property.getPropertiesByLocation`. See [Methods](/sdk/typescript#methods). |
| Every call fails with `statusCode` 401 | `VEPLER_API_KEY` is not set in the process running the SDK, or the key is not valid. Keys start with `vpr_live_`. |
# TypeScript SDK
Source: https://docs.vepler.com/sdk/typescript
Call the Vepler API from TypeScript or JavaScript with @vepler/sdk.
`@vepler/sdk` is a typed client for every public endpoint, generated from the [OpenAPI document](https://api.vepler.com/openapi). This page describes version 6.8.0.
```bash theme={null}
npm install @vepler/sdk
```
Other package managers, runtimes and TypeScript settings are on [Installation](/sdk/installation).
## Create a client
```typescript theme={null}
import { SDK } from "@vepler/sdk";
const sdk = new SDK({ apiKeyAuth: process.env.VEPLER_API_KEY });
```
The SDK sends the key in the `x-api-key` header. Create keys in the [dashboard](https://app.vepler.com/dashboard/api-keys). Create one client and reuse it.
## Call an endpoint
```typescript theme={null}
const result = await sdk.property.getPropertiesByLocation({
locationIds: "100023336956",
});
for (const property of result.data) {
console.log(property.locationId, property.address?.displayAddress);
}
```
A method returns the parsed response body.
* Names that are snake\_case in the API are camelCase in the SDK: `has_more` becomes `hasMore`, `total_count` becomes `totalCount` and the `geo_mode` parameter becomes `geoMode`.
* Some query parameters are typed as strings, including `limit` and `offset` on several GET endpoints such as `getPropertiesByLocation`: `limit: "50"`. The request types show which.
* The SDK sends `X-API-Version: 2` with every request, so list endpoints that offer two formats return `{ object, url, data, has_more, total_count }`, and the single-listing endpoints return the listing itself with `"object": "listing"`. See [Versions](/concepts/versions).
## Methods
Methods are grouped by API area: `address`, `airQuality`, `areas`, `avm`, `buildings`, `census`, `companies`, `connectivity`, `conservation`, `councils`, `energy`, `epc`, `floodRisk`, `geography`, `heritage`, `landConstraint`, `listings`, `metrics`, `planning`, `poi`, `property`, `prosperity`, `safety`, `schools`, `schoolsCatchment`, `search`, `sites`, `streets`, `system`, `titleDeeds` and `transactions`. The [package README](https://www.npmjs.com/package/@vepler/sdk) lists every method. Some common ones:
| Endpoint | Method |
| - | - |
| `GET /v1/property/{locationIds}` | `sdk.property.getPropertiesByLocation` |
| `POST /v1/property/query` | `sdk.property.searchProperties` |
| `GET /v1/address/postcodes/{postcode}` | `sdk.address.lookupPostcode` |
| `GET /v1/address/uprn/{uprn}` | `sdk.address.lookupUprn` |
| `POST /v1/address/resolve` | `sdk.address.resolveAddress` |
| `GET /v1/search/suggest` | `sdk.search.searchSuggest` |
| `POST /v1/planning/query` | `sdk.planning.searchPlanningApplications` |
| `POST /v1/listings/query/advanced` | `sdk.listings.queryListingsAdvanced` |
| `POST /v1/transactions/query` | `sdk.transactions.queryTransactions` |
## Errors
Methods throw on any 4xx or 5xx response.
```typescript theme={null}
import { SDKBaseError } from "@vepler/sdk/sdk/models/errors";
try {
const address = await sdk.address.lookupUprn({ uprn: "100023336956" });
console.log(address.formatted);
} catch (err) {
if (err instanceof SDKBaseError) {
console.error(err.statusCode, err.body);
}
throw err;
}
```
| Error | Thrown when |
| - | - |
| `SDKBaseError` | The base class for every HTTP error. It has `statusCode`, `headers`, `body` (the raw text) and `rawResponse`. |
| A class for each documented error shape | The body matched the error shape that method documents. The parsed body is in `data$`. |
| `SDKError` | The status is one the method does not document. |
| `ResponseValidationError` | The response body did not match the shape the method documents, as with a 401 on methods that document one. `rawValue` holds the body. |
| `ConnectionError`, `RequestTimeoutError`, `RequestAbortedError` | The request did not complete. These are not `SDKBaseError`s. |
| `SDKValidationError` | Your arguments failed the SDK's own checks, so nothing was sent. `instanceof SDKValidationError` is also true for a `ResponseValidationError`, so test `SDKBaseError` first. |
The class depends on the response body, so branch on `statusCode`. Every class is exported from `@vepler/sdk/sdk/models/errors`. The error bodies themselves are described on [Errors](/concepts/errors).
## Retries
The SDK does not retry unless you configure it. Set a policy for the client, and override it for a single call with `retries`:
```typescript theme={null}
const sdk = new SDK({
apiKeyAuth: process.env.VEPLER_API_KEY,
retryConfig: {
strategy: "backoff",
backoff: {
initialInterval: 500,
maxInterval: 10_000,
exponent: 1.5,
maxElapsedTime: 60_000,
},
retryConnectionErrors: false,
},
});
await sdk.address.lookupUprn({ uprn: "100023336956" }, { retries: { strategy: "none" } });
```
* Times are in milliseconds.
* The SDK retries 429, 500, 502, 503 and 504 responses. Pass `retryCodes` with a call to change the list.
* When a response has `Retry-After`, the SDK waits that long, up to `maxInterval`.
* After `maxElapsedTime` it stops and throws the last error.
* `retryConnectionErrors: true` also retries failed connections and timeouts. A request that timed out may still have completed and been charged, and a retry is charged again.
* Without `backoff`, the defaults are `initialInterval` 500, `maxInterval` 60,000, `exponent` 1.5 and `maxElapsedTime` 3,600,000 (one hour).
A 429 from the daily credit cap does not clear until midnight UTC, so keep `maxElapsedTime` short.
## Timeouts and cancellation
There is no timeout unless you set `timeoutMs`, on the client or on one call. A timeout throws `RequestTimeoutError`.
```typescript theme={null}
const sdk = new SDK({ apiKeyAuth: process.env.VEPLER_API_KEY, timeoutMs: 10_000 });
await sdk.address.lookupUprn({ uprn: "100023336956" }, { timeoutMs: 3_000 });
const controller = new AbortController();
setTimeout(() => controller.abort(), 3_000);
await sdk.address.lookupUprn({ uprn: "100023336956" }, { signal: controller.signal });
```
Aborting through `signal` throws `RequestAbortedError`. A call given a `signal` ignores `timeoutMs`.
## Client options
| Option | Purpose |
| - | - |
| `apiKeyAuth` | The API key, or an async function that returns it. |
| `serverURL` | The base URL. Defaults to `https://api.vepler.com`. |
| `timeoutMs` | Request timeout in milliseconds. None by default. |
| `retryConfig` | Retry policy. Off by default. |
| `httpClient` | An `HTTPClient` with your own `fetch`, or hooks that run before and after each request. |
| `debugLogger` | Logs each request and response, for example to `console`. The log includes the `x-api-key` header, so use it only on your own machine. |
| `userAgent` | Replaces the `user-agent` header the SDK sends. |
## Types
Request and response types are in `@vepler/sdk/sdk/models/shared` and `@vepler/sdk/sdk/models/operations`:
```typescript theme={null}
import type { PropertyListResponse } from "@vepler/sdk/sdk/models/shared";
import type { GetPropertiesByLocationRequest } from "@vepler/sdk/sdk/models/operations";
const request: GetPropertiesByLocationRequest = { locationIds: "100023336956", limit: "50" };
const page: PropertyListResponse = await sdk.property.getPropertiesByLocation(request);
```
These subpaths need `moduleResolution` set to `nodenext`, `node16` or `bundler`.
## Standalone functions
Every method is also a function you can import on its own, so a bundler can leave out the rest of the SDK. These functions return a result object rather than throwing on API and network errors:
```typescript theme={null}
import { SDKCore } from "@vepler/sdk/core.js";
import { addressLookupUprn } from "@vepler/sdk/funcs/addressLookupUprn.js";
const core = new SDKCore({ apiKeyAuth: process.env.VEPLER_API_KEY });
const res = await addressLookupUprn(core, { uprn: "100023336956" });
if (res.ok) {
console.log(res.value.formatted);
} else {
console.error(res.error);
}
```
## Versions
Each SDK release takes the number of the API release it was generated from: 6.8.0 matches API 6.8.0. The SDK is not republished for every API release, so later additions, such as the streets cursor `starting_after`, are not in 6.8.0. Pin an exact version, as [Installation](/sdk/installation) explains. See [Versions](/concepts/versions).