Suggest a country from a visitor's IP address

Combine IP and country lookups in a browser form that suggests a country on request and always preserves the visitor's manual edits.

JavaScript (browser)Last tested Get the complete example ↓
On this page

Working example

HTML + JavaScript

Before you start

Build an editable country field with a Suggest my country button. Clicking it looks up the current browser connection with /ip, then requests the country's name and calling code with /country/{code}. The country stays editable throughout.

Create a Public key on the keys page. Allow localhost and the hostname where you will publish the form. Replace YOUR_PUBLIC_API_KEY in the complete HTML example, then serve the downloaded file locally. A parse_public_ key belongs in browser code; keep secret keys on your server.

From the directory containing suggest-country-from-ip.html, run:

Terminal
python3 -m http.server 8000

Open the local example. Use the allowed localhost hostname, rather than opening the file with file://.

The example uses a two-letter country input to keep the complete file small. You can apply the same behavior to your existing country selector. Match options by their country codes and leave the choice editable.

1. Ask before making the suggestion

The form starts empty and sends no requests on page load. The button names the action, and the adjacent copy explains that the suggestion comes from the internet connection.

HTML
<form id="country-form" aria-busy="false">
  <div class="lookup">
    <label for="country">Country code
      <input id="country" name="country" type="text" autocomplete="country" spellcheck="false"
        autocapitalize="characters" maxlength="2" placeholder="US" aria-describedby="country-help">
    </label>
    <button type="submit">Suggest my country</button>
  </div>
  <p id="country-help" class="note">Use a two-letter country code, such as US, GB, or FR. You can always edit it.</p>
  <p id="status" role="status" aria-live="polite" aria-atomic="true" data-error="false">Your connection is only checked when you click.</p>
  <div id="details" class="result" hidden></div>
</form>

This workflow does not use browser geolocation or request device-location permission. It asks parseAPI about the public IP on the browser's request. The visitor can skip the lookup and type a country code instead.

Treat this as an optional convenience. The country of a connection is not necessarily where someone lives, receives deliveries, or wants their account based. A VPN, a corporate network, or travel can make the suggestion unsuitable.

2. Compose the two lookups

Call the bare /ip route from the browser to get the connection's IP information. Read its country field and use that code for the country-detail request.

JavaScript
async function getJSON(path, signal) {
  if (!API_KEY.startsWith('parse_public_')) {
    throw new Error('Add your public API key and allow this hostname in its domain list.');
  }
  const url = new URL(`https://api.parseapi.com${path}`);
  url.search = new URLSearchParams({ key: API_KEY });
  const response = await fetch(url, { signal });
  if (!response.ok) {
    const messages = {
      401: 'Check your public API key.',
      403: 'Check your public key, allowed hostnames, and account access.',
      404: 'Country details are unavailable. Enter your country code manually.',
      429: 'The request limit was reached. Enter your country code manually.'
    };
    throw new Error(messages[response.status] || 'The suggestion is unavailable. Enter your country code manually.');
  }
  return response.json();
}

async function suggestCountry(signal) {
  // Calling the bare /ip route from the browser looks up this connection.
  const ip = await getJSON('/ip', signal);
  if (!ip || typeof ip.country !== 'string' || !/^[A-Z]{2}$/.test(ip.country)) {
    throw new Error('Your connection did not return a country. Enter your country code manually.');
  }
  const data = await getJSON(`/country/${encodeURIComponent(ip.country)}`, signal);
  if (!data || data.country !== ip.country || typeof data.name !== 'string') {
    throw new Error('Country details are unavailable. Enter your country code manually.');
  }
  return data;
}

The two calls are deliberately sequential: the country code is the input to the second call. If the IP response has no country, stop and leave manual entry available. An absent country should never silently become a hardcoded default.

The /ip call belongs in the browser for this example. Moving that same call to your server would look up your server's connection. If you build a server-rendered variation, consult the IP API reference and pass a visitor IP obtained from your own trusted request handling.

Check both HTTP responses before reading their fields. The complete example handles rejected keys, limits, missing country data, unreadable responses, and a ten-second timeout across the whole sequence.

3. Fill the country without taking away control

A successful country response supplies a two-letter country code and a readable name. Show the calling code when it is available; leave missing optional details out of the display.

JavaScript
function fillCountry(data) {
  country.value = data.country;
  const lines = [`Suggested country: ${data.name} (${data.country}).`];
  if (typeof data.calling_code === 'string') lines.push(`Calling code: ${data.calling_code}.`);
  details.replaceChildren(...lines.map((text) => {
    const line = document.createElement('p');
    line.textContent = text;
    return line;
  }));
  details.hidden = false;
  setStatus('Country suggested. Check it and edit the code if needed.');
}

