Build your own UI
The call sequence for a custom type-ahead field, with a framework-neutral example
If the widget does not fit your design system, your
mobile app or your backend flow, call predict and resolve yourself. The calls and fields are in the
integration guide; this page is the sequence your
client has to implement and a worked example.
Sequence
User Your field Your code Service
| types "Hau" | | |
|---------------->| input event | |
| |-------------------------->| wait 250 ms, text >= 3? |
| | | abort the previous request |
| | |--- POST predict ----------->|
| | | {input, session: none} |
| | |<-- {session: S, suggestions}|
| |<-- render list + attribution |
| types "Haupt" | | |
|---------------->|-------------------------->| (debounce, abort, same S) |
| | |--- POST predict ----------->|
| | | {input, session: S} |
| | |<-- {session: S, suggestions}|
| picks item 2 | | |
|---------------->|-------------------------->|--- POST resolve ----------->|
| | | {id, session: S} |
| | |<-- {address, verification} |
| |<-- fill the form fields | drop S |What your client must do
- Wait for 3 characters. Send nothing while the trimmed text is shorter than the service minimum (3 by default). Shorter input returns an empty list, not an error.
- Debounce. Wait 200 to 300 ms after the last keystroke before calling
predict. - Cancel the request in flight. When a new keystroke passes the debounce, abort the previous
predict(AbortControllerin a browser) and ignore anything that still arrives for it. Also cancel when the text drops below the minimum, so an old response cannot reopen the list. - One session per address entry. Send no
sessionon the firstpredict, keep the one the response returns, send it on every furtherpredictand on theresolve, then drop it. Start a new session when the user clears the field and starts over. The value must be letters and digits only; anything else is silently replaced, sonullor a dashed UUID gives a new session on every call. - Resolve on select. The structured address comes only from
resolve. Suggestion text is for display; do not fill form fields from it and do not parse or storeid. - Show
attributionnext to the list when it is not empty. - Read
precision. Ask the user for the house number when it isstreetorarea. - Keep the field usable on errors.
502and network failures are not "invalid address": show a try-again hint and let the user type by hand. Retry only502, a few times, with a growing delay. - Follow the ARIA combobox pattern, see Accessibility for a custom UI.
- Restrict countries with
countries(one or more codes, for example['DE']) if you serve specific markets, see Country and address type.
Example
Plain JavaScript, no framework. BASE is the Umbrella base URL plus /address/autocomplete (gateway
sub-path pending verification) or the Mock address, KEY is your bearer token. Keep the key on your backend
if the field runs in a public page, see the note on browser keys in the widget page.
const MIN_CHARS = 3;
const DEBOUNCE_MS = 250;
function createAddressField({ input, base, key, countries, render, onAddress, onError }) {
let session = null; // one per address entry
let timer = null;
let controller = null; // the request in flight
async function post(path, body, signal) {
const r = await fetch(base + path, {
method: 'POST',
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${key}` },
body: JSON.stringify(body),
signal,
});
if (!r.ok) {
const { detail } = await r.json().catch(() => ({}));
const err = new Error(typeof detail === 'string' ? detail : `HTTP ${r.status}`);
err.status = r.status;
throw err;
}
return r.json();
}
function cancel() {
clearTimeout(timer);
if (controller) controller.abort();
controller = null;
}
async function predict() {
const text = input.value.trim();
if (text.length < MIN_CHARS) { render([], ''); return; }
controller = new AbortController();
try {
const body = { input: text, session, countries };
const res = await post('/predict', body, controller.signal);
session = res.session;
render(res.suggestions, res.attribution); // draw the list, show attribution
} catch (err) {
if (err.name === 'AbortError') return; // superseded by a newer keystroke
render([], '');
onError(err); // keep the field editable
}
}
input.addEventListener('input', () => {
cancel(); // abort the old request, restart the debounce
if (input.value.trim().length < MIN_CHARS) { render([], ''); return; }
timer = setTimeout(predict, DEBOUNCE_MS);
});
return {
// call this when the user picks a suggestion
async select(suggestion) {
cancel();
input.value = suggestion.text;
try {
const res = await post('/resolve', { id: suggestion.id, session });
session = null; // the entry is finished
onAddress(res.address, res.verification);
} catch (err) {
onError(err); // 404: expired, run predict again
}
},
// call this when the user clears the field and starts over
reset() { cancel(); session = null; render([], ''); },
};
}Wiring it up:
const field = createAddressField({
input: document.querySelector('#address'),
base: BASE,
key: KEY,
countries: ['DE'],
render: (suggestions, attribution) => { /* draw the listbox, set aria-expanded, show attribution */ },
onAddress: (address) => {
document.querySelector('#street').value = address.street;
document.querySelector('#nr').value = address.house_number;
document.querySelector('#plz').value = address.postal_code;
document.querySelector('#city').value = address.city;
if (address.precision !== 'house') document.querySelector('#nr').focus();
},
onError: (err) => { /* err.status is 401, 404, 422, 502 or undefined for a network error */ },
});
// in your list: item click or Enter -> field.select(suggestions[i])Against Mock, which holds German and Austrian sample addresses only, set base to https://<this portal>/mock/api/services/address/autocomplete and leave the
Authorization header out; Mock needs no token. Try Haupt or Bahnhofstr 12 Wien: the data is invented, so
your own address will not be found.
Pending verification: the example follows the service source and has not been run against the live service.