Skip to main content

The model in one line

Vepler search is a two-step funnelsuggest as the user types, then open the full address record for the one they pick. You are billed per search, not per keystroke.

Keystrokes are free. A search costs 1 credit.

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.
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.

Quick start

Two calls: surface suggestions as the user types, then look up the one they choose.
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

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). 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.

Parameters

q
string
required
The search query. Anything from a partial postcode to a full address line.
source
string
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.
classification
string
default:"residential"
residential, commercial, or all. Filters address results by property class.
granularity
string
default:"address"
address (one row per property) or street (one row per named street / USRN — useful for early keystrokes and street-level pickers).
addressable_only
boolean
default:"false"
When true, excludes historical and superseded addresses — only live, addressable records.
lat
number
Latitude (WGS84, −90 to 90) of a centre point to make results proximity-aware. Must be paired with lng.
lng
number
Longitude (WGS84, −180 to 180). Must be paired with lat.
radius_meters
integer
default:"10000"
Radius around the centre point (1–200,000 m). Required when geo_mode=restrict.
geo_mode
string
default:"bias"
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.
limit
integer
default:"10"
Number of results, 1–100.
offset
integer
default:"0"
Pagination offset (max 10,000).
search_id
string (uuid)
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.

Response

The response is a standard list envelope. Address rows are the slim discovery projection:
data
array
The suggestions. type is address, postcode, location, etc.; address rows carry uprn.
search_id
string | null
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.
set_complete
boolean
true when data already holds the complete match set — you can filter further keystrokes locally instead of calling the API again.
has_more
boolean
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

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

uprn
string
required
The uprn of the chosen suggestion (1–12 digits). Its full record is returned and charged.
include
string
Comma-separated includes. Pass geography to add a geographic-context block (administrative and statistical geographies) to the record.
dataset
string
default:"os"
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); the record lookup is its own, separately deduplicated charge.

Response

Through the SDK (vepler.address.lookupUprn), the same fields are camelCased — line1, postTown, classificationCode.

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: 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 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 too.
1

Debounce keystrokes (~300ms)

Wait for a short pause in typing before firing. One request per settled query instead of one per character.
2

Set a minimum query length (3 chars)

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.
3

Cancel superseded requests

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).
4

Reuse the search_id

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.
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:

geo_mode=bias

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.

geo_mode=restrict

Hard cutoff. Only address results within radius_meters of the point are returned. Best for “search within this area” experiences. Requires radius_meters.

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:
1

First real query returns a candidate set

The response includes search_id. If set_complete is true, data is the entire match set.
2

Further keystrokes narrow locally

When set_complete is true, filter the rows you already hold client-side. No new request, no latency, nothing to meter.
3

Reuse search_id when you do call again

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). 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.

Debounce and gate short queries

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.

Open records only on selection

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.

Charged once per address, per period

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.

Cache within the period

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.

Filter locally when set_complete

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:
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.

Suggest throughput

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.

Daily spend cap

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 for 429 handling and backoff, and contact us 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

Address Billing & Sessions

Why charge-once beats session tokens, and what it means for your invoices.

Usage & Limits

Rate limits, 429 handling, and backoff.

API Reference

Full request/response schemas for every search parameter.

Authentication

API keys and the x-api-key header.