The field receives the country code, while the result explains which country was suggested. Use DOM text properties for response values instead of inserting them as HTML.

The example applies the suggestion only if the request is still current. Editing the country cancels the pending work and increments a revision counter. A late answer cannot replace the visitor's manual choice, even if it arrives after cancellation.

When the visitor edits an existing suggestion, remove its old descriptive details so the page does not pair one country's code with another country's name. Clicking Suggest my country again explicitly replaces the current choice after a successful lookup. A failed attempt keeps the existing code.

Try the form

Click Suggest my country and inspect the browser's Network panel. There should be an /ip request followed by a /country/{code} request, with no deep parameter. The result depends on your current connection; it is not a fixed demonstration IP.

Change the country code manually and confirm the old country details disappear. Start another lookup and immediately type a different code. Your edit should survive when the request finishes.

Try the browser offline. The error should leave the field editable and preserve its current value. Also test a missing country in a mocked IP response: the form should explain that no country was available and skip the country-detail call.

Use the suggestion for the right job

An optional default can reduce typing. It should not decide someone's residence, nationality, legal eligibility, or shipping destination. Let the person make the final selection and validate that selection through your normal form handling.

Use the Country API reference when you want to add country details to the rest of your interface. For a more specific address step, autofill city and state from a ZIP code after the visitor supplies their postal code.

Complete example

One HTML file with the form, styles, and JavaScript. Replace YOUR_PUBLIC_API_KEY with your public key, then run it on a domain you have added to that key.

