Skip to main content

How to Track IP Geolocation in Salesforce using LWC and Apex Callouts

Tracking user locations based on IP addresses is a frequent requirement for security auditing, fraud detection, and creating localized customer experiences inside Salesforce. While legacy systems relied heavily on Visualforce, modern Salesforce architecture dictates using Lightning Web Components (LWC) backed by an Apex REST callout to fetch and display this geographic data securely.

In plain words: An LWC captures the IP address input by the user and passes it to an Apex class. The Apex class sends an HTTP request to an external geolocation API, receives a JSON package containing location details (like city, country, and time zone), parses it, and sends it back to the LWC to be displayed on the screen.

Key Points Summary

  • Use standard lightning-input and lightning-button elements to capture the IP address in your LWC.
  • Make API callouts through Apex rather than native JavaScript fetch() to bypass strict browser CORS restrictions.
  • Use @AuraEnabled on your Apex wrapper class properties so the LWC can easily read the parsed JSON data.
  • Always prefer Named Credentials over old Remote Site Settings to authorize your external API endpoints securely.

Step 1: Create the Apex Callout Controller

Because browser security policies (CORS) usually block direct API requests from the frontend, our Apex controller will act as a proxy. It constructs the HttpRequest, communicates with a geolocation endpoint (like ipapi.co), and deserializes the JSON response into an LWC-friendly wrapper class.

Real-Life Example: Apex Controller with REST Callout
Below is the Apex class. Notice that we throw an AuraHandledException if the API fails, which ensures the LWC catches a clean error message.
public with sharing class IPLocationTracker {

    @AuraEnabled
    public static IPDetails getIPDetails(String ipAddress) {
        Http http = new Http();
        HttpRequest request = new HttpRequest();
        
        // Dynamically build endpoint depending on user input
        // Note: For production, use a Named Credential instead of hardcoding the URL
        String endpoint = 'https://ipapi.co/';
        if (String.isNotBlank(ipAddress)) {
            endpoint += EncodingUtil.urlEncode(ipAddress, 'UTF-8') + '/json/';
        } else {
            endpoint += 'json/'; // Fetches data for the caller's IP
        }
        
        request.setEndpoint(endpoint);
        request.setMethod('GET');
        request.setTimeout(120000); 
        
        try {
            HttpResponse response = http.send(request);
            if (response.getStatusCode() == 200) {
                // Deserialize the JSON directly into our Wrapper Class
                return (IPDetails) JSON.deserialize(response.getBody(), IPDetails.class);
            } else {
                throw new CalloutException('Failed to fetch data. Status Code: ' + response.getStatusCode());
            }
        } catch (Exception ex) {
            // Throwing AuraHandledException passes the error cleanly to the LWC
            throw new AuraHandledException(ex.getMessage());
        }
    }

    // Wrapper class matching the API's JSON response schema
    // @AuraEnabled is required on each property for LWC visibility
    public class IPDetails {
        @AuraEnabled public String ip { get; set; }
        @AuraEnabled public String country_code { get; set; }
        @AuraEnabled public String country_name { get; set; }
        @AuraEnabled public String region { get; set; }
        @AuraEnabled public String city { get; set; }
        @AuraEnabled public String postal { get; set; }
        @AuraEnabled public String timezone { get; set; }
        @AuraEnabled public Decimal latitude { get; set; }
        @AuraEnabled public Decimal longitude { get; set; }
    }
}

Step 2: Build the LWC User Interface (HTML)

Next, we build a clean, modern user interface using standard Lightning base components. We will use a lightning-card to display the results dynamically once the data is returned from Apex.

