IDCanopy Developers
Umbrella APIAddress Autocomplete

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

  1. 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.
  2. Debounce. Wait 200 to 300 ms after the last keystroke before calling predict.
  3. Cancel the request in flight. When a new keystroke passes the debounce, abort the previous predict (AbortController in 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.
  4. One session per address entry. Send no session on the first predict, keep the one the response returns, send it on every further predict and on the resolve, 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, so null or a dashed UUID gives a new session on every call.
  5. 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 store id.
  6. Show attribution next to the list when it is not empty.
  7. Read precision. Ask the user for the house number when it is street or area.
  8. Keep the field usable on errors. 502 and network failures are not "invalid address": show a try-again hint and let the user type by hand. Retry only 502, a few times, with a growing delay.
  9. Follow the ARIA combobox pattern, see Accessibility for a custom UI.
  10. 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.

On this page