Inaccurate customer addresses lead to delivery delays, bad tax calculations, and dirty CRM data. While Salesforce provides native custom address fields, verifying real-time global addresses and fetching geolocation coordinates requires an external location intelligence service. Integrating the Google Maps Geocoding & Places API into Salesforce solves this by giving users autocomplete validation, street-level accuracy, and clean data parsing directly in their CRM workflows.
1. Architecture Overview: Google Maps API & Salesforce Callouts
Connecting Salesforce to the Google Maps Geocoding service involves three main components:
- Google Cloud Platform (GCP) Project: Provides a secure API Key authorized specifically for the Geocoding API and Places API.
- Endpoint Security Tier: Configures Remote Site Settings or Salesforce Named Credentials to whitelist outbound HTTPS traffic to Google services.
- Apex Service Engine: Constructs the URL-encoded HTTP request, validates HTTP status codes, parses the returned JSON payload into Apex wrapper classes, and maps validated components back to target sObject fields.
- Endpoint:
https://maps.googleapis.com/maps/api/geocode/json - Method: HTTPS
GETwith URL-encoded query parameters. - Response Format: Structured JSON containing address components, formatted strings, and latitude/longitude geometry.
- Modern Security Standard: Store API Keys in Named Credentials / External Credentials or Protected Custom Metadata (never hardcode in Apex).
2. Step-by-Step Implementation: Building the Apex Service
Navigate to
Setup > Security > Remote Site Settings and click New Remote Site. Provide a name (e.g., Google_Maps_API) and set the URL to https://maps.googleapis.com.
The following service class safely encodes user input, handles callout execution, and parses key address attributes into a structured wrapper.
public with sharing class GoogleAddressValidationService {
// In production, retrieve from Named Credentials or Protected Custom Metadata
private static final String GOOGLE_API_KEY = 'YOUR_GOOGLE_MAPS_API_KEY';
private static final String GEOCODE_ENDPOINT = 'https://maps.googleapis.com/maps/api/geocode/json';
// Wrapper class to structure the parsed address response
public class ValidatedAddress {
@AuraEnabled public String formattedAddress { get; set; }
@AuraEnabled public String streetNumber { get; set; }
@AuraEnabled public String route { get; set; }
@AuraEnabled public String city { get; set; }
@AuraEnabled public String state { get; set; }
@AuraEnabled public String postalCode { get; set; }
@AuraEnabled public String country { get; set; }
@AuraEnabled public Decimal latitude { get; set; }
@AuraEnabled public Decimal longitude { get; set; }
}
@AuraEnabled(cacheable=true)
public static ValidatedAddress searchAndGeocodeAddress(String rawAddress) {
if (String.isBlank(rawAddress)) {
throw new IllegalArgumentException('Address search parameter cannot be blank.');
}
String encodedAddress = EncodingUtil.urlEncode(rawAddress.trim(), 'UTF-8');
String endpointUrl = GEOCODE_ENDPOINT + '?address=' + encodedAddress + '&key=' + GOOGLE_API_KEY;
HttpRequest req = new HttpRequest();
req.setEndpoint(endpointUrl);
req.setMethod('GET');
req.setHeader('Accept', 'application/json');
req.setTimeout(10000); // 10-second timeout
Http http = new Http();
HttpResponse res = http.send(req);
if (res.getStatusCode() == 200) {
return parseGoogleResponse(res.getBody());
} else {
throw new CalloutException('Google API Error: HTTP ' + res.getStatusCode() + ' - ' + res.getStatus());
}
}
private static ValidatedAddress parseGoogleResponse(String jsonResponse) {
ValidatedAddress result = new ValidatedAddress();
Map<String, Object> root = (Map<String, Object>) JSON.deserializeUntyped(jsonResponse);
String status = (String) root.get('status');
if (status != 'OK') {
throw new CalloutException('Google Geocoding failed with status: ' + status);
}
List<Object> resultsList = (List<Object>) root.get('results');
if (resultsList == null || resultsList.isEmpty()) {
return result;
}
Map<String, Object> firstResult = (Map<String, Object>) resultsList[0];
result.formattedAddress = (String) firstResult.get('formatted_address');
// Extract Latitude and Longitude
Map<String, Object> geometry = (Map<String, Object>) firstResult.get('geometry');
if (geometry != null && geometry.containsKey('location')) {
Map<String, Object> location = (Map<String, Object>) geometry.get('location');
result.latitude = (Decimal) location.get('lat');
result.longitude = (Decimal) location.get('lng');
}
// Parse individual address components
List<Object> components = (List<Object>) firstResult.get('address_components');
if (components != null) {
for (Object compObj : components) {
Map<String, Object> comp = (Map<String, Object>) compObj;
List<Object> types = (List<Object>) comp.get('types');
String longName = (String) comp.get('long_name');
String shortName = (String) comp.get('short_name');
if (types.contains('street_number')) {
result.streetNumber = longName;
} else if (types.contains('route')) {
result.route = longName;
} else if (types.contains('locality')) {
result.city = longName;
} else if (types.contains('administrative_area_level_1')) {
result.state = shortName;
} else if (types.contains('postal_code')) {
result.postalCode = longName;
} else if (types.contains('country')) {
result.country = longName;
}
}
}
return result;
}
}
3. Unit Testing with HttpCalloutMock
Salesforce prohibits live HTTP callouts in unit tests. To deploy your integration, create a mock response provider implementing the HttpCalloutMock interface:
@isTest
public class GoogleAddressValidationMock implements HttpCalloutMock {
public HttpResponse respond(HttpRequest req) {
HttpResponse res = new HttpResponse();
res.setHeader('Content-Type', 'application/json');
res.setStatusCode(200);
res.setBody('{' +
'"status": "OK",' +
'"results": [{' +
'"formatted_address": "1600 Amphitheatre Pkwy, Mountain View, CA 94043, USA",' +
'"geometry": { "location": { "lat": 37.4224764, "lng": -122.0842499 } },' +
'"address_components": [' +
'{"long_name": "1600", "short_name": "1600", "types": ["street_number"]},' +
'{"long_name": "Amphitheatre Pkwy", "short_name": "Amphitheatre Pkwy", "types": ["route"]},' +
'{"long_name": "Mountain View", "short_name": "Mountain View", "types": ["locality"]},' +
'{"long_name": "California", "short_name": "CA", "types": ["administrative_area_level_1"]},' +
'{"long_name": "94043", "short_name": "94043", "types": ["postal_code"]},' +
'{"long_name": "United States", "short_name": "US", "types": ["country"]}' +
']' +
'}]' +
'}');
return res;
}
}
4. Common Traps & Security Best Practices
Placing raw Google API keys inside Apex string constants or client-side JavaScript exposes credentials in version control and sandboxes. Furthermore, an unrestricted Google API key can be extracted and abused by third parties. Always restrict your Google Cloud API key by HTTP Referrer / IP Address and enable only the Geocoding API and Places API on that key.
- Always URL-Encode Parameters: Raw street addresses contain spaces, commas, and hash symbols (e.g.,
#4B). Forgetting to encode inputs will produce broken HTTP requests. - Handle Quota Limits Gracefully: Check for Google API error statuses like
OVER_QUERY_LIMITorREQUEST_DENIEDin your Apex logic and provide user-friendly alerts. - Leverage Salesforce Address Auto-Complete: For purely declarative address search in standard forms, consider pairing Apex validation with Salesforce's native Address Data Service feature in Setup.
Summary
Integrating Google Address and Geocoding APIs with Salesforce ensures clean, standardized data entry across lead capture, customer onboarding, and logistics workflows. By structuring Apex HTTP callouts safely, deserializing nested JSON components into Apex wrappers, and securing credentials through platform best practices, developers can deliver accurate address validation across the entire Salesforce platform.