Download HTML
View and copy the complete source
suggest-country-from-ip.html
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Suggest a country from an IP address</title>
  <style>
    :root { color-scheme: light dark; --bg: #fff; --ink: #152029; --muted: #5e6b75; --line: #d9e2e7; --field: #f6f9fa; --accent: #007c91; --button: #c5f6ff; --error: #b42335; }
    @media (prefers-color-scheme: dark) {
      :root:not([data-theme="light"]) { --bg: #0b1015; --ink: #eff7fa; --muted: #94a6b2; --line: #2c3942; --field: #121b23; --accent: #67e8f9; --button: #67e8f9; --error: #fda4af; }
    }
    :root[data-theme="dark"] { color-scheme: dark; --bg: #0b1015; --ink: #eff7fa; --muted: #94a6b2; --line: #2c3942; --field: #121b23; --accent: #67e8f9; --button: #67e8f9; --error: #fda4af; }
    :root[data-theme="light"] { color-scheme: light; }
    * { box-sizing: border-box; }
    body { margin: 0; padding: 24px; background: var(--bg); color: var(--ink); font: 15px/1.5 system-ui, sans-serif; }
    main { max-width: 540px; margin: 0 auto; }
    h1 { margin: 0 0 6px; font-size: clamp(23px, 5vw, 30px); font-weight: 600; letter-spacing: -.035em; }
    p { margin: 0; }
    .intro, .note { color: var(--muted); }
    form { margin-top: 24px; }
    label { display: block; font-size: 13px; font-weight: 550; }
    input, button { font: inherit; }
    input { width: 100%; min-width: 0; margin-top: 7px; padding: 11px 12px; border: 1px solid var(--line); border-radius: 7px; background: var(--field); color: var(--ink); }
    :focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
    .lookup { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: end; gap: 12px; }
    .fields { display: grid; grid-template-columns: minmax(0, 1fr) 90px; gap: 12px; margin-top: 20px; }
    button { min-height: 46px; padding: 11px 16px; border: 0; border-radius: 7px; background: var(--button); color: #07313c; cursor: pointer; font-size: 14px; font-weight: 650; }
    button:disabled { opacity: .55; cursor: wait; }
    #status { margin-top: 14px; min-height: 44px; color: var(--muted); font-size: 13px; }
    #status[data-error="true"] { color: var(--error); }
    .note { border-top: 1px solid var(--line); margin-top: 12px; padding-top: 16px; font-size: 12px; }
    @media (max-width: 380px) { body { padding: 18px; } .lookup { grid-template-columns: 1fr; } button { width: 100%; } }

    [hidden] { display: none !important; }
    .actions > button { max-width: 100%; overflow-wrap: anywhere; }
    .actions { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 16px; }
    .secondary { background: var(--field); color: var(--ink); border: 1px solid var(--line); }
    .result { margin-top: 16px; font-size: 13px; }
    .result p + p { margin-top: 6px; }
    output { overflow-wrap: anywhere; display: block; margin-top: 14px; font-size: 13px; color: var(--accent); }
  </style>
</head>
<body>
  <main>
    <h1>A starting point. Your choice.</h1>
    <p class="intro">Suggest a country from your current internet connection, or enter one yourself.</p>
    <!-- example:form -->
    <form id="country-form" aria-busy="false">
      <div class="lookup">
        <label for="country">Country code
          <input id="country" name="country" type="text" autocomplete="country" spellcheck="false"
            autocapitalize="characters" maxlength="2" placeholder="US" aria-describedby="country-help">
        </label>
        <button type="submit">Suggest my country</button>
      </div>
      <p id="country-help" class="note">Use a two-letter country code, such as US, GB, or FR. You can always edit it.</p>
      <p id="status" role="status" aria-live="polite" aria-atomic="true" data-error="false">Your connection is only checked when you click.</p>
      <div id="details" class="result" hidden></div>
    </form>
    <!-- /example:form -->
    <p class="note">An IP suggests the country of a network connection. It does not establish residence, nationality, or a delivery address.</p>
  </main>
  <script>
    // Restrict this public key to your development and production hostnames.
    const API_KEY = 'YOUR_PUBLIC_API_KEY';
    const form = document.querySelector('form');
    const button = form.querySelector('button[type="submit"]');
    const status = document.querySelector('#status');
    let controller;
    let revision = 0;

    function setStatus(message, error = false) {
      status.textContent = message;
      status.dataset.error = String(error);
    }

    function cancelRequest() {
      revision += 1;
      controller?.abort();
      controller = undefined;
      button.disabled = false;
      form.setAttribute('aria-busy', 'false');
    }

    const country = document.querySelector('#country');
    const details = document.querySelector('#details');

    // example:request
    async function getJSON(path, signal) {
      if (!API_KEY.startsWith('parse_public_')) {
        throw new Error('Add your public API key and allow this hostname in its domain list.');
      }
      const url = new URL(`https://api.parseapi.com${path}`);
      url.search = new URLSearchParams({ key: API_KEY });
      const response = await fetch(url, { signal });
      if (!response.ok) {
        const messages = {
          401: 'Check your public API key.',
          403: 'Check your public key, allowed hostnames, and account access.',
          404: 'Country details are unavailable. Enter your country code manually.',
          429: 'The request limit was reached. Enter your country code manually.'
        };
        throw new Error(messages[response.status] || 'The suggestion is unavailable. Enter your country code manually.');
      }
      return response.json();
    }

    async function suggestCountry(signal) {
      // Calling the bare /ip route from the browser looks up this connection.
      const ip = await getJSON('/ip', signal);
      if (!ip || typeof ip.country !== 'string' || !/^[A-Z]{2}$/.test(ip.country)) {
        throw new Error('Your connection did not return a country. Enter your country code manually.');
      }
      const data = await getJSON(`/country/${encodeURIComponent(ip.country)}`, signal);
      if (!data || data.country !== ip.country || typeof data.name !== 'string') {
        throw new Error('Country details are unavailable. Enter your country code manually.');
      }
      return data;
    }
    // /example:request

    // example:result
    function fillCountry(data) {
      country.value = data.country;
      const lines = [`Suggested country: ${data.name} (${data.country}).`];
      if (typeof data.calling_code === 'string') lines.push(`Calling code: ${data.calling_code}.`);
      details.replaceChildren(...lines.map((text) => {
        const line = document.createElement('p');
        line.textContent = text;
        return line;
      }));
      details.hidden = false;
      setStatus('Country suggested. Check it and edit the code if needed.');
    }
    // /example:result

    country.addEventListener('input', () => {
      cancelRequest();
      details.hidden = true;
      details.replaceChildren();
      setStatus('Your choice is kept. Click Suggest my country again to replace it.');
    });

    form.addEventListener('submit', async (event) => {
      event.preventDefault();
      cancelRequest();
      details.hidden = true;
      details.replaceChildren();
      const current = revision;
      controller = new AbortController();
      const request = controller;
      let timedOut = false;
      const timeout = setTimeout(() => { timedOut = true; request.abort(); }, 10000);
      button.disabled = true;
      form.setAttribute('aria-busy', 'true');
      setStatus('Finding a country for this connection...');
      try {
        const data = await suggestCountry(request.signal);
        if (current !== revision) return;
        fillCountry(data);
      } catch (error) {
        if (current !== revision) return;
        setStatus(timedOut ? 'The suggestion timed out. Enter your country code manually.'
          : error instanceof TypeError ? 'Could not reach parseAPI. Enter your country code manually.'
          : error instanceof SyntaxError ? 'The response could not be read. Enter your country code manually.'
          : error.message, true);
      } finally {
        clearTimeout(timeout);
        if (current === revision) cancelRequest();
      }
    });
  </script>
</body>
</html>

Need a key? Create a public API key. For every field and parameter, see IP API reference and Country API reference.