<template>
    <lightning-card title="IP Geolocation Tracker" icon-name="standard:location">
        
        <div class="slds-p-around_medium">
            <!-- Input Area -->
            <lightning-layout vertical-align="end" class="slds-m-bottom_medium">
                <lightning-layout-item padding="around-small" size="8">
                    <lightning-input type="text" label="Enter IP Address" value={ipInput} onchange={handleInputChange} placeholder="e.g. 8.8.8.8"></lightning-input>
                </lightning-layout-item>
                <lightning-layout-item padding="around-small" size="4">
                    <lightning-button label="Get Details" variant="brand" onclick={fetchDetails} disabled={isLoading}></lightning-button>
                </lightning-layout-item>
            </lightning-layout>

            <!-- Loading Spinner -->
            <template if:true={isLoading}>
                <lightning-spinner alternative-text="Loading" size="small"></lightning-spinner>
            </template>

            <!-- Error Message -->
            <template if:true={errorMsg}>
                <div class="slds-text-color_error slds-m-bottom_medium">
                    {errorMsg}
                </div>
            </template>

            <!-- Results Area -->
            <template if:true={ipData}>
                <div class="slds-box slds-theme_default">
                    <p><strong>IP:</strong> {ipData.ip}</p>
                    <p><strong>City:</strong> {ipData.city}</p>
                    <p><strong>Region:</strong> {ipData.region}</p>
                    <p><strong>Country:</strong> {ipData.country_name} ({ipData.country_code})</p>
                    <p><strong>Zip/Postal:</strong> {ipData.postal}</p>
                    <p><strong>Time Zone:</strong> {ipData.timezone}</p>
                    <p><strong>Coordinates:</strong> {ipData.latitude}, {ipData.longitude}</p>
                </div>
            </template>
        </div>
        
    </lightning-card>
</template>

Step 3: Wire it all together with JavaScript

The JavaScript controller imports our Apex method, handles the input changes, and imperatively calls the backend when the user clicks the button.

import { LightningElement } from 'lwc';
import getIPDetails from '@salesforce/apex/IPLocationTracker.getIPDetails';

export default class IpTracker extends LightningElement {
    ipInput = '';
    ipData;
    errorMsg;
    isLoading = false;

    // Track user input
    handleInputChange(event) {
        this.ipInput = event.target.value;
    }

    // Call Apex imperatively
    fetchDetails() {
        this.isLoading = true;
        this.ipData = null;
        this.errorMsg = null;

        getIPDetails({ ipAddress: this.ipInput })
            .then(result => {
                this.ipData = result;
                this.isLoading = false;
            })
            .catch(error => {
                this.errorMsg = 'Error fetching details: ' + (error.body ? error.body.message : error.message);
                this.isLoading = false;
            });
    }
}
Common Developer Pitfalls & Security Traps:
  • Missing Named Credentials: While older tutorials recommend setting up "Remote Site Settings," the modern best practice is to configure a Named Credential in Setup. This securely manages authentication and base URLs, removing hardcoded endpoints from your Apex class.
  • LWC Wrapper Visibility: In Apex, if you create a custom wrapper class (like IPDetails) but forget to add @AuraEnabled to its individual properties, the LWC will receive an empty object {}.
  • CORS Issues with JS Fetch: You might be tempted to use the native JavaScript fetch() API directly inside your LWC to avoid Apex entirely. However, Salesforce Lightning blocks unauthorized external calls via Content Security Policy (CSP) and CORS rules. Calling out via Apex avoids these strict browser blocks.

Frequently Asked Questions (FAQ)

Q: Can I use @wire instead of calling the Apex method imperatively?

A: You can, but it is not recommended for this use case. Because the user determines exactly when the callout should fire (by clicking a button after typing an IP), an imperative Apex call (using .then() and .catch()) gives you much better control over loading spinners and error handling than the automatic @wire service.

Q: Why did my Apex exception not show a proper message in the LWC?

A: Standard exceptions (like CalloutException) are often masked by the Salesforce framework for security purposes when passed to an LWC, showing a generic "Server Error". Always catch your standard exceptions in Apex and throw a new AuraHandledException with your custom message to pass the error cleanly to the frontend.

Always execute HTTP callouts to external APIs asynchronously using Imperative Apex, rather than `@wire`, to ensure you can control loading states and handle unpredictable timeout errors gracefully.
360 Summary Card
  • Frontend Framework: Lightning Web Components (LWC)
  • Callout Execution: Imperative Apex (triggered by a button click)
  • Parsing Method: JSON.deserialize() mapped to an @AuraEnabled Wrapper Class
  • Security Standard: Use Named Credentials instead of hardcoded URLs and Remote Site Settings.