# Authentication
Source: https://docs.vepler.com/authentication
Learn how to authenticate with the Vepler API
## Overview
The Vepler API uses **API key authentication** for all requests. You must include your API key in the `x-api-key` header of every request.
Keep your API keys secure! Never expose them in client-side code or public repositories.
## Obtaining API Keys
Sign up at [app.vepler.com](https://app.vepler.com)
Go to **Settings → API Keys** in your dashboard
Click **"Create New Key"** and give it a descriptive name
Copy your key immediately - it won't be shown again
## Making Authenticated Requests
Include your API key in the `x-api-key` header:
```typescript TypeScript theme={null}
import { SDK } from '@vepler/sdk';
const vepler = new SDK({
apiKey: process.env.VEPLER_API_KEY
});
// Make a request
const response = await vepler.property.getV1PropertyLocationIds({
locationIds: 'p_0x000123456789'
});
```
```javascript JavaScript theme={null}
const response = await fetch('https://api.vepler.com/v1/property/p_0x000123456789', {
headers: {
'x-api-key': 'vpr_live_abc123...',
'Content-Type': 'application/json'
}
});
const result = await response.json();
```
```bash cURL theme={null}
curl -X GET "https://api.vepler.com/v1/property/p_0x000123456789" \
-H "x-api-key: vpr_live_abc123..." \
-H "Content-Type: application/json"
```
```python Python theme={null}
import requests
headers = {
'x-api-key': 'vpr_live_abc123...',
'Content-Type': 'application/json'
}
response = requests.get('https://api.vepler.com/v1/property/p_0x000123456789', headers=headers)
data = response.json()
```
## API Key Format
API keys use the prefix `vpr_live_` followed by a unique identifier:
```
vpr_live_sk_1234567890abcdef
```
## Security Best Practices
Never hardcode API keys. Use environment variables:
```typescript theme={null}
// Don't do this
const apiKey = 'vpr_live_sk_1234567890';
// Do this instead
const apiKey = process.env.VEPLER_API_KEY;
```
```bash .env theme={null}
VEPLER_API_KEY=vpr_live_sk_1234567890
```
Rotate your API keys regularly:
1. Generate a new key in the dashboard
2. Update your application with the new key
3. Verify the new key works
4. Delete the old key from the dashboard
Always make API calls from your server, never from client-side code. Your API key should never be exposed in browser JavaScript, mobile apps, or public repositories.
## Error Handling
Handle authentication errors gracefully:
```typescript theme={null}
try {
const response = await vepler.property.getV1PropertyLocationIds({
locationIds: 'p_0x000123456789'
});
if (response.errorResponse) {
console.error('API Error:', response.errorResponse.error);
}
} catch (error) {
if (error.statusCode === 401) {
console.error('Invalid or missing API key');
} else if (error.statusCode === 403) {
console.error('Insufficient permissions');
}
}
```
## Authentication Errors
| Status Code | Description | Solution |
| ----------- | -------------------------- | -------------------------------------------------------------------- |
| `401` | Invalid or missing API key | Check your API key is correct and included in the `x-api-key` header |
| `403` | Insufficient permissions | Your API key does not have access to this resource |
## Testing Authentication
Verify your setup with a health check endpoint:
```typescript TypeScript theme={null}
import { SDK } from '@vepler/sdk';
const vepler = new SDK({
apiKey: process.env.VEPLER_API_KEY
});
const health = await vepler.system.getV1PropertyHealth();
console.log(health);
```
```bash cURL theme={null}
curl -X GET "https://api.vepler.com/v1/property/health" \
-H "x-api-key: YOUR_API_KEY"
```
## Next Steps
Build your first integration
Explore all endpoints
Use the official SDK
Handle errors effectively
# Error Handling
Source: https://docs.vepler.com/concepts/errors
Understanding API error responses and how to handle them
## Error Response Structure
All API errors return a JSON response:
```json theme={null}
{
"error": {
"code": "authentication_required",
"message": "Invalid or missing API key"
}
}
```
## HTTP Status Codes
### Success Codes (2xx)
| Status | Description | When Used |
| ------------------ | ----------------- | -------------------- |
| **200 OK** | Request succeeded | Successful GET, POST |
| **201 Created** | Resource created | Successful creation |
| **204 No Content** | Success, no body | Successful DELETE |
### Client Error Codes (4xx)
| Status | Description |
| ---------------------------- | ---------------------------------------------- |
| **400 Bad Request** | Malformed request syntax or invalid parameters |
| **401 Unauthorised** | Missing or invalid API key |
| **403 Forbidden** | Valid API key but insufficient permissions |
| **404 Not Found** | Requested resource does not exist |
| **422 Unprocessable Entity** | Request body validation failed |
| **429 Too Many Requests** | Usage limit exceeded |
### Server Error Codes (5xx)
| Status | Description |
| ----------------------------- | ------------------------------- |
| **500 Internal Server Error** | Unexpected server error |
| **503 Service Unavailable** | Service temporarily unavailable |
## Error Handling with the SDK
The SDK returns typed responses that may contain either a success or error response:
```typescript theme={null}
import { SDK } from '@vepler/sdk';
const vepler = new SDK({ apiKey: process.env.VEPLER_API_KEY });
const response = await vepler.property.getV1PropertyLocationIds({
locationIds: 'p_0x000123456789'
});
// Check for successful response
if (response.propertyListResponse) {
console.log('Success:', response.propertyListResponse.result);
}
// Check for error response
if (response.errorResponse) {
console.error('Error:', response.errorResponse.error);
console.error('Code:', response.errorResponse.code);
}
```
## Error Handling with HTTP
```typescript theme={null}
const response = await fetch('https://api.vepler.com/v1/property/p_0x000123456789', {
headers: { 'x-api-key': process.env.VEPLER_API_KEY }
});
if (!response.ok) {
const error = await response.json();
switch (response.status) {
case 401:
console.error('Invalid API key');
break;
case 404:
console.error('Resource not found');
break;
case 429:
console.error('Usage limit exceeded — retry shortly');
break;
default:
console.error('Error:', error);
}
}
```
## Retry Strategy
Implement exponential backoff for transient errors (429, 5xx):
```typescript theme={null}
async function retryWithBackoff(
fn: () => Promise,
maxRetries = 3,
baseDelay = 1000
): Promise {
for (let i = 0; i < maxRetries; i++) {
try {
return await fn();
} catch (error) {
const err = error as { statusCode?: number };
// Don't retry client errors (except 429)
if (err.statusCode && err.statusCode >= 400 && err.statusCode < 500 && err.statusCode !== 429) {
throw error;
}
if (i === maxRetries - 1) throw error;
const delay = baseDelay * Math.pow(2, i);
console.log(`Retry ${i + 1}/${maxRetries} after ${delay}ms`);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
```
## Best Practices
Never assume API calls will succeed:
```typescript theme={null}
const response = await vepler.property.getV1PropertyLocationIds({
locationIds: id
});
if (response.errorResponse) {
// Handle error
console.error(response.errorResponse);
return null;
}
return response.propertyListResponse;
```
Handle different status codes appropriately:
```typescript theme={null}
function handleError(statusCode: number): void {
switch (statusCode) {
case 401:
console.error('Check your API key');
break;
case 404:
console.log('Resource not found');
break;
case 429:
console.log('Slow down — retry shortly');
break;
default:
console.error('Unexpected error:', statusCode);
}
}
```
Set reasonable timeouts:
```typescript theme={null}
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 5000);
try {
const response = await fetch('https://api.vepler.com/v1/property/health', {
headers: { 'x-api-key': process.env.VEPLER_API_KEY! },
signal: controller.signal
});
} finally {
clearTimeout(timeout);
}
```
## Next Steps
Understand usage metering
Handle paginated responses
# Pagination
Source: https://docs.vepler.com/concepts/pagination
Learn how to handle paginated responses in the Vepler API
## Overview
The Vepler API uses **offset-based pagination** for list and query endpoints. Requests accept `limit` and `offset` parameters, and responses indicate whether more data is available.
## Request Parameters
| Parameter | Type | Default | Description |
| --------- | ------- | ------- | ---------------------------------------------- |
| `limit` | integer | 25 | Number of items to return (varies by endpoint) |
| `offset` | integer | 0 | Number of items to skip |
## Response Format
Paginated responses include metadata alongside the results:
```json theme={null}
{
"success": true,
"result": [ ... ],
"totalSize": 1250,
"size": 25,
"hasMore": true,
"nextOffset": 25
}
```
| Field | Description |
| --------------------- | ------------------------------------------------- |
| `totalSize` / `total` | Total number of matching records |
| `size` | Number of records returned in this response |
| `hasMore` | Whether additional records exist beyond this page |
| `nextOffset` | The offset value to use for the next page |
## Basic Usage
### First Page
```typescript theme={null}
import { SDK } from '@vepler/sdk';
const vepler = new SDK({ apiKey: process.env.VEPLER_API_KEY });
const response = await vepler.property.getV1PropertyLocationIds({
locationIds: 'loc_123',
limit: '50',
offset: '0'
});
if (response.propertyListResponse) {
console.log(response.propertyListResponse.result);
console.log('Has more:', response.propertyListResponse.hasMore);
}
```
### Subsequent Pages
```typescript theme={null}
const page2 = await vepler.property.getV1PropertyLocationIds({
locationIds: 'loc_123',
limit: '50',
offset: '50'
});
```
## Complete Iteration Example
```typescript theme={null}
async function getAllProperties(locationId: string) {
const vepler = new SDK({ apiKey: process.env.VEPLER_API_KEY });
const allProperties: unknown[] = [];
let offset = 0;
const limit = 100;
while (true) {
const response = await vepler.property.getV1PropertyLocationIds({
locationIds: locationId,
limit: String(limit),
offset: String(offset)
});
if (!response.propertyListResponse) break;
allProperties.push(...response.propertyListResponse.result);
if (!response.propertyListResponse.hasMore) break;
offset += limit;
// Small delay between pages
await new Promise(resolve => setTimeout(resolve, 100));
}
return allProperties;
}
```
## Query Endpoint Pagination
For POST query endpoints, pagination parameters are in the request body:
```typescript theme={null}
const response = await vepler.property.postV1PropertyQuery({
area: [{ type: 'postcode', value: 'SW1A' }],
query: { beds: [2, 3] },
limit: 50,
offset: 0
});
// Next page
const page2 = await vepler.property.postV1PropertyQuery({
area: [{ type: 'postcode', value: 'SW1A' }],
query: { beds: [2, 3] },
limit: 50,
offset: 50
});
```
## Best Practices
* Use larger page sizes (50-100) for bulk operations
* Use smaller page sizes (10-25) for user-facing displays
* Check each endpoint's documentation for maximum allowed values
When iterating through many pages, add a small delay to avoid hitting usage limits:
```typescript theme={null}
await new Promise(resolve => setTimeout(resolve, 100));
```
Reduce response size by requesting only the fields you need:
```typescript theme={null}
const response = await vepler.property.getV1PropertyLocationIds({
locationIds: 'loc_123',
limit: '100',
offset: '0',
attributes: 'address,pricing'
});
```
## Next Steps
Handle errors effectively
Understand usage metering
# Address Billing & Sessions
Source: https://docs.vepler.com/guides/address-billing
How address search and record billing works on the Vepler API — why there are no session tokens, and how you are charged once per address.
## Overview
Most address and location workflows follow the same funnel:
1. **Discover** — a user types into a search box and you surface matching addresses (autocomplete / typeahead, or a postcode lookup). Each keystroke may fire a request.
2. **Select** — the user picks one result and you fetch the full, verified record for it via `GET /v1/address/uprn/{uprn}` (the address detail, geocode, and identifiers).
The question every billing model has to answer is: **how do you avoid charging for the same address twice across that funnel** — once for surfacing it in autocomplete, and again for fetching its details?
Vepler answers this with a **sessionless, entity-keyed** model:
**Discovery is free per request. A search that forms a result set costs 1 credit. The full record is charged once per address, per billing period.** You never manage a session token — we deduplicate on the address itself, on the server.
***
## Two ways to bill a search funnel
### The session-token model
Some mapping and autocomplete APIs solve double-billing with a **session token**. Your client generates a token, attaches it to every autocomplete keystroke, and then attaches the *same* token to the final "fetch details" call. The provider bundles everything carrying that token into **one billable session**, so the keystrokes and the final fetch are billed together rather than separately.
It works, but it pushes real responsibility onto your client:
* You must **generate, attach, and reset** the token at exactly the right moments.
* The token typically **expires after a few minutes**; get the lifecycle wrong and you are billed twice, or not at all.
* It only deduplicates **within one session** — the same address fetched again tomorrow, or fetched through a different endpoint, is a brand-new charge.
### The direct-access (sessionless) model — what Vepler uses
Vepler does not use session tokens. There is nothing for your client to generate, attach, or reset. Instead, billing is keyed on the **address entity itself** — its UPRN (Unique Property Reference Number), the stable identifier the rest of the API already uses as the `locationId`.
When you retrieve an address, we record that **this account has paid for this UPRN in this billing period**. Any later retrieval of the same UPRN — same endpoint or different, one minute later or three weeks later — recognises that ledger entry and is **not charged again** until the next period.
This is why we call it *direct access*: you call the endpoint you need, directly, with no session bookkeeping. The deduplication happens server-side on data we control (the returned UPRN), not on a token your client has to manage correctly.
***
## Why direct access is the better fit
We chose the entity-keyed model deliberately. The logic:
No token to generate, attach, expire, or reset. A category of integration bugs simply does not exist for you.
Autocomplete → `/address/uprn` → `/property` all collapse onto the same UPRN. A session token can only deduplicate within a single session; the address key spans endpoints and products.
You are billed for **distinct addresses retrieved in a period**, not for request volume. Retries, refreshes, and repeated lookups of an address you already paid for do not add to your bill.
Because the charge is tied to the address and the period — not to a short-lived session — you can cache a retrieved record for the rest of the period and re-fetch it freely if you need to.
***
## How charging works
### Discovery is free
Surfacing addresses so a user can pick one is **not charged per keystroke**. Use autocomplete and postcode search as freely as your rate limits allow — typeahead does not run up a bill. (Candidate-set search adds one flat cost per search: when a query narrows to a result set — a `search_id` is issued — that session settles a **1-credit charge**, automatically and unconditionally, whether or not the user selects anything. See [Search & Autocomplete](/guides/search-and-autocomplete#how-a-search-is-billed).)
Discovery returns enough to identify and display a result — including its **UPRN**, which is the key you carry into the retrieval step.
```typescript theme={null}
import { SDK } from '@vepler/sdk';
const vepler = new SDK({ apiKeyAuth: process.env.VEPLER_API_KEY });
// Free: surface candidate addresses as the user types.
const suggestions = await vepler.search.searchSuggest({ q: '10 Downing' });
// The user picks an address row; carry its UPRN into the (charged) retrieval step.
const chosenUprn = suggestions.data.find((r) => r.type === 'address' && r.uprn)?.uprn;
```
### Retrieval is charged — once per address, per period
Fetching the full record for an address is the **chargeable** step. The first time you retrieve a given UPRN in a billing period, it is charged. Every subsequent retrieval of that **same** UPRN in the **same** period is recognised as already paid and costs nothing.
```typescript theme={null}
// Charged the first time this UPRN is retrieved this period.
const first = await vepler.address.lookupUprn({ uprn: chosenUprn });
// Same UPRN, same period — recognised as already paid, not charged again.
const again = await vepler.address.lookupUprn({ uprn: chosenUprn });
```
The billing period is currently the **calendar month** (UTC). Retrieving an address you already paid for earlier in the same month is free; retrieving it again in a new month is a new charge.
### A worked example
They type "10 Downing Street" into your search box. Autocomplete fires several requests and surfaces matching addresses. **Cost: nothing per request** — and when the query narrows to a result set, the search settles its flat 1 credit automatically.
You fetch the full record for the chosen UPRN via `GET /v1/address/uprn/{uprn}`. **Cost: one charge** for that address.
The same user (or another user on your account) retrieves that UPRN again this month — from a detail page, a refresh, or a different endpoint. **Cost: nothing** — already paid this period.
The same address is retrieved again in a new billing period. **Cost: one charge** — a new period resets the ledger.
The funnel that surfaced the address and then fetched it is billed as **one search plus one address**, not as a stack of per-request charges.
***
## What this means for your integration
If you are porting from a session-token API, you can drop all token generation and lifecycle code. Call the endpoints directly.
A retried retrieval after a timeout does not double-charge — the address is already claimed for the period. Retry with confidence.
Store retrieved records and reuse them for the rest of the period. Re-fetching an address you have already paid for is free, so caching is an optimisation, not a billing necessity.
Bulk and batch retrievals are billed per **distinct address** returned, deduplicated the same way as individual calls.
### Free tiers and licensed access
Some accounts hold a licence grant that **zero-rates** address retrieval. Where that applies, retrieval is free for that account, but the model is otherwise identical — addresses are still recorded as retrieved, simply at no cost. There is nothing different to do in your integration.
***
## Frequently asked questions
No. Individual keystrokes are free. The charge lands when you fetch the full record for an address. One nuance for candidate-set search: each search that narrows to a result set (a `search_id` is issued) settles a flat 1-credit charge, automatically and regardless of whether a record is then opened — see [Search & Autocomplete](/guides/search-and-autocomplete#how-a-search-is-billed).
Once. The first retrieval in the billing period is charged; the other nine are recognised as already paid and cost nothing.
No. Billing is keyed on the address, not on the request. A retry re-claims the same address, sees it is already paid for the period, and is not charged again.
The UPRN. Two retrievals are deduplicated when they return the same UPRN, regardless of which endpoint surfaced or fetched it, or how the identifier was formatted.
At the start of the next billing period (currently the calendar month). The same address retrieved in a new period is a new charge.
No. There is no session token. Direct access means you call the endpoint you need with no session bookkeeping — deduplication is handled for you on the server. (Suggest responses do carry an optional, server-minted `search_id` you can echo for faster narrowing, but it is never required and never affects what you are charged — see [Search & Autocomplete](/guides/search-and-autocomplete).)
***
## Next steps
How requests are metered and how to optimise throughput.
Explore the address and search endpoints.
# Property Attributes & Query Builder
Source: https://docs.vepler.com/guides/property-attributes
Discover every property attribute, what it costs, and compose queries visually
## Overview
The property endpoints serve several hundred attributes, selected with the `attributes` parameter as dotted paths:
```json theme={null}
{
"area": [{ "type": "postcode", "value": "SW1A" }],
"attributes": ["address", "epc.rating.current.band", "pricing.currentSale"],
"limit": 25
}
```
Two tools tell you exactly what is available:
* **The attribute catalogue** — `GET /v1/property/attributes`, a free, machine-readable list of every attribute.
* **The query builder** — a visual composer in the [dashboard](https://app.vepler.com/dashboard/query-builder) that shows live per-record cost as you pick attributes and generates the request for you.
## The attribute catalogue
```bash theme={null}
curl https://api.vepler.com/v1/property/attributes \
-H "x-api-key: $VEPLER_API_KEY"
```
The response covers, for every attribute:
| Field | Meaning |
| -------------------- | ---------------------------------------- |
| `path` | The dotted path accepted by `attributes` |
| `type` | JSON type of the value |
| `tier` | The billing tier requesting it triggers |
| `requiresPermission` | Extended permission needed, or `null` |
Plus the request vocabulary for `POST /v1/property/query`:
* `filterFields` — the whitelisted aliases accepted by `query[].groups[].conditions[].field`. **Filtering uses different names from attribute paths** — you project `roomDetails.beds` but filter on `beds`. Fields outside this list are rejected.
* `sortFields` — the fields accepted by `sort[].field`.
* `comparators` — the operators accepted by conditions.
* `tiers` — credit price per tier, and `pencePerCredit` to convert to GBP.
* `grantedPermissions` — the extended permissions your key holds. Attributes gated behind a permission you do not hold are silently omitted from responses, never a `403`.
The catalogue endpoint is free to call and safe to cache.
## Cost model
Attributes are priced in cumulative tiers, per property record returned. Requesting anything beyond the core identity/address set triggers the corresponding tier; any paid tier also charges Property detail, and Core is always charged. The catalogue's `tiers` array carries the current credit prices — or open the query builder to see the total move as you select.
## Requesting whole groups
A parent path serves the entire object beneath it — `"attributes": ["epc"]` returns the full EPC block. The query builder does this automatically when you select every field in a group.
# Usage & Limits
Source: https://docs.vepler.com/guides/rate-limiting
Understanding API usage metering and how to optimise your requests
## Overview
The Vepler API meters usage on a per-request basis. Your API usage is tracked and billed according to your subscription plan. If you exceed your plan's limits, the API will return a `429 Too Many Requests` response.
## Handling 429 Responses
When you exceed your usage limits, the API returns a 429 status code:
```json theme={null}
{
"error": {
"code": "rate_limit_exceeded",
"message": "Too many requests. Please retry shortly."
}
}
```
### Retry with Exponential Backoff
Implement exponential backoff when you receive a 429 response:
```typescript theme={null}
async function requestWithBackoff(
fn: () => Promise,
maxRetries = 3,
baseDelay = 1000
): Promise {
for (let i = 0; i < maxRetries; i++) {
try {
return await fn();
} catch (error) {
if (error.statusCode !== 429) throw error;
if (i === maxRetries - 1) throw error;
const delay = Math.min(baseDelay * Math.pow(2, i), 30000);
console.log(`Rate limited, retrying in ${delay}ms...`);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
```
## Best Practices
### 1. Use Batch Endpoints
Where available, use batch endpoints to fetch multiple records in a single request:
```typescript theme={null}
import { SDK } from '@vepler/sdk';
const vepler = new SDK({ apiKey: process.env.VEPLER_API_KEY });
// Instead of individual requests
// const prop1 = await vepler.property.getV1PropertyLocationIds({ locationIds: 'id1' });
// const prop2 = await vepler.property.getV1PropertyLocationIds({ locationIds: 'id2' });
// Batch multiple IDs in one request
const response = await vepler.property.getV1PropertyLocationIds({
locationIds: 'id1,id2,id3'
});
```
### 2. Cache Responses
Cache frequently accessed data to reduce API calls:
```typescript theme={null}
const cache = new Map();
async function getCached(key: string, fetcher: () => Promise, ttlMs = 300_000): Promise {
const cached = cache.get(key);
if (cached && cached.expiry > Date.now()) {
return cached.data;
}
const data = await fetcher();
cache.set(key, { data, expiry: Date.now() + ttlMs });
return data;
}
```
### 3. Use Request Queuing
Queue requests to control throughput:
```typescript theme={null}
class RequestQueue {
private queue: Array<{ fn: () => Promise; resolve: (v: unknown) => void; reject: (e: unknown) => void }> = [];
private processing = false;
private delayMs: number;
constructor(requestsPerSecond: number) {
this.delayMs = 1000 / requestsPerSecond;
}
async add(fn: () => Promise): Promise {
return new Promise((resolve, reject) => {
this.queue.push({ fn, resolve: resolve as (v: unknown) => void, reject });
if (!this.processing) this.process();
});
}
private async process(): Promise {
this.processing = true;
while (this.queue.length > 0) {
const item = this.queue.shift();
if (!item) break;
try {
const result = await item.fn();
item.resolve(result);
} catch (error) {
item.reject(error);
}
if (this.queue.length > 0) {
await new Promise(resolve => setTimeout(resolve, this.delayMs));
}
}
this.processing = false;
}
}
// Usage
const queue = new RequestQueue(10);
const result = await queue.add(() =>
vepler.property.getV1PropertyLocationIds({ locationIds: 'p_0x000123456789' })
);
```
### 4. Select Only the Fields You Need
Use the `attributes` parameter to request only the fields you need, reducing response size and improving performance:
```typescript theme={null}
const response = await vepler.property.getV1PropertyLocationIds({
locationIds: 'p_0x000123456789',
attributes: 'address,pricing,roomDetails'
});
```
## Increasing Your Limits
If you need higher usage limits:
1. **Upgrade your plan** at [app.vepler.com](https://app.vepler.com)
2. **Contact sales** at [sales@vepler.com](mailto:sales@vepler.com) for enterprise requirements
## Next Steps
Handle errors effectively
Explore all endpoints
# Search & Autocomplete
Source: https://docs.vepler.com/guides/search-and-autocomplete
Build fast, accurate UK address autocomplete with Vepler — how session billing works (free keystrokes, one credit per search), every tuning lever, and how to keep your bill low.
## The model in one line
Vepler search is a **two-step funnel** — [`suggest`](#step-1-suggest) as the user types, then
[open the full address record](#step-2-select-the-full-address-record) for the one they pick. You
are billed **per search, not per keystroke**.
Autocomplete keystrokes cost nothing. When a query narrows to a **result set** (the suggest
response returns a `search_id`), that search settles a flat **1-credit charge** — automatically,
about ten minutes later, whether or not the user picks anything. The record they open is charged
**separately, once per address per billing period**: look up the **same address again within the
same period and you are not charged for it again**.
Broad early keystrokes that never narrow to a set are free with nothing to settle — the meter
only starts when a `search_id` is issued. See [How a search is billed](#how-a-search-is-billed).
Everything else in this guide is detail on those two calls: the levers that make results
sharper, the autocomplete patterns that make the box feel instant, and the handful of habits
that keep your invoice tracking real usage rather than request volume.
New to the billing side? This guide covers the search flow end-to-end. For the *why* behind
charge-once and how it compares to session-token pricing, read
[Address Billing & Sessions](/guides/address-billing).
***
## Quick start
Two calls: surface suggestions as the user types, then look up the one they choose.
```typescript SDK theme={null}
import { SDK } from '@vepler/sdk';
const vepler = new SDK({ apiKeyAuth: process.env.VEPLER_API_KEY });
// 1. Free — surface candidate addresses as the user types.
const suggest = await vepler.search.searchSuggest({ q: '10 Downing' });
// The user picks an address row; its `uprn` is the key you carry into the lookup.
const chosen = suggest.data.find((r) => r.type === 'address' && r.uprn);
if (!chosen?.uprn) return;
// 2. Charged (once per UPRN, per period) — fetch the full record on selection.
const record = await vepler.address.lookupUprn({ uprn: chosen.uprn });
console.log(record.formatted, record.coordinates);
```
```bash cURL theme={null}
# 1. Free discovery
curl -G "https://api.vepler.com/v1/search/suggest" \
-H "x-api-key: $VEPLER_API_KEY" \
--data-urlencode "q=10 Downing"
# 2. Charged lookup of the chosen UPRN
curl "https://api.vepler.com/v1/address/uprn/100023336956" \
-H "x-api-key: $VEPLER_API_KEY"
```
All requests authenticate with the `x-api-key` header and run **server-side** — your API key
must never reach the browser. Put the search endpoints behind your own backend and proxy the
results to your UI.
***
## Step 1: Suggest
```
GET /v1/search/suggest?q={query}
```
Fuzzy autocomplete across UK addresses and locations. It detects what the user is typing
(postcode, address, place name) and returns ranked suggestions. **The suggest response itself is
never charged** — the meter starts only when a query narrows to a result set and a `search_id` is
issued (see [How a search is billed](#how-a-search-is-billed)).
Address suggestions come back as a **discovery projection** — a label, the `uprn`, and the
matched substrings for highlighting. They deliberately carry **no coordinates, postcode, or
classification**; those arrive with the [full record lookup](#step-2-select-the-full-address-record).
### Parameters
The search query. Anything from a partial postcode to a full address line.
Restrict the sources searched: `address` (individual properties) or `location` (postcodes,
towns, places). Omit to search both. Pair with `source=address` for the most predictable
proximity behaviour.
`residential`, `commercial`, or `all`. Filters address results by property class.
`address` (one row per property) or `street` (one row per named street / USRN — useful for
early keystrokes and street-level pickers).
When `true`, excludes historical and superseded addresses — only live, addressable records.
Latitude (WGS84, −90 to 90) of a centre point to make results proximity-aware. Must be paired
with `lng`.
Longitude (WGS84, −180 to 180). Must be paired with `lat`.
Radius around the centre point (1–200,000 m). Required when `geo_mode=restrict`.
How the centre point is applied — `bias` (soft: near matches rank higher, distant matches
still returned) or `restrict` (hard: only address results within `radius_meters` are returned).
See [Proximity](#proximity-bias-vs-restrict).
Number of results, 1–100.
Pagination offset (max 10,000).
A candidate-set token from a previous suggest response. Pass it back on subsequent keystrokes
to narrow within the same match set without a new data fetch. See
[Candidate sets](#candidate-sets-narrow-without-re-fetching).
### Response
The response is a standard list envelope. Address rows are the slim discovery projection:
```json theme={null}
{
"object": "list",
"url": "/v1/search/suggest",
"data": [
{
"id": "loc-sw1a1aa",
"title": "SW1A 1AA",
"description": "Westminster, London",
"type": "postcode",
"source": "location",
"relevanceScore": 0.95
},
{
"id": "addr-100023336956",
"title": "10 Downing Street, London, SW1A 2AA",
"type": "address",
"source": "address",
"relevanceScore": 0.92,
"uprn": "100023336956",
"main_text": "10 Downing Street, London, SW1A 2AA",
"matched_substrings": [{ "offset": 0, "length": 2 }]
}
],
"has_more": true,
"total_count": 15,
"search_id": null,
"set_complete": false,
"metadata": { "processingTime": "45ms" }
}
```
The suggestions. `type` is `address`, `postcode`, `location`, etc.; address rows carry `uprn`.
Set once the query narrows to a small exact match set. Carry it into the next keystroke to
narrow within the same set. `null` when no candidate set has formed yet.
`true` when `data` already holds the **complete** match set — you can filter further keystrokes
locally instead of calling the API again.
Whether more results exist beyond this page (`limit`/`offset`).
Through the SDK, the same fields are camelCased — `searchId`, `setComplete`, `hasMore`,
`totalCount`, `mainText`, `matchedSubstrings`.
***
## Step 2: Select — the full address record
```
GET /v1/address/uprn/{uprn}
```
The user picked a suggestion — now fetch its full record from the address API. This is the
**chargeable** step: address lines, postcode, coordinates, and classification, keyed on the
`uprn` from the chosen suggestion.
### Parameters
The `uprn` of the chosen suggestion (1–12 digits). Its full record is returned and charged.
Comma-separated includes. Pass `geography` to add a geographic-context block (administrative
and statistical geographies) to the record.
Address dataset: `os` (OS AddressBase, default) or `paf` (Royal Mail PAF).
The lookup takes **no `search_id`** — there is nothing to link or complete. The search session
settles on its own (see [How a search is billed](#how-a-search-is-billed)); the record lookup is
its own, separately deduplicated charge.
### Response
```json theme={null}
{
"object": "address",
"uprn": "100023336956",
"line_1": "10 Downing Street",
"line_2": "",
"line_3": "",
"post_town": "London",
"county": "Greater London",
"postcode": "SW1A 2AA",
"formatted": "10 Downing Street, London, SW1A 2AA",
"coordinates": { "lat": 51.5034, "lng": -0.1276 },
"classification_code": "RD"
}
```
Through the SDK (`vepler.address.lookupUprn`), the same fields are camelCased — `line1`,
`postTown`, `classificationCode`.
| Status | Meaning |
| ------ | ----------------------------------------------------------------------- |
| `200` | Full record returned (and charged, unless already claimed this period). |
| `400` | Invalid `uprn` — must be 1–12 digits. |
| `404` | UPRN not found — **nothing is ledgered or charged**. |
***
## How a search is billed
The model is honest and simple: **a search that forms a result set costs 1 credit; the record you
open is charged once per address per month.** Never one charge per keystroke. The lifecycle:
| What happens | Charge |
| ----------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Keystrokes, and broad queries that never narrow to a set (**no `search_id`**) | **Free** — nothing to settle. |
| Query narrows to a **result set** (`search_id` issued) | A flat **1-credit session settlement** — always, whether or not the user selects anything. Settled automatically about ten minutes after the set is delivered. |
| The user opens a full record (`GET /v1/address/uprn/{uprn}`) | The **record charge** — once per UPRN, per billing period. |
The result set is the unit that costs: once a `search_id` is issued, that session settles exactly
one credit, on its own, with nothing for you to do. Narrowing within a set you already hold costs
nothing, and repeat searches that reuse a live set do not open a new session.
**You cannot be charged per keystroke — even if you ignore `search_id` entirely.** Sessions are
coalesced on the server: keystrokes that fetch a subset of a live candidate set attach to that
session rather than opening a new one. Echoing `search_id` is a performance optimisation, not a
billing requirement.
**Charge-once per address, per period.** The billing period is the **calendar month (UTC)**. The
first lookup of a UPRN in that period is charged; every later lookup of the same UPRN — same
endpoint or different, retry or refresh — is recognised as already paid (`X-Already-Charged:
true`) and is **not charged again until the next period**. Retries after a timeout never
double-charge. See [Address Billing & Sessions](/guides/address-billing) for the full model.
***
## Getting autocomplete right
A search box that fires a raw request on every keystroke feels laggy and burns your rate limit.
Four patterns make it feel instant. They are mostly about **latency and rate limits** — but
debouncing and a minimum length also form fewer throwaway result sets, which trims
[session settlements](#how-a-search-is-billed) too.
Wait for a short pause in typing before firing. One request per settled query instead of one
per character.
Below 2–3 characters, matches are too broad to be useful. Show a *"keep typing…"* hint
instead of querying. Fewer noisy round-trips, sharper first results.
Attach an `AbortController` to each request and abort the previous one when a new keystroke
fires. This prevents a slow early response from clobbering a fast later one (out-of-order
results).
Once a response returns a `search_id`, pass it back on the next keystroke. The server narrows
within the cached candidate set instead of re-running the whole search — faster for the user,
lighter on your rate limit.
```typescript theme={null}
// A minimal, correct autocomplete controller.
const MIN_QUERY_LENGTH = 3;
let inFlight: AbortController | null = null;
let searchId: string | undefined;
async function onQueryChange(q: string): Promise {
q = q.trim();
if (q.length < MIN_QUERY_LENGTH) return; // keep typing…
inFlight?.abort(); // supersede the previous request
const controller = new AbortController();
inFlight = controller;
const res = await vepler.search.searchSuggest(
{ q, searchId }, // reuse the candidate set
{ signal: controller.signal }, // supersede via AbortController
);
if (controller.signal.aborted) return;
searchId = res.searchId ?? undefined;
render(res.data);
}
```
Debounce \~300ms, minimum 3 characters, and abort-on-supersede is the combination we use in our
own products. It turns a chatty box into one clean request per intent.
***
## Flexibility — the tuning levers
### Proximity: bias vs restrict
Pass the map centre (or the user's location) as `lat`/`lng` to make results local. Two modes:
**Soft preference (default).** Nearby matches rank higher, but strong textual matches from
further away still appear. Best for a general search box where the user might mean somewhere
else.
**Hard cutoff.** Only address results within `radius_meters` of the point are returned. Best
for "search within this area" experiences. Requires `radius_meters`.
```typescript theme={null}
// "Addresses within 2km of the map centre, closest first."
const local = await vepler.search.searchSuggest({
q: 'high street',
source: 'address',
lat: 51.5034,
lng: -0.1276,
geoMode: 'restrict',
radiusMeters: 2000,
});
```
### Source and classification
* **`source=address`** for property-level typeahead; **`source=location`** for postcode / town
pickers. Omit to blend both.
* **`classification=residential`** (default) / `commercial` / `all` to scope address results to
the property class your product cares about.
### Granularity: address vs street
`granularity=street` collapses results to one row per named street (USRN). It shines on early
keystrokes and in "pick a street" flows, where a list of every flat on the road is noise.
### Candidate sets: narrow without re-fetching
When a query resolves to a small exact match set, the response carries a `search_id` and, often,
`set_complete: true`. That is your signal to stop calling the API:
The response includes `search_id`. If `set_complete` is `true`, `data` is the **entire**
match set.
When `set_complete` is `true`, filter the rows you already hold client-side. No new request,
no latency, nothing to meter.
If you need the server (e.g. `set_complete` was `false`), pass `search_id` back so it narrows
within the cached set rather than re-running the search.
Issuing the `search_id` is the **billable moment** — that session settles its flat 1-credit
charge automatically (see [How a search is billed](#how-a-search-is-billed)). Narrowing *within*
a set you already hold is free, so once `set_complete` is `true`, filter locally.
***
## Cost saving
You are billed **1 credit per search that forms a set** and **charge-once per address**, so keeping
your bill low comes down to two things: **form fewer throwaway sets**, and **never pay twice for
the same address** — the API already handles the second for you.
Individual keystrokes are free, but every query that narrows to a result set settles 1 credit.
Debouncing (\~300ms) and a minimum length form fewer throwaway sets — the cheapest saving
there is.
Each full-record lookup is a real charge. Call `/v1/address/uprn/{uprn}` only when a user
actually picks a result — never speculatively for every suggestion on screen.
The first lookup of a UPRN in the billing period (calendar month, UTC) is charged; later
lookups of that same UPRN are not charged again that period. Retries after a timeout do
**not** double-charge.
Store retrieved records and reuse them — re-fetching an address you've already been charged for
this period is not charged again, so caching is a latency win, not a billing necessity.
When the response already holds the full match set, narrow it client-side. Fewer round-trips,
lower latency.
### Billing transparency headers
Every charged response tells you exactly what happened, so you can reconcile without guessing:
| Header | Meaning |
| ------------------- | ------------------------------------------------------------------------- |
| `X-Credits-Charged` | Credits charged by **this** request (`0` if already claimed this period). |
| `X-Already-Charged` | `true` when this UPRN was already paid for this period — no new charge. |
| `X-Credits-Balance` | Your remaining credit balance after the request. |
```typescript theme={null}
const record = await vepler.address.lookupUprn({ uprn });
// Read the transparency headers off the raw response when you need them:
// X-Credits-Charged / X-Already-Charged / X-Credits-Balance
```
There is no session token to manage. `search_id` is minted by the server when a query first
narrows to a result set — echo it on subsequent suggest calls for faster narrowing, or don't:
server-side session coalescing keeps the billing identical either way. Record charges
deduplicate on the UPRN, on the server.
***
## Rate limits
Suggest and the charged lookups are metered separately.
Free discovery is rate-limited per account (600 requests/minute by default). Over the limit
returns `429` with a `Retry-After` header. Debouncing keeps you well under it.
Charged requests are also protected by a per-account daily credit cap (50,000 credits/day by
default), guarding against a runaway loop draining your balance.
Both limits are defaults and can be raised for your plan — see [Usage & Limits](/guides/rate-limiting)
for `429` handling and backoff, and [contact us](mailto:sales@vepler.com) for higher limits.
***
## FAQ
No. Individual keystrokes are free. The meter starts only when a query narrows to a result set
(a `search_id` is issued), and that session settles a flat 1 credit — see the next question.
If the query never narrowed to a set, nothing is charged. If it did form a set (`search_id`
issued), the flat **1-credit session settlement** is the only cost — and it applies regardless
of whether anything is selected. Nothing else is charged unless a full record is opened.
Debouncing and a minimum query length reduce how often throwaway sets form.
Once. The billing period is the calendar month (UTC): the first lookup of a UPRN in that
period is charged; the rest are recognised as already paid (`X-Already-Charged: true`) and are
not charged again until the next period.
No. Billing is keyed on the address, not the request. A retry re-claims the same UPRN, sees it
is already paid for the period, and is not charged again.
It's optional, and it only applies to suggest — no other endpoint accepts it. Passing it lets
the server narrow within a cached candidate set instead of re-running the search, which is
faster. Billing is safe either way: sessions are coalesced server-side, so a client that never
echoes `search_id` still cannot be charged per keystroke.
Discovery returns only what's needed to display and pick a result — label, UPRN, and match
highlights. Coordinates, postcode, and classification arrive with the (charged) full-record
lookup at `/v1/address/uprn/{uprn}`. This split is what keeps the type-ahead responses free.
No. `search_id` is always minted by the server — client-generated identifiers are never
accepted. Echo the `search_id` you receive and every request in that search is grouped and
attributed for you, in your usage dashboard and in billing.
***
## Next steps
Why charge-once beats session tokens, and what it means for your invoices.
Rate limits, `429` handling, and backoff.
Full request/response schemas for every search parameter.
API keys and the `x-api-key` header.
# Introduction
Source: https://docs.vepler.com/introduction
Welcome to the Vepler API documentation
## Welcome to Vepler API
The definitive API for UK property technology. Access 30M+ properties, 8M+ planning applications, 32K schools, and comprehensive safety data through a single, type-safe API.
```typescript theme={null}
import { SDK } from '@vepler/sdk';
const vepler = new SDK({ apiKey: process.env.VEPLER_API_KEY });
// Get comprehensive property data
const response = await vepler.property.getV1PropertyLocationIds({
locationIds: 'p_0x123456789'
});
```
## What Can You Build?
Build advanced property search with 30M+ UK properties. Filter by location, price, features, and more.
Create valuation models with historical prices, comparable sales, and market trends.
Analyse property markets, identify trends, and generate insights with comprehensive data.
Assess properties with planning history, crime data, school information, and more.
## Quick Start
Sign up at [app.vepler.com](https://app.vepler.com) and get your API key from the dashboard.
```bash theme={null}
npm install @vepler/sdk
```
```typescript theme={null}
import { SDK } from '@vepler/sdk';
const vepler = new SDK({ apiKey: 'vpr_live_...' });
const response = await vepler.property.getV1PropertyLocationIds({
locationIds: 'p_0x123456789'
});
```
Complete quick start guide
Browse all endpoints
SDK documentation
## Key Concepts
### Properties and Locations
```typescript theme={null}
// A location can have multiple properties (e.g., flats in a building)
const properties = await vepler.property.getV1PropertyLocationIds({
locationIds: 'loc_123'
});
// Get multiple properties at once
const batch = await vepler.property.getV1PropertyPropertyIdPropertyIds({
propertyIds: 'prop_1,prop_2,prop_3'
});
```
**Location ID**: Groups properties at the same address (e.g., flats 1-10 in a building)
**Property ID**: Unique identifier for a specific property unit
**Source ID**: Original identifier from data source (e.g., Land Registry UPRN)
### Advanced Querying
```typescript theme={null}
// Query with filters, pagination, and sorting
const results = await vepler.property.postV1PropertyQuery({
area: [{
type: 'point',
// GeoJSON order: [longitude, latitude]
coordinates: [-0.1278, 51.5074],
radius: 2000
}],
query: {
propertyType: ['flat'],
beds: [2, 3, 4],
priceMax: 50000000 // in pence
},
limit: 50,
offset: 0,
sortBy: 'price',
sortOrder: 'asc'
});
```
## Data Coverage
Our API provides access to comprehensive UK property data:
| Dataset | Coverage | Data Source |
| ------------------ | ------------------------------- | ----------------------------------- |
| **Properties** | 30M+ properties | Land Registry, VOA, Ordnance Survey |
| **Planning** | 8M+ applications | 350+ Local Planning Authorities |
| **Schools** | 32,000 schools | Ofsted, DfE |
| **Crime & Safety** | National coverage | Police.uk |
| **Price Paid** | Historical data from 1995 | HM Land Registry |
| **Listings** | Active and historical | Multiple providers |
| **EPC** | Energy performance certificates | MHCLG |
| **Companies** | Companies House data | Companies House |
### Geographic Coverage
* **England**: Complete coverage
* **Wales**: Complete coverage
* **Scotland**: Property and planning data
* **Northern Ireland**: Limited coverage
## API Features
Full TypeScript support with auto-completion and type checking
Query by location, price, features, and custom criteria
Fetch multiple properties in a single request
Property, planning, schools, safety, AVM, listings, address, and more
Offset-based pagination for navigating large datasets
Request only the data you need to optimise performance
## Next Steps
Set up API authentication
Build your first integration
Explore property endpoints
Use the TypeScript SDK
## Need Help?
* **Email**: [hello@vepler.com](mailto:hello@vepler.com)
* **Documentation**: Browse this site for guides and references
* **API Status**: Check service health via [/health](/api-reference/health/property-service-health-check) endpoints
# Quick Start
Source: https://docs.vepler.com/quickstart
Get started with the Vepler API
## Installation
```bash npm theme={null}
npm install @vepler/sdk
```
```bash yarn theme={null}
yarn add @vepler/sdk
```
```bash pnpm theme={null}
pnpm add @vepler/sdk
```
```bash bun theme={null}
bun add @vepler/sdk
```
## Basic Usage
```typescript theme={null}
import { SDK } from '@vepler/sdk';
const vepler = new SDK({
apiKey: process.env.VEPLER_API_KEY
});
// Get property by location ID
const response = await vepler.property.getV1PropertyLocationIds({
locationIds: 'p_0x000123456789'
});
if (response.propertyListResponse) {
console.log(response.propertyListResponse.result);
}
```
## Authentication
The API uses `x-api-key` header authentication:
```bash theme={null}
curl -X GET "https://api.vepler.com/v1/property/p_0x000123456789" \
-H "x-api-key: YOUR_API_KEY" \
-H "Content-Type: application/json"
```
## Available Endpoints
### Property
* `GET /v1/property/{locationIds}` - Get properties by location IDs
* `GET /v1/property/propertyId/{propertyIds}` - Get properties by property IDs
* `GET /v1/property/sources/{sourceIds}` - Get properties by source IDs
* `POST /v1/property/properties/by-slugs` - Get properties by slugs
* `POST /v1/property/query` - Query properties with advanced filters
### Planning
* `GET /v1/planning/{applicationIds}` - Get planning applications
* `GET /v1/planning/sources/{sourceIds}` - Get planning by source IDs
* `POST /v1/planning/query` - Query planning applications
### Schools
* `GET /v1/schools` - List schools
* `GET /v1/schools/{id}` - Get school by ID
* `GET /v1/schools/search/nearby` - Search nearby schools
### Safety
* `GET /v1/safety/crime` - Get crime data by location
* `GET /v1/safety/crime/stats` - Get aggregated crime statistics
### Address
* `POST /v1/address/resolve` - Resolve an address
* `GET /v1/address/postcodes/{postcode}` - Lookup a postcode
* `GET /v1/address/uprn/{uprn}` - Lookup by UPRN
### AVM
* `POST /v1/avm/predict` - Predict property value
* `POST /v1/avm/analysis` - Run valuation analysis
### Listings
* `GET /v1/listings/{id}` - Get a listing
* `POST /v1/listings/query` - Query listings
See the [full API reference](/api-reference/introduction) for all 80 endpoints.
## Next Steps
* [API Reference](/api-reference/introduction) - Full endpoint documentation
* [TypeScript SDK](/sdk/typescript) - SDK documentation
* [Authentication](/authentication) - Authentication details
# SDK Examples
Source: https://docs.vepler.com/sdk/examples/general
Practical code examples for using the Vepler SDK
## Setup
```typescript theme={null}
import { SDK } from '@vepler/sdk';
const vepler = new SDK({
apiKey: process.env.VEPLER_API_KEY
});
```
## Property Examples
### Get Properties by Location ID
```typescript theme={null}
async function getProperties() {
const response = await vepler.property.getV1PropertyLocationIds({
locationIds: 'p_0x000123456789'
});
if (response.propertyListResponse) {
const properties = response.propertyListResponse.result;
console.log(`Found ${properties.length} properties`);
for (const property of properties) {
console.log(property);
}
}
}
```
### Query Properties with Filters
```typescript theme={null}
async function searchProperties() {
const response = await vepler.property.postV1PropertyQuery({
area: [{
type: 'postcode',
value: 'SW1A'
}],
query: {
priceMin: 250000,
priceMax: 500000,
beds: [2, 3],
propertyType: ['flat', 'terraced']
},
limit: 25,
offset: 0,
sortBy: 'price',
sortOrder: 'desc'
});
if (response.propertyListResponse) {
console.log(`Total results: ${response.propertyListResponse.totalSize}`);
console.log(`Has more: ${response.propertyListResponse.hasMore}`);
}
}
```
### Get Properties by Slugs
```typescript theme={null}
async function getBySlug() {
const response = await vepler.property.postV1PropertyPropertiesBySlugs({
slugs: ['10-downing-street-london-sw1a-2aa'],
limit: 10
});
if (response.propertyListResponse) {
console.log(response.propertyListResponse.result);
}
}
```
## Planning Application Examples
### Search Planning Applications
```typescript theme={null}
async function searchPlanning() {
const response = await vepler.planning.postV1PlanningQuery({
query: {
councils: ['Westminster', 'Camden'],
statuses: ['approved', 'pending_decision'],
receivedDateFrom: '2024-01-01',
receivedDateTo: '2024-12-31'
},
limit: 50,
offset: 0,
sortBy: 'receivedDate',
sortOrder: 'desc'
});
if (response.planningListResponse) {
for (const app of response.planningListResponse.applications) {
console.log(`${app.key}: ${app.description}`);
console.log(` Status: ${app.status}`);
console.log(` Provider: ${app.provider}`);
}
}
}
```
## Pagination Example
```typescript theme={null}
async function getAllProperties(locationId: string) {
const allProperties: unknown[] = [];
let offset = 0;
const limit = 100;
while (true) {
const response = await vepler.property.getV1PropertyLocationIds({
locationIds: locationId,
limit: String(limit),
offset: String(offset)
});
if (response.propertyListResponse) {
allProperties.push(...response.propertyListResponse.result);
if (!response.propertyListResponse.hasMore) {
break;
}
offset += limit;
} else {
break;
}
// Small delay between pages
await new Promise(resolve => setTimeout(resolve, 100));
}
return allProperties;
}
```
## Error Handling Example
```typescript theme={null}
async function robustFetch(id: string) {
const maxRetries = 3;
let retries = 0;
while (retries < maxRetries) {
try {
const response = await vepler.property.getV1PropertyLocationIds({
locationIds: id
});
if (response.errorResponse) {
console.error('API error:', response.errorResponse.error);
return null;
}
return response.propertyListResponse;
} catch (error) {
const err = error as { statusCode?: number };
if (err.statusCode === 404) {
console.log('Property not found');
return null;
}
if (err.statusCode === 429) {
retries++;
const delay = Math.pow(2, retries) * 1000;
console.log(`Rate limited. Retry ${retries}/${maxRetries} in ${delay}ms`);
await new Promise(resolve => setTimeout(resolve, delay));
continue;
}
if (err.statusCode && err.statusCode >= 500) {
retries++;
const delay = Math.pow(2, retries) * 1000;
console.log(`Server error. Retry ${retries}/${maxRetries} in ${delay}ms`);
await new Promise(resolve => setTimeout(resolve, delay));
continue;
}
throw error;
}
}
throw new Error('Max retries exceeded');
}
```
## Batch Operations Example
```typescript theme={null}
async function batchLookup(ids: string[]) {
// Use comma-separated IDs for batch lookups
const batchSize = 10;
const results: unknown[] = [];
for (let i = 0; i < ids.length; i += batchSize) {
const batch = ids.slice(i, i + batchSize);
const response = await vepler.property.getV1PropertyLocationIds({
locationIds: batch.join(',')
});
if (response.propertyListResponse) {
results.push(...response.propertyListResponse.result);
}
// Delay between batches
if (i + batchSize < ids.length) {
await new Promise(resolve => setTimeout(resolve, 200));
}
}
return results;
}
```
## Next Steps
Full API documentation
Interactive API reference
# Installation
Source: https://docs.vepler.com/sdk/installation
How to install and set up the Vepler SDK
## Requirements
Before installing the Vepler SDK, ensure you have:
* Node.js 18.0 or higher
* npm, yarn, pnpm, or bun package manager
* A Vepler API key ([get one here](https://app.vepler.com))
## Installation
Choose your preferred package manager:
```bash theme={null}
npm install @vepler/sdk
```
```bash theme={null}
yarn add @vepler/sdk
```
```bash theme={null}
pnpm add @vepler/sdk
```
```bash theme={null}
bun add @vepler/sdk
```
## Verify Installation
Check that the SDK is installed correctly:
```bash theme={null}
npm list @vepler/sdk
```
## Basic Setup
### 1. Import the SDK
```typescript TypeScript theme={null}
import { SDK } from '@vepler/sdk';
```
```javascript CommonJS theme={null}
const { SDK } = require('@vepler/sdk');
```
### 2. Initialise with API Key
```typescript theme={null}
const vepler = new SDK({
apiKey: 'vpr_live_abc123...'
});
```
### 3. Make Your First Call
```typescript theme={null}
const response = await vepler.property.getV1PropertyLocationIds({
locationIds: 'p_0x000123456789'
});
if (response.propertyListResponse) {
console.log(response.propertyListResponse.result);
}
```
## Environment Variables
Never hardcode API keys in your source code. Use environment variables instead.
### Create .env File
```bash .env theme={null}
VEPLER_API_KEY=vpr_live_abc123...
```
### Load Environment Variables
```typescript Node.js theme={null}
import dotenv from 'dotenv';
import { SDK } from '@vepler/sdk';
dotenv.config();
const vepler = new SDK({
apiKey: process.env.VEPLER_API_KEY!
});
```
```typescript Next.js theme={null}
// Environment variables loaded automatically
import { SDK } from '@vepler/sdk';
const vepler = new SDK({
apiKey: process.env.VEPLER_API_KEY!
});
```
```typescript Vite theme={null}
import { SDK } from '@vepler/sdk';
const vepler = new SDK({
apiKey: import.meta.env.VITE_VEPLER_API_KEY
});
```
## Framework-Specific Setup
```typescript theme={null}
// app/lib/vepler.ts
import { SDK } from '@vepler/sdk';
const vepler = new SDK({
apiKey: process.env.VEPLER_API_KEY!
});
export default vepler;
```
```typescript theme={null}
// app/api/property/route.ts
import vepler from '@/lib/vepler';
export async function GET(request: Request) {
const response = await vepler.property.getV1PropertyLocationIds({
locationIds: 'p_0x000123456789'
});
return Response.json(response.propertyListResponse);
}
```
```typescript theme={null}
// src/config/vepler.ts
import { SDK } from '@vepler/sdk';
const vepler = new SDK({
apiKey: process.env.VEPLER_API_KEY!
});
export default vepler;
```
```typescript theme={null}
// src/routes/property.ts
import express from 'express';
import vepler from '../config/vepler';
const router = express.Router();
router.get('/property/:id', async (req, res) => {
try {
const response = await vepler.property.getV1PropertyLocationIds({
locationIds: req.params.id
});
res.json(response.propertyListResponse);
} catch (error) {
res.status(500).json({ error: (error as Error).message });
}
});
```
```typescript theme={null}
// vepler.module.ts
import { Module, Global } from '@nestjs/common';
import { SDK } from '@vepler/sdk';
@Global()
@Module({
providers: [
{
provide: 'VEPLER_SDK',
useFactory: () => {
return new SDK({
apiKey: process.env.VEPLER_API_KEY!
});
}
}
],
exports: ['VEPLER_SDK']
})
export class VeplerModule {}
```
```typescript theme={null}
// property.service.ts
import { Injectable, Inject } from '@nestjs/common';
import { SDK } from '@vepler/sdk';
@Injectable()
export class PropertyService {
constructor(@Inject('VEPLER_SDK') private vepler: SDK) {}
async getProperty(id: string) {
return this.vepler.property.getV1PropertyLocationIds({
locationIds: id
});
}
}
```
## TypeScript Configuration
The SDK includes TypeScript definitions. Recommended tsconfig settings:
```json tsconfig.json theme={null}
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"lib": ["ES2020"],
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"moduleResolution": "node"
}
}
```
## Troubleshooting
Ensure the package is installed:
```bash theme={null}
npm install @vepler/sdk
```
Clear your package manager cache:
```bash theme={null}
npm cache clean --force
```
Update TypeScript to latest version:
```bash theme={null}
npm install -D typescript@latest
```
Ensure your tsconfig.json has:
```json theme={null}
{
"compilerOptions": {
"moduleResolution": "node",
"esModuleInterop": true
}
}
```
Check that:
* The key starts with `vpr_live_`
* No extra spaces or quotes around the key
* The key has not expired
## Version Management
### Check Installed Version
```bash theme={null}
npm list @vepler/sdk
```
### Update to Latest
```bash theme={null}
npm update @vepler/sdk
```
## Next Steps
Full SDK usage guide
See working examples
Authentication details
Full endpoint documentation
# TypeScript SDK
Source: https://docs.vepler.com/sdk/typescript
Official Vepler TypeScript SDK documentation
## Installation
```bash npm theme={null}
npm install @vepler/sdk
```
```bash yarn theme={null}
yarn add @vepler/sdk
```
```bash pnpm theme={null}
pnpm add @vepler/sdk
```
```bash bun theme={null}
bun add @vepler/sdk
```
## Basic Usage
```typescript theme={null}
import { SDK } from '@vepler/sdk';
// Initialise the SDK
const vepler = new SDK({
apiKey: process.env.VEPLER_API_KEY
});
// Make your first request
const response = await vepler.property.getV1PropertyLocationIds({
locationIds: 'p_0x000123456789'
});
if (response.propertyListResponse) {
console.log(response.propertyListResponse.result);
}
```
## Authentication
The SDK uses `x-api-key` header authentication. Get your API key from the [Vepler dashboard](https://app.vepler.com).
```typescript theme={null}
const vepler = new SDK({
apiKey: 'vpr_live_...'
});
```
## Available Modules
The SDK provides access to all API modules:
### Property Module
```typescript theme={null}
// Get properties by location IDs
const locationResponse = await vepler.property.getV1PropertyLocationIds({
locationIds: 'UK-12345678,UK-87654321',
limit: '10',
offset: '0'
});
// Get properties by property IDs
const propertyResponse = await vepler.property.getV1PropertyPropertyIdPropertyIds({
propertyIds: 'PROP-123,PROP-456'
});
// Get properties by source IDs
const sourceResponse = await vepler.property.getV1PropertySourcesSourceIds({
sourceIds: 'provider-a::123,provider-b::456'
});
// Get properties by slugs
const slugResponse = await vepler.property.postV1PropertyPropertiesBySlugs({
slugs: ['property-slug-1', 'property-slug-2'],
limit: 25
});
// Query properties with advanced filters
const queryResponse = await vepler.property.postV1PropertyQuery({
area: [{
type: 'postcode',
value: 'SW1A'
}],
query: {
priceMin: 250000,
priceMax: 500000,
beds: [2, 3]
},
limit: 25,
offset: 0
});
```
### Planning Module
```typescript theme={null}
// Get planning applications by IDs
const planningResponse = await vepler.planning.getV1PlanningApplicationIds({
applicationIds: 'APP-123,APP-456'
});
// Get planning applications by source IDs
const planningSourceResponse = await vepler.planning.getV1PlanningSourcesSourceIds({
sourceIds: 'council-a::APP/2024/0001'
});
// Query planning applications
const planningQueryResponse = await vepler.planning.postV1PlanningQuery({
query: {
councils: ['Westminster', 'Camden'],
statuses: ['approved', 'pending_decision'],
receivedDateFrom: '2024-01-01',
receivedDateTo: '2024-12-31'
},
limit: 50,
offset: 0,
sortBy: 'receivedDate',
sortOrder: 'desc'
});
```
### Health Module
```typescript theme={null}
// Check property service health
const propertyHealth = await vepler.system.getV1PropertyHealth();
// Check planning service health
const planningHealth = await vepler.system.getV1PlanningHealth();
if (propertyHealth.healthResponse?.status === 'healthy') {
console.log('Property service is operational');
}
```
## Error Handling
The SDK returns typed responses with proper error handling:
```typescript theme={null}
const response = await vepler.property.getV1PropertyLocationIds({
locationIds: 'UK-123'
});
// Check for successful response
if (response.propertyListResponse) {
console.log('Total properties:', response.propertyListResponse.totalSize);
console.log('Properties:', response.propertyListResponse.result);
}
// Check for error response
if (response.errorResponse) {
console.error('Error:', response.errorResponse.error);
}
// HTTP metadata is also available
console.log('Status:', response.statusCode);
console.log('Headers:', response.headers);
```
## TypeScript Support
The SDK is fully typed. All request and response types are available:
```typescript theme={null}
import { SDK } from '@vepler/sdk';
import type {
PropertyQueryRequest,
PropertyListResponse,
PlanningApplication,
ErrorResponse
} from '@vepler/sdk/models/components';
import type {
GetV1PropertyLocationIdsRequest,
GetV1PropertyLocationIdsResponse
} from '@vepler/sdk/models/operations';
const request: GetV1PropertyLocationIdsRequest = {
locationIds: 'UK-123',
limit: '10'
};
const response: GetV1PropertyLocationIdsResponse =
await vepler.property.getV1PropertyLocationIds(request);
```
## Pagination
Handle paginated responses using limit and offset:
```typescript theme={null}
async function getAllProperties(locationId: string) {
const properties: unknown[] = [];
let offset = 0;
const limit = 100;
while (true) {
const response = await vepler.property.getV1PropertyLocationIds({
locationIds: locationId,
limit: String(limit),
offset: String(offset)
});
if (response.propertyListResponse) {
properties.push(...response.propertyListResponse.result);
if (properties.length >= response.propertyListResponse.totalSize) {
break;
}
offset += limit;
} else {
break;
}
}
return properties;
}
```
## Advanced Configuration
```typescript theme={null}
import { SDK } from '@vepler/sdk';
const vepler = new SDK({
apiKey: process.env.VEPLER_API_KEY,
serverURL: 'https://api.vepler.com' // Optional, defaults to production
});
```
## API Reference
Property endpoints documentation
Planning endpoints documentation
Service health endpoints
View on npm
This SDK is auto-generated using [Speakeasy](https://speakeasy.com) from our OpenAPI specification. The SDK is regenerated whenever the API changes to ensure it stays in